ecb-exchange-rates-ts 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ntelikatos
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,172 @@
1
+ # ecb-exchange-rates-ts
2
+
3
+ A typed TypeScript wrapper for the **European Central Bank** exchange rates SDMX API. Zero dependencies, production-ready, fully typed.
4
+
5
+ ## Features
6
+
7
+ - **Zero dependencies** — uses only native `fetch` (Node.js 18+)
8
+ - **Fully typed** — strict TypeScript with exported types for everything
9
+ - **Configurable base currency** — defaults to EUR, but can be changed per-client or per-query
10
+ - **Dual ESM/CJS** — works with `import` and `require`
11
+ - **SOLID architecture** — modular, testable, extensible
12
+ - **Dependency injection** — swap HTTP fetcher for testing or custom transports
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install ecb-exchange-rates-ts
18
+ # or
19
+ pnpm add ecb-exchange-rates-ts
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```ts
25
+ import { EcbClient } from "ecb-exchange-rates-ts";
26
+
27
+ const ecb = new EcbClient();
28
+
29
+ // Get a rate for a specific date
30
+ const result = await ecb.getRate("USD", "2025-01-15");
31
+ console.log(result.rates.get("2025-01-15")); // 1.03
32
+
33
+ // Convert 100 of the base currency to USD
34
+ const conversion = await ecb.convert(100, "USD", "2025-01-15");
35
+ console.log(conversion); // { amount: 103, rate: 1.03, date: "2025-01-15", currency: "USD" }
36
+
37
+ // Get rate history
38
+ const history = await ecb.getRateHistory("USD", "2025-01-01", "2025-01-31");
39
+ for (const [date, rate] of history.rates) {
40
+ console.log(`${date}: ${rate}`);
41
+ }
42
+
43
+ // Multiple currencies at once
44
+ const multi = await ecb.getRates({
45
+ currencies: ["USD", "GBP", "JPY"],
46
+ startDate: "2025-01-01",
47
+ endDate: "2025-01-31",
48
+ });
49
+ for (const [date, rates] of multi.rates) {
50
+ console.log(`${date}: USD=${rates.USD}, GBP=${rates.GBP}, JPY=${rates.JPY}`);
51
+ }
52
+
53
+ // Raw observations (full control)
54
+ const observations = await ecb.getObservations({
55
+ currencies: ["USD"],
56
+ startDate: "2025-01-01",
57
+ endDate: "2025-01-31",
58
+ frequency: "D",
59
+ });
60
+ ```
61
+
62
+ ## API Reference
63
+
64
+ ### `EcbClient`
65
+
66
+ #### Constructor
67
+
68
+ ```ts
69
+ new EcbClient(config?: EcbClientConfig)
70
+ ```
71
+
72
+ | Option | Type | Default | Description |
73
+ | -------------- | ---------- | ---------------------------------------- | ----------------------------------- |
74
+ | `baseCurrency` | `string` | `"EUR"` | Default base (denomination) currency |
75
+ | `baseUrl` | `string` | `https://data-api.ecb.europa.eu/service` | ECB API base URL |
76
+ | `timeoutMs` | `number` | `30000` | Request timeout in ms |
77
+ | `fetchFn` | `Function` | `globalThis.fetch` | Custom fetch for DI |
78
+
79
+ #### Methods
80
+
81
+ | Method | Description | Returns |
82
+ | ----------------- | ------------------------------------------------- | ---------------------------------- |
83
+ | `getRate` | Single currency, single date | `ExchangeRateResult` |
84
+ | `getRateHistory` | Single currency, date range | `ExchangeRateResult` |
85
+ | `getRates` | Multiple currencies, date range | `ExchangeRatesResult` |
86
+ | `getObservations` | Raw observation array | `ExchangeRateObservation[]` |
87
+ | `convert` | Convert base currency amount to target currency | `{ amount, rate, date, currency }` |
88
+
89
+ #### Static Factory
90
+
91
+ ```ts
92
+ // Inject a custom HTTP fetcher (useful for testing)
93
+ const client = EcbClient.withFetcher(myCustomFetcher, { baseCurrency: "USD" });
94
+ ```
95
+
96
+ ### Base Currency Configuration
97
+
98
+ The base currency defaults to `"EUR"` but can be configured at three levels:
99
+
100
+ ```ts
101
+ // 1. Client-level default
102
+ const client = new EcbClient({ baseCurrency: "USD" });
103
+
104
+ // 2. Per-query override
105
+ const result = await client.getRates({
106
+ currencies: ["GBP", "JPY"],
107
+ startDate: "2025-01-15",
108
+ baseCurrency: "CHF", // overrides client default for this query
109
+ });
110
+
111
+ // 3. The result's `base` field is derived from the API response
112
+ console.log(result.base); // "CHF"
113
+ ```
114
+
115
+ ## Architecture
116
+
117
+ ```
118
+ src/
119
+ ├── types/ # Type definitions (interfaces, SDMX-JSON shapes)
120
+ ├── errors/ # Error hierarchy (EcbError -> EcbApiError, EcbNetworkError, etc.)
121
+ ├── parsers/ # SDMX-JSON response parser
122
+ ├── services/ # HTTP fetcher abstraction
123
+ ├── utils/ # URL builder, query validation
124
+ ├── client.ts # Main EcbClient facade
125
+ └── index.ts # Public API barrel export
126
+ ```
127
+
128
+ ### SOLID Principles
129
+
130
+ - **S** — Each module has a single responsibility (parsing, fetching, validating, URL building)
131
+ - **O** — New formats/transports can be added without modifying existing code
132
+ - **L** — All error types extend `EcbError` and are interchangeable
133
+ - **I** — `HttpFetcher` interface exposes only what consumers need
134
+ - **D** — `EcbClient` depends on the `HttpFetcher` abstraction, not `fetch` directly
135
+
136
+ ## Error Handling
137
+
138
+ ```ts
139
+ import { EcbApiError, EcbNetworkError, EcbValidationError } from "ecb-exchange-rates-ts";
140
+
141
+ try {
142
+ const result = await ecb.getRate("USD", "2025-01-15");
143
+ } catch (error) {
144
+ if (error instanceof EcbValidationError) {
145
+ // Invalid query parameters
146
+ } else if (error instanceof EcbApiError) {
147
+ // HTTP error from ECB (error.statusCode, error.statusText)
148
+ } else if (error instanceof EcbNetworkError) {
149
+ // Network failure / timeout
150
+ }
151
+ }
152
+ ```
153
+
154
+ ## Testing
155
+
156
+ ```bash
157
+ pnpm test # Run tests
158
+ pnpm test:coverage # Run tests with coverage
159
+ pnpm typecheck # Type check
160
+ pnpm lint # Lint + format check
161
+ ```
162
+
163
+ ## Notes
164
+
165
+ - **No API key required** — the ECB API is free and open access.
166
+ - **No weekend/holiday data** — the ECB only publishes rates on TARGET business days.
167
+ - **Historical data from 1999** — data is available from January 4, 1999.
168
+ - **Rates published at ~16:00 CET** — reference rates are set daily around 16:00 CET.
169
+
170
+ ## License
171
+
172
+ [MIT](LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,359 @@
1
+ 'use strict';
2
+
3
+ // src/errors/index.ts
4
+ var EcbError = class extends Error {
5
+ code;
6
+ constructor(message, code, options) {
7
+ super(message, options);
8
+ this.name = "EcbError";
9
+ this.code = code;
10
+ }
11
+ };
12
+ var EcbApiError = class extends EcbError {
13
+ statusCode;
14
+ statusText;
15
+ constructor(statusCode, statusText, body) {
16
+ const message = `ECB API returned ${statusCode} ${statusText}${body ? `: ${body}` : ""}`;
17
+ super(message, "ECB_API_ERROR");
18
+ this.name = "EcbApiError";
19
+ this.statusCode = statusCode;
20
+ this.statusText = statusText;
21
+ }
22
+ };
23
+ var EcbNetworkError = class extends EcbError {
24
+ constructor(message, cause) {
25
+ super(message, "ECB_NETWORK_ERROR", { cause });
26
+ this.name = "EcbNetworkError";
27
+ }
28
+ };
29
+ var EcbParseError = class extends EcbError {
30
+ constructor(message, cause) {
31
+ super(message, "ECB_PARSE_ERROR", { cause });
32
+ this.name = "EcbParseError";
33
+ }
34
+ };
35
+ var EcbValidationError = class extends EcbError {
36
+ constructor(message) {
37
+ super(message, "ECB_VALIDATION_ERROR");
38
+ this.name = "EcbValidationError";
39
+ }
40
+ };
41
+
42
+ // src/parsers/sdmx-json-parser.ts
43
+ function parseJsonResponse(data) {
44
+ const dataSets = data.dataSets;
45
+ if (!dataSets || dataSets.length === 0) {
46
+ return [];
47
+ }
48
+ const structure = data.structure;
49
+ if (!structure?.dimensions) {
50
+ throw new EcbParseError("SDMX-JSON response missing structure.dimensions.");
51
+ }
52
+ const seriesDims = structure.dimensions.series;
53
+ const obsDims = structure.dimensions.observation;
54
+ const currencyDimIndex = findDimensionIndex(seriesDims, "CURRENCY");
55
+ const denomDimIndex = findDimensionIndex(seriesDims, "CURRENCY_DENOM");
56
+ const timeDimIndex = findObservationDimensionIndex(obsDims, "TIME_PERIOD");
57
+ const currencyValues = seriesDims[currencyDimIndex]?.values;
58
+ const denomValues = seriesDims[denomDimIndex]?.values;
59
+ const timeValues = obsDims[timeDimIndex]?.values;
60
+ if (!currencyValues || !denomValues || !timeValues) {
61
+ throw new EcbParseError("SDMX-JSON response missing dimension values.");
62
+ }
63
+ const observations = [];
64
+ for (const dataSet of dataSets) {
65
+ const series = dataSet.series;
66
+ if (!series) continue;
67
+ for (const [seriesKey, seriesData] of Object.entries(series)) {
68
+ const dimIndices = seriesKey.split(":").map(Number);
69
+ const currencyIdx = dimIndices[currencyDimIndex];
70
+ const denomIdx = dimIndices[denomDimIndex];
71
+ if (currencyIdx === void 0 || denomIdx === void 0) {
72
+ continue;
73
+ }
74
+ const currency = currencyValues[currencyIdx]?.id;
75
+ const baseCurrency = denomValues[denomIdx]?.id;
76
+ if (!currency || !baseCurrency) continue;
77
+ for (const [obsKey, obsValues] of Object.entries(seriesData.observations)) {
78
+ const timeIdx = Number(obsKey);
79
+ const date = timeValues[timeIdx]?.id;
80
+ const rate = obsValues[0];
81
+ if (!date || rate === null || rate === void 0) continue;
82
+ observations.push({ date, currency, baseCurrency, rate });
83
+ }
84
+ }
85
+ }
86
+ return observations;
87
+ }
88
+ function parseSdmxJson(raw) {
89
+ try {
90
+ return JSON.parse(raw);
91
+ } catch (error) {
92
+ throw new EcbParseError("Failed to parse ECB JSON response.", error);
93
+ }
94
+ }
95
+ function findDimensionIndex(dims, id) {
96
+ const index = dims.findIndex((d) => d.id === id);
97
+ if (index === -1) {
98
+ throw new EcbParseError(`Missing series dimension "${id}" in SDMX-JSON structure.`);
99
+ }
100
+ return index;
101
+ }
102
+ function findObservationDimensionIndex(dims, id) {
103
+ const index = dims.findIndex((d) => d.id === id);
104
+ if (index === -1) {
105
+ throw new EcbParseError(`Missing observation dimension "${id}" in SDMX-JSON structure.`);
106
+ }
107
+ return index;
108
+ }
109
+
110
+ // src/services/http-fetcher.ts
111
+ var FetchHttpFetcher = class {
112
+ fetchFn;
113
+ timeoutMs;
114
+ constructor(fetchFn = globalThis.fetch.bind(globalThis), timeoutMs = 3e4) {
115
+ this.fetchFn = fetchFn;
116
+ this.timeoutMs = timeoutMs;
117
+ }
118
+ async get(url, externalSignal) {
119
+ const timeoutController = new AbortController();
120
+ const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);
121
+ const signal = externalSignal ? AbortSignal.any([externalSignal, timeoutController.signal]) : timeoutController.signal;
122
+ try {
123
+ const response = await this.fetchFn(url, {
124
+ method: "GET",
125
+ headers: { Accept: "application/json" },
126
+ signal
127
+ });
128
+ if (!response.ok) {
129
+ const body = await response.text().catch(() => "");
130
+ throw new EcbApiError(response.status, response.statusText, body);
131
+ }
132
+ return await response.text();
133
+ } catch (error) {
134
+ if (error instanceof EcbApiError) {
135
+ throw error;
136
+ }
137
+ if (error instanceof Error && error.name === "AbortError") {
138
+ throw new EcbNetworkError(`Request timed out after ${this.timeoutMs}ms: ${url}`, error);
139
+ }
140
+ throw new EcbNetworkError(
141
+ `Network request failed for ${url}: ${error instanceof Error ? error.message : String(error)}`,
142
+ error
143
+ );
144
+ } finally {
145
+ clearTimeout(timeoutId);
146
+ }
147
+ }
148
+ };
149
+
150
+ // src/utils/url-builder.ts
151
+ var DEFAULT_BASE_URL = "https://data-api.ecb.europa.eu/service";
152
+ function buildExchangeRateUrl(query, baseUrl = DEFAULT_BASE_URL) {
153
+ const frequency = query.frequency ?? "D";
154
+ const currencyKey = query.currencies.join("+");
155
+ const baseCurrency = query.baseCurrency ?? "EUR";
156
+ const seriesKey = `${frequency}.${currencyKey}.${baseCurrency}.SP00.A`;
157
+ const url = new URL(`${baseUrl}/data/EXR/${seriesKey}`);
158
+ url.searchParams.set("startPeriod", query.startDate);
159
+ if (query.endDate !== void 0) {
160
+ url.searchParams.set("endPeriod", query.endDate);
161
+ }
162
+ return url.toString();
163
+ }
164
+
165
+ // src/utils/validation.ts
166
+ var ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
167
+ var CURRENCY_REGEX = /^[A-Z]{3}$/;
168
+ function validateQuery(query) {
169
+ if (query.currencies.length === 0) {
170
+ throw new EcbValidationError("At least one currency must be specified.");
171
+ }
172
+ for (const currency of query.currencies) {
173
+ if (!CURRENCY_REGEX.test(currency)) {
174
+ throw new EcbValidationError(
175
+ `Invalid currency code "${currency}". Must be a 3-letter ISO 4217 code.`
176
+ );
177
+ }
178
+ }
179
+ if (!ISO_DATE_REGEX.test(query.startDate)) {
180
+ throw new EcbValidationError(
181
+ `Invalid startDate "${query.startDate}". Expected format: YYYY-MM-DD.`
182
+ );
183
+ }
184
+ if (query.endDate !== void 0 && !ISO_DATE_REGEX.test(query.endDate)) {
185
+ throw new EcbValidationError(
186
+ `Invalid endDate "${query.endDate}". Expected format: YYYY-MM-DD.`
187
+ );
188
+ }
189
+ if (query.endDate !== void 0 && query.startDate > query.endDate) {
190
+ throw new EcbValidationError(
191
+ `startDate "${query.startDate}" must not be after endDate "${query.endDate}".`
192
+ );
193
+ }
194
+ if (query.baseCurrency !== void 0 && !CURRENCY_REGEX.test(query.baseCurrency)) {
195
+ throw new EcbValidationError(
196
+ `Invalid baseCurrency "${query.baseCurrency}". Must be a 3-letter ISO 4217 code.`
197
+ );
198
+ }
199
+ }
200
+
201
+ // src/client.ts
202
+ var DEFAULT_BASE_URL2 = "https://data-api.ecb.europa.eu/service";
203
+ var DEFAULT_BASE_CURRENCY = "EUR";
204
+ var EcbClient = class _EcbClient {
205
+ baseUrl;
206
+ baseCurrency;
207
+ fetcher;
208
+ constructor(config = {}) {
209
+ this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL2;
210
+ this.baseCurrency = config.baseCurrency ?? DEFAULT_BASE_CURRENCY;
211
+ this.fetcher = new FetchHttpFetcher(config.fetchFn, config.timeoutMs);
212
+ }
213
+ /**
214
+ * Creates an EcbClient with a custom HttpFetcher implementation.
215
+ * Useful for testing or when you need full control over HTTP behavior.
216
+ */
217
+ static withFetcher(fetcher, config = {}) {
218
+ const client = new _EcbClient(config);
219
+ Object.defineProperty(client, "fetcher", { value: fetcher });
220
+ return client;
221
+ }
222
+ /**
223
+ * Get the exchange rate for a single currency against the base currency.
224
+ *
225
+ * @param currency - Target currency code (e.g. "USD")
226
+ * @param date - The date to query (YYYY-MM-DD). Returns the nearest available rate.
227
+ */
228
+ async getRate(currency, date) {
229
+ const query = {
230
+ currencies: [currency],
231
+ startDate: date,
232
+ endDate: date
233
+ };
234
+ return this.getSingleCurrencyRates(query);
235
+ }
236
+ /**
237
+ * Get the exchange rate for a single currency over a date range.
238
+ *
239
+ * @param currency - Target currency code (e.g. "USD")
240
+ * @param startDate - Start date (YYYY-MM-DD)
241
+ * @param endDate - End date (YYYY-MM-DD)
242
+ * @param frequency - Data frequency (default: "D" daily)
243
+ */
244
+ async getRateHistory(currency, startDate, endDate, frequency = "D") {
245
+ const query = {
246
+ currencies: [currency],
247
+ startDate,
248
+ endDate,
249
+ frequency
250
+ };
251
+ return this.getSingleCurrencyRates(query);
252
+ }
253
+ /**
254
+ * Get exchange rates for multiple currencies.
255
+ *
256
+ * @param query - The query parameters
257
+ */
258
+ async getRates(query) {
259
+ const resolved = this.resolveQuery(query);
260
+ validateQuery(resolved);
261
+ const observations = await this.fetchAndParse(resolved);
262
+ return this.transformToMultiCurrencyResult(observations, resolved);
263
+ }
264
+ /**
265
+ * Get raw observations — useful when you need full control over the data.
266
+ *
267
+ * @param query - The query parameters
268
+ */
269
+ async getObservations(query) {
270
+ const resolved = this.resolveQuery(query);
271
+ validateQuery(resolved);
272
+ return this.fetchAndParse(resolved);
273
+ }
274
+ /**
275
+ * Convert an amount from the base currency to a target currency at a specific date.
276
+ *
277
+ * @param amount - The amount in the base currency
278
+ * @param currency - Target currency code
279
+ * @param date - The date for the exchange rate (YYYY-MM-DD)
280
+ * @returns The converted amount, or null if no rate is available
281
+ */
282
+ async convert(amount, currency, date) {
283
+ const result = await this.getRate(currency, date);
284
+ const firstEntry = result.rates.entries().next();
285
+ if (firstEntry.done) {
286
+ return null;
287
+ }
288
+ const [actualDate, rate] = firstEntry.value;
289
+ return {
290
+ amount: Math.round(amount * rate * 100) / 100,
291
+ rate,
292
+ date: actualDate,
293
+ currency
294
+ };
295
+ }
296
+ // ── Private helpers ──────────────────────────────────────────────────
297
+ /**
298
+ * Resolves a query by applying the client's default baseCurrency
299
+ * when the query doesn't specify one.
300
+ */
301
+ resolveQuery(query) {
302
+ if (query.baseCurrency !== void 0) {
303
+ return query;
304
+ }
305
+ return { ...query, baseCurrency: this.baseCurrency };
306
+ }
307
+ async fetchAndParse(query) {
308
+ const url = buildExchangeRateUrl(query, this.baseUrl);
309
+ const raw = await this.fetcher.get(url);
310
+ const json = parseSdmxJson(raw);
311
+ return parseJsonResponse(json);
312
+ }
313
+ async getSingleCurrencyRates(query) {
314
+ const resolved = this.resolveQuery(query);
315
+ validateQuery(resolved);
316
+ const observations = await this.fetchAndParse(resolved);
317
+ const rates = /* @__PURE__ */ new Map();
318
+ for (const obs of observations) {
319
+ rates.set(obs.date, obs.rate);
320
+ }
321
+ const base = observations[0]?.baseCurrency ?? resolved.baseCurrency ?? this.baseCurrency;
322
+ return {
323
+ base,
324
+ currency: resolved.currencies[0],
325
+ rates
326
+ };
327
+ }
328
+ transformToMultiCurrencyResult(observations, query) {
329
+ const rates = /* @__PURE__ */ new Map();
330
+ for (const obs of observations) {
331
+ let dateRates = rates.get(obs.date);
332
+ if (!dateRates) {
333
+ dateRates = {};
334
+ rates.set(obs.date, dateRates);
335
+ }
336
+ dateRates[obs.currency] = obs.rate;
337
+ }
338
+ const base = observations[0]?.baseCurrency ?? query.baseCurrency ?? this.baseCurrency;
339
+ return {
340
+ base,
341
+ currencies: query.currencies,
342
+ rates
343
+ };
344
+ }
345
+ };
346
+
347
+ exports.EcbApiError = EcbApiError;
348
+ exports.EcbClient = EcbClient;
349
+ exports.EcbError = EcbError;
350
+ exports.EcbNetworkError = EcbNetworkError;
351
+ exports.EcbParseError = EcbParseError;
352
+ exports.EcbValidationError = EcbValidationError;
353
+ exports.FetchHttpFetcher = FetchHttpFetcher;
354
+ exports.buildExchangeRateUrl = buildExchangeRateUrl;
355
+ exports.parseJsonResponse = parseJsonResponse;
356
+ exports.parseSdmxJson = parseSdmxJson;
357
+ exports.validateQuery = validateQuery;
358
+ //# sourceMappingURL=index.cjs.map
359
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors/index.ts","../src/parsers/sdmx-json-parser.ts","../src/services/http-fetcher.ts","../src/utils/url-builder.ts","../src/utils/validation.ts","../src/client.ts"],"names":["DEFAULT_BASE_URL"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Base error for all ECB client errors.\n * Follows the Liskov Substitution Principle — all subtypes are interchangeable.\n */\nexport class EcbError extends Error {\n public readonly code: string;\n\n constructor(message: string, code: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"EcbError\";\n this.code = code;\n }\n}\n\n/**\n * Thrown when the ECB API returns an HTTP error.\n */\nexport class EcbApiError extends EcbError {\n public readonly statusCode: number;\n public readonly statusText: string;\n\n constructor(statusCode: number, statusText: string, body?: string) {\n const message = `ECB API returned ${statusCode} ${statusText}${body ? `: ${body}` : \"\"}`;\n super(message, \"ECB_API_ERROR\");\n this.name = \"EcbApiError\";\n this.statusCode = statusCode;\n this.statusText = statusText;\n }\n}\n\n/**\n * Thrown when the network request fails (timeout, DNS, connection refused, etc.).\n */\nexport class EcbNetworkError extends EcbError {\n constructor(message: string, cause?: unknown) {\n super(message, \"ECB_NETWORK_ERROR\", { cause });\n this.name = \"EcbNetworkError\";\n }\n}\n\n/**\n * Thrown when the API response cannot be parsed.\n */\nexport class EcbParseError extends EcbError {\n constructor(message: string, cause?: unknown) {\n super(message, \"ECB_PARSE_ERROR\", { cause });\n this.name = \"EcbParseError\";\n }\n}\n\n/**\n * Thrown when query parameters are invalid.\n */\nexport class EcbValidationError extends EcbError {\n constructor(message: string) {\n super(message, \"ECB_VALIDATION_ERROR\");\n this.name = \"EcbValidationError\";\n }\n}\n","import { EcbParseError } from \"../errors/index.js\";\nimport type { ExchangeRateObservation, SdmxJsonResponse, SdmxStructure } from \"../types/index.js\";\n\n/**\n * Parses an SDMX-JSON response from the ECB into typed observations.\n *\n * Single Responsibility: only concerned with SDMX-JSON → typed data transformation.\n *\n * The ECB SDMX-JSON format uses index-based keys:\n * - Series keys (e.g. \"0:0:0:0:0\") map to dimension value indices\n * - Observation keys (e.g. \"0\") map to observation-dimension value indices\n * - Observation values are tuples: [rate, ...attributeIndices]\n *\n * @see https://data.ecb.europa.eu/help/api/data\n */\nexport function parseJsonResponse(data: SdmxJsonResponse): ExchangeRateObservation[] {\n const dataSets = data.dataSets;\n if (!dataSets || dataSets.length === 0) {\n return [];\n }\n\n const structure = data.structure;\n if (!structure?.dimensions) {\n throw new EcbParseError(\"SDMX-JSON response missing structure.dimensions.\");\n }\n\n const seriesDims = structure.dimensions.series;\n const obsDims = structure.dimensions.observation;\n\n const currencyDimIndex = findDimensionIndex(seriesDims, \"CURRENCY\");\n const denomDimIndex = findDimensionIndex(seriesDims, \"CURRENCY_DENOM\");\n const timeDimIndex = findObservationDimensionIndex(obsDims, \"TIME_PERIOD\");\n\n const currencyValues = seriesDims[currencyDimIndex]?.values;\n const denomValues = seriesDims[denomDimIndex]?.values;\n const timeValues = obsDims[timeDimIndex]?.values;\n\n if (!currencyValues || !denomValues || !timeValues) {\n throw new EcbParseError(\"SDMX-JSON response missing dimension values.\");\n }\n\n const observations: ExchangeRateObservation[] = [];\n\n for (const dataSet of dataSets) {\n const series = dataSet.series;\n if (!series) continue;\n\n for (const [seriesKey, seriesData] of Object.entries(series)) {\n const dimIndices = seriesKey.split(\":\").map(Number);\n\n const currencyIdx = dimIndices[currencyDimIndex];\n const denomIdx = dimIndices[denomDimIndex];\n\n if (currencyIdx === undefined || denomIdx === undefined) {\n continue;\n }\n\n const currency = currencyValues[currencyIdx]?.id;\n const baseCurrency = denomValues[denomIdx]?.id;\n\n if (!currency || !baseCurrency) continue;\n\n for (const [obsKey, obsValues] of Object.entries(seriesData.observations)) {\n const timeIdx = Number(obsKey);\n const date = timeValues[timeIdx]?.id;\n const rate = obsValues[0];\n\n if (!date || rate === null || rate === undefined) continue;\n\n observations.push({ date, currency, baseCurrency, rate });\n }\n }\n }\n\n return observations;\n}\n\n/**\n * Safely parses a raw JSON string into an SdmxJsonResponse.\n */\nexport function parseSdmxJson(raw: string): SdmxJsonResponse {\n try {\n return JSON.parse(raw) as SdmxJsonResponse;\n } catch (error) {\n throw new EcbParseError(\"Failed to parse ECB JSON response.\", error);\n }\n}\n\n// ── Private helpers ────────────────────────────────────────────────────\n\nfunction findDimensionIndex(dims: SdmxStructure[\"dimensions\"][\"series\"], id: string): number {\n const index = dims.findIndex((d) => d.id === id);\n if (index === -1) {\n throw new EcbParseError(`Missing series dimension \"${id}\" in SDMX-JSON structure.`);\n }\n return index;\n}\n\nfunction findObservationDimensionIndex(\n dims: SdmxStructure[\"dimensions\"][\"observation\"],\n id: string,\n): number {\n const index = dims.findIndex((d) => d.id === id);\n if (index === -1) {\n throw new EcbParseError(`Missing observation dimension \"${id}\" in SDMX-JSON structure.`);\n }\n return index;\n}\n","import { EcbApiError, EcbNetworkError } from \"../errors/index.js\";\n\n/**\n * Contract for HTTP fetching — enables dependency inversion.\n * Consumers depend on this abstraction, not on concrete fetch implementations.\n */\nexport interface HttpFetcher {\n get(url: string, signal?: AbortSignal): Promise<string>;\n}\n\n/**\n * Default HTTP fetcher using the global `fetch` API.\n * Single Responsibility: only concerned with HTTP GET and error mapping.\n *\n * Dependency Inversion: Accepts `fetch` as a constructor parameter,\n * allowing tests or consumers to inject alternatives.\n */\nexport class FetchHttpFetcher implements HttpFetcher {\n private readonly fetchFn: typeof globalThis.fetch;\n private readonly timeoutMs: number;\n\n constructor(\n fetchFn: typeof globalThis.fetch = globalThis.fetch.bind(globalThis),\n timeoutMs = 30_000,\n ) {\n this.fetchFn = fetchFn;\n this.timeoutMs = timeoutMs;\n }\n\n async get(url: string, externalSignal?: AbortSignal): Promise<string> {\n const timeoutController = new AbortController();\n const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);\n\n // Combine external abort signal with our timeout signal\n const signal = externalSignal\n ? AbortSignal.any([externalSignal, timeoutController.signal])\n : timeoutController.signal;\n\n try {\n const response = await this.fetchFn(url, {\n method: \"GET\",\n headers: { Accept: \"application/json\" },\n signal,\n });\n\n if (!response.ok) {\n const body = await response.text().catch(() => \"\");\n throw new EcbApiError(response.status, response.statusText, body);\n }\n\n return await response.text();\n } catch (error) {\n if (error instanceof EcbApiError) {\n throw error;\n }\n\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new EcbNetworkError(`Request timed out after ${this.timeoutMs}ms: ${url}`, error);\n }\n\n throw new EcbNetworkError(\n `Network request failed for ${url}: ${error instanceof Error ? error.message : String(error)}`,\n error,\n );\n } finally {\n clearTimeout(timeoutId);\n }\n }\n}\n","import type { ExchangeRateQuery } from \"../types/index.js\";\n\nconst DEFAULT_BASE_URL = \"https://data-api.ecb.europa.eu/service\";\n\n/**\n * Builds the ECB SDMX API URL for exchange rate queries.\n * Single Responsibility: only concerned with URL construction.\n *\n * URL pattern: {base}/data/EXR/{freq}.{currencies}.{baseCurrency}.SP00.A?params\n *\n * Content format is controlled via the HTTP Accept header (application/json),\n * not via a URL parameter.\n */\nexport function buildExchangeRateUrl(\n query: ExchangeRateQuery,\n baseUrl: string = DEFAULT_BASE_URL,\n): string {\n const frequency = query.frequency ?? \"D\";\n const currencyKey = query.currencies.join(\"+\");\n const baseCurrency = query.baseCurrency ?? \"EUR\";\n\n // EXR series key: FREQ.CURRENCY.CURRENCY_DENOM.EXR_TYPE.EXR_SUFFIX\n const seriesKey = `${frequency}.${currencyKey}.${baseCurrency}.SP00.A`;\n\n const url = new URL(`${baseUrl}/data/EXR/${seriesKey}`);\n url.searchParams.set(\"startPeriod\", query.startDate);\n\n if (query.endDate !== undefined) {\n url.searchParams.set(\"endPeriod\", query.endDate);\n }\n\n return url.toString();\n}\n","import { EcbValidationError } from \"../errors/index.js\";\nimport type { ExchangeRateQuery } from \"../types/index.js\";\n\nconst ISO_DATE_REGEX = /^\\d{4}-\\d{2}-\\d{2}$/;\nconst CURRENCY_REGEX = /^[A-Z]{3}$/;\n\n/**\n * Validates exchange rate query parameters.\n * Single Responsibility: only concerned with input validation.\n */\nexport function validateQuery(query: ExchangeRateQuery): void {\n if (query.currencies.length === 0) {\n throw new EcbValidationError(\"At least one currency must be specified.\");\n }\n\n for (const currency of query.currencies) {\n if (!CURRENCY_REGEX.test(currency)) {\n throw new EcbValidationError(\n `Invalid currency code \"${currency}\". Must be a 3-letter ISO 4217 code.`,\n );\n }\n }\n\n if (!ISO_DATE_REGEX.test(query.startDate)) {\n throw new EcbValidationError(\n `Invalid startDate \"${query.startDate}\". Expected format: YYYY-MM-DD.`,\n );\n }\n\n if (query.endDate !== undefined && !ISO_DATE_REGEX.test(query.endDate)) {\n throw new EcbValidationError(\n `Invalid endDate \"${query.endDate}\". Expected format: YYYY-MM-DD.`,\n );\n }\n\n if (query.endDate !== undefined && query.startDate > query.endDate) {\n throw new EcbValidationError(\n `startDate \"${query.startDate}\" must not be after endDate \"${query.endDate}\".`,\n );\n }\n\n if (query.baseCurrency !== undefined && !CURRENCY_REGEX.test(query.baseCurrency)) {\n throw new EcbValidationError(\n `Invalid baseCurrency \"${query.baseCurrency}\". Must be a 3-letter ISO 4217 code.`,\n );\n }\n}\n","import { parseJsonResponse, parseSdmxJson } from \"./parsers/sdmx-json-parser.js\";\nimport { FetchHttpFetcher, type HttpFetcher } from \"./services/http-fetcher.js\";\nimport type {\n EcbClientConfig,\n ExchangeRateObservation,\n ExchangeRateQuery,\n ExchangeRateResult,\n ExchangeRatesResult,\n Frequency,\n} from \"./types/index.js\";\nimport { buildExchangeRateUrl } from \"./utils/url-builder.js\";\nimport { validateQuery } from \"./utils/validation.js\";\n\nconst DEFAULT_BASE_URL = \"https://data-api.ecb.europa.eu/service\";\nconst DEFAULT_BASE_CURRENCY = \"EUR\";\n\n/**\n * ECB Exchange Rates Client.\n *\n * Provides a clean, typed interface to the European Central Bank's\n * exchange rate reference data via the SDMX RESTful API.\n *\n * Uses the `Accept: application/json` header to receive SDMX-JSON\n * responses, then parses them into strongly-typed results.\n *\n * Design principles:\n * - **Single Responsibility**: Orchestrates query → fetch → parse → transform.\n * - **Open/Closed**: New data sources can be added via the `HttpFetcher` interface.\n * - **Dependency Inversion**: Depends on `HttpFetcher` abstraction, not `fetch` directly.\n * - **Interface Segregation**: Exposes focused methods for specific use cases.\n *\n * @example\n * ```ts\n * const client = new EcbClient();\n *\n * // Get rate for a specific date (defaults to EUR base)\n * const result = await client.getRate(\"USD\", \"2025-01-15\");\n * console.log(result.rates.get(\"2025-01-15\")); // e.g. 1.03\n *\n * // Use a different base currency\n * const client2 = new EcbClient({ baseCurrency: \"USD\" });\n * ```\n */\nexport class EcbClient {\n private readonly baseUrl: string;\n private readonly baseCurrency: string;\n private readonly fetcher: HttpFetcher;\n\n constructor(config: EcbClientConfig = {}) {\n this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;\n this.baseCurrency = config.baseCurrency ?? DEFAULT_BASE_CURRENCY;\n this.fetcher = new FetchHttpFetcher(config.fetchFn, config.timeoutMs);\n }\n\n /**\n * Creates an EcbClient with a custom HttpFetcher implementation.\n * Useful for testing or when you need full control over HTTP behavior.\n */\n static withFetcher(\n fetcher: HttpFetcher,\n config: Pick<EcbClientConfig, \"baseUrl\" | \"baseCurrency\"> = {},\n ): EcbClient {\n const client = new EcbClient(config);\n Object.defineProperty(client, \"fetcher\", { value: fetcher });\n return client;\n }\n\n /**\n * Get the exchange rate for a single currency against the base currency.\n *\n * @param currency - Target currency code (e.g. \"USD\")\n * @param date - The date to query (YYYY-MM-DD). Returns the nearest available rate.\n */\n async getRate(currency: string, date: string): Promise<ExchangeRateResult> {\n const query: ExchangeRateQuery = {\n currencies: [currency],\n startDate: date,\n endDate: date,\n };\n return this.getSingleCurrencyRates(query);\n }\n\n /**\n * Get the exchange rate for a single currency over a date range.\n *\n * @param currency - Target currency code (e.g. \"USD\")\n * @param startDate - Start date (YYYY-MM-DD)\n * @param endDate - End date (YYYY-MM-DD)\n * @param frequency - Data frequency (default: \"D\" daily)\n */\n async getRateHistory(\n currency: string,\n startDate: string,\n endDate: string,\n frequency: Frequency = \"D\",\n ): Promise<ExchangeRateResult> {\n const query: ExchangeRateQuery = {\n currencies: [currency],\n startDate,\n endDate,\n frequency,\n };\n return this.getSingleCurrencyRates(query);\n }\n\n /**\n * Get exchange rates for multiple currencies.\n *\n * @param query - The query parameters\n */\n async getRates(query: ExchangeRateQuery): Promise<ExchangeRatesResult> {\n const resolved = this.resolveQuery(query);\n validateQuery(resolved);\n\n const observations = await this.fetchAndParse(resolved);\n return this.transformToMultiCurrencyResult(observations, resolved);\n }\n\n /**\n * Get raw observations — useful when you need full control over the data.\n *\n * @param query - The query parameters\n */\n async getObservations(query: ExchangeRateQuery): Promise<ExchangeRateObservation[]> {\n const resolved = this.resolveQuery(query);\n validateQuery(resolved);\n return this.fetchAndParse(resolved);\n }\n\n /**\n * Convert an amount from the base currency to a target currency at a specific date.\n *\n * @param amount - The amount in the base currency\n * @param currency - Target currency code\n * @param date - The date for the exchange rate (YYYY-MM-DD)\n * @returns The converted amount, or null if no rate is available\n */\n async convert(\n amount: number,\n currency: string,\n date: string,\n ): Promise<{ amount: number; rate: number; date: string; currency: string } | null> {\n const result = await this.getRate(currency, date);\n const firstEntry = result.rates.entries().next();\n\n if (firstEntry.done) {\n return null;\n }\n\n const [actualDate, rate] = firstEntry.value;\n\n return {\n amount: Math.round(amount * rate * 100) / 100,\n rate,\n date: actualDate,\n currency,\n };\n }\n\n // ── Private helpers ──────────────────────────────────────────────────\n\n /**\n * Resolves a query by applying the client's default baseCurrency\n * when the query doesn't specify one.\n */\n private resolveQuery(query: ExchangeRateQuery): ExchangeRateQuery {\n if (query.baseCurrency !== undefined) {\n return query;\n }\n return { ...query, baseCurrency: this.baseCurrency };\n }\n\n private async fetchAndParse(query: ExchangeRateQuery): Promise<ExchangeRateObservation[]> {\n const url = buildExchangeRateUrl(query, this.baseUrl);\n const raw = await this.fetcher.get(url);\n const json = parseSdmxJson(raw);\n return parseJsonResponse(json);\n }\n\n private async getSingleCurrencyRates(query: ExchangeRateQuery): Promise<ExchangeRateResult> {\n const resolved = this.resolveQuery(query);\n validateQuery(resolved);\n\n const observations = await this.fetchAndParse(resolved);\n\n const rates = new Map<string, number>();\n for (const obs of observations) {\n rates.set(obs.date, obs.rate);\n }\n\n // Derive base from parsed response when available, fall back to query\n const base = observations[0]?.baseCurrency ?? resolved.baseCurrency ?? this.baseCurrency;\n\n return {\n base,\n currency: resolved.currencies[0] as string,\n rates,\n };\n }\n\n private transformToMultiCurrencyResult(\n observations: ExchangeRateObservation[],\n query: ExchangeRateQuery,\n ): ExchangeRatesResult {\n const rates = new Map<string, Record<string, number>>();\n\n for (const obs of observations) {\n let dateRates = rates.get(obs.date);\n if (!dateRates) {\n dateRates = {};\n rates.set(obs.date, dateRates);\n }\n dateRates[obs.currency] = obs.rate;\n }\n\n // Derive base from parsed response when available, fall back to query\n const base = observations[0]?.baseCurrency ?? query.baseCurrency ?? this.baseCurrency;\n\n return {\n base,\n currencies: query.currencies,\n rates,\n };\n }\n}\n"]}