Skip to content

Commit edd3677

Browse files
karlwaldmanclaude
andcommitted
feat: Add Data Connector support for BYOS prices
Add getDataConnectorPrices() method for fetching prices from connected data sources (BYOS - Bring Your Own Subscription). New types: - DataConnectorPrice: price, currency, fuel_type, port, region, unit, source, timestamp - DataConnectorOptions: fuelType, port, region, since filters Requires Data Connector feature enabled on organization. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ba137ac commit edd3677

3 files changed

Lines changed: 172 additions & 58 deletions

File tree

src/client.ts

Lines changed: 82 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@ import type {
77
Commodity,
88
CommoditiesResponse,
99
CategoriesResponse,
10-
} from './types.js';
10+
DataConnectorPrice,
11+
DataConnectorOptions,
12+
} from "./types.js";
1113
import {
1214
OilPriceAPIError,
1315
AuthenticationError,
1416
RateLimitError,
1517
NotFoundError,
1618
ServerError,
1719
TimeoutError,
18-
} from './errors.js';
19-
import { DieselResource } from './resources/diesel.js';
20-
import { AlertsResource } from './resources/alerts.js';
21-
import { SDK_VERSION, SDK_NAME, buildUserAgent } from './version.js';
20+
} from "./errors.js";
21+
import { DieselResource } from "./resources/diesel.js";
22+
import { AlertsResource } from "./resources/alerts.js";
23+
import { SDK_VERSION, SDK_NAME, buildUserAgent } from "./version.js";
2224

2325
/**
2426
* Official Node.js client for Oil Price API
@@ -69,14 +71,14 @@ export class OilPriceAPI {
6971

7072
constructor(config: OilPriceAPIConfig) {
7173
if (!config.apiKey) {
72-
throw new OilPriceAPIError('API key is required');
74+
throw new OilPriceAPIError("API key is required");
7375
}
7476

7577
this.apiKey = config.apiKey;
76-
this.baseUrl = config.baseUrl || 'https://api.oilpriceapi.com';
78+
this.baseUrl = config.baseUrl || "https://api.oilpriceapi.com";
7779
this.retries = config.retries !== undefined ? config.retries : 3;
7880
this.retryDelay = config.retryDelay || 1000;
79-
this.retryStrategy = config.retryStrategy || 'exponential';
81+
this.retryStrategy = config.retryStrategy || "exponential";
8082
this.timeout = config.timeout || 90000; // 90 seconds for slow historical queries
8183
this.debug = config.debug || false;
8284
this.appUrl = config.appUrl;
@@ -93,7 +95,7 @@ export class OilPriceAPI {
9395
private log(message: string, data?: any): void {
9496
if (this.debug) {
9597
const timestamp = new Date().toISOString();
96-
console.log(`[OilPriceAPI ${timestamp}] ${message}`, data || '');
98+
console.log(`[OilPriceAPI ${timestamp}] ${message}`, data || "");
9799
}
98100
}
99101

@@ -102,11 +104,11 @@ export class OilPriceAPI {
102104
*/
103105
private calculateRetryDelay(attempt: number): number {
104106
switch (this.retryStrategy) {
105-
case 'exponential':
107+
case "exponential":
106108
return this.retryDelay * Math.pow(2, attempt);
107-
case 'linear':
109+
case "linear":
108110
return this.retryDelay * (attempt + 1);
109-
case 'fixed':
111+
case "fixed":
110112
default:
111113
return this.retryDelay;
112114
}
@@ -116,15 +118,15 @@ export class OilPriceAPI {
116118
* Sleep for specified milliseconds
117119
*/
118120
private sleep(ms: number): Promise<void> {
119-
return new Promise(resolve => setTimeout(resolve, ms));
121+
return new Promise((resolve) => setTimeout(resolve, ms));
120122
}
121123

122124
/**
123125
* Determine if error is retryable
124126
*/
125127
private isRetryable(error: any): boolean {
126128
// Retry on network errors
127-
if (error instanceof TypeError && error.message.includes('fetch')) {
129+
if (error instanceof TypeError && error.message.includes("fetch")) {
128130
return true;
129131
}
130132

@@ -152,7 +154,7 @@ export class OilPriceAPI {
152154
*/
153155
private async request<T>(
154156
endpoint: string,
155-
params?: Record<string, string>
157+
params?: Record<string, string>,
156158
): Promise<T> {
157159
// Build URL with query parameters
158160
const url = new URL(`${this.baseUrl}${endpoint}`);
@@ -183,23 +185,23 @@ export class OilPriceAPI {
183185
try {
184186
// Build headers with optional telemetry
185187
const headers: Record<string, string> = {
186-
'Authorization': `Bearer ${this.apiKey}`,
187-
'Content-Type': 'application/json',
188-
'User-Agent': buildUserAgent(),
189-
'X-SDK-Name': SDK_NAME,
190-
'X-SDK-Version': SDK_VERSION,
188+
Authorization: `Bearer ${this.apiKey}`,
189+
"Content-Type": "application/json",
190+
"User-Agent": buildUserAgent(),
191+
"X-SDK-Name": SDK_NAME,
192+
"X-SDK-Version": SDK_VERSION,
191193
};
192194

193195
// Add optional telemetry headers (10% bonus for appUrl!)
194196
if (this.appUrl) {
195-
headers['X-App-URL'] = this.appUrl;
197+
headers["X-App-URL"] = this.appUrl;
196198
}
197199
if (this.appName) {
198-
headers['X-App-Name'] = this.appName;
200+
headers["X-App-Name"] = this.appName;
199201
}
200202

201203
const response = await fetch(url.toString(), {
202-
method: 'GET',
204+
method: "GET",
203205
headers,
204206
signal: controller.signal,
205207
});
@@ -216,7 +218,8 @@ export class OilPriceAPI {
216218
// Try to parse JSON error response
217219
try {
218220
const errorJson = JSON.parse(errorBody);
219-
errorMessage = errorJson.message || errorJson.error || errorMessage;
221+
errorMessage =
222+
errorJson.message || errorJson.error || errorMessage;
220223
} catch {
221224
// Use default error message if response isn't JSON
222225
}
@@ -230,15 +233,17 @@ export class OilPriceAPI {
230233
case 404:
231234
throw new NotFoundError(errorMessage);
232235
case 429:
233-
const retryAfter = response.headers.get('Retry-After');
236+
const retryAfter = response.headers.get("Retry-After");
234237
const rateLimitError = new RateLimitError(
235238
errorMessage,
236-
retryAfter ? parseInt(retryAfter, 10) : undefined
239+
retryAfter ? parseInt(retryAfter, 10) : undefined,
237240
);
238241

239242
// If rate limited and we have retries left, wait and retry
240243
if (attempt < this.retries && rateLimitError.retryAfter) {
241-
this.log(`Rate limited. Waiting ${rateLimitError.retryAfter}s`);
244+
this.log(
245+
`Rate limited. Waiting ${rateLimitError.retryAfter}s`,
246+
);
242247
await this.sleep(rateLimitError.retryAfter * 1000);
243248
continue;
244249
}
@@ -253,51 +258,49 @@ export class OilPriceAPI {
253258
throw new OilPriceAPIError(
254259
errorMessage,
255260
response.status,
256-
'HTTP_ERROR'
261+
"HTTP_ERROR",
257262
);
258263
}
259264
}
260265

261266
// Parse successful response
262267
const responseData: any = await response.json();
263268

264-
this.log('Response data received', {
269+
this.log("Response data received", {
265270
status: responseData.status,
266-
hasData: !!responseData.data
271+
hasData: !!responseData.data,
267272
});
268273

269274
// Handle different response structures
270275
// Latest endpoint: { status, data: { price, ... } }
271276
// Historical endpoint: { status, data: { prices: [...] } }
272-
if (responseData.status === 'success' && responseData.data) {
277+
if (responseData.status === "success" && responseData.data) {
273278
if (responseData.data.prices) {
274279
// Historical endpoint - return prices array
275280
this.log(`Returning ${responseData.data.prices.length} prices`);
276281
return responseData.data.prices as T;
277282
} else if (responseData.data.price !== undefined) {
278283
// Latest endpoint - wrap single price in array
279-
this.log('Returning single price (wrapped in array)');
284+
this.log("Returning single price (wrapped in array)");
280285
return [responseData.data] as T;
281286
}
282287
}
283288

284289
// Fallback - return data as-is
285-
this.log('Returning data as-is');
290+
this.log("Returning data as-is");
286291
return responseData.data as T;
287-
288292
} catch (error) {
289293
// Handle abort (timeout)
290-
if (error instanceof Error && error.name === 'AbortError') {
291-
throw new TimeoutError('Request timeout', this.timeout);
294+
if (error instanceof Error && error.name === "AbortError") {
295+
throw new TimeoutError("Request timeout", this.timeout);
292296
}
293297
throw error;
294298
}
295-
296299
} catch (error) {
297300
lastError = error as Error;
298301
this.log(`Request failed: ${lastError.message}`, {
299302
attempt,
300-
retryable: this.isRetryable(lastError)
303+
retryable: this.isRetryable(lastError),
301304
});
302305

303306
// Re-throw our custom errors if not retryable
@@ -316,7 +319,7 @@ export class OilPriceAPI {
316319
throw new OilPriceAPIError(
317320
`Request failed after ${this.retries + 1} attempts: ${error.message}`,
318321
undefined,
319-
'NETWORK_ERROR'
322+
"NETWORK_ERROR",
320323
);
321324
}
322325

@@ -331,7 +334,7 @@ export class OilPriceAPI {
331334
}
332335

333336
// This should never be reached, but TypeScript wants it
334-
throw lastError || new OilPriceAPIError('Unknown error occurred');
337+
throw lastError || new OilPriceAPIError("Unknown error occurred");
335338
}
336339

337340
/**
@@ -356,7 +359,7 @@ export class OilPriceAPI {
356359
params.by_code = options.commodity;
357360
}
358361

359-
return this.request<Price[]>('/v1/prices/latest', params);
362+
return this.request<Price[]>("/v1/prices/latest", params);
360363
}
361364

362365
/**
@@ -382,7 +385,7 @@ export class OilPriceAPI {
382385
* ```
383386
*/
384387
async getHistoricalPrices(
385-
options?: HistoricalPricesOptions
388+
options?: HistoricalPricesOptions,
386389
): Promise<Price[]> {
387390
const params: Record<string, string> = {};
388391

@@ -426,7 +429,41 @@ export class OilPriceAPI {
426429
// Issue: SDK was returning wrong dates for historical queries
427430
// Root Cause: Backend has_scope :by_period not working on /v1/prices
428431
// Solution: Use /v1/prices/past_year which uses direct WHERE clauses
429-
return this.request<Price[]>('/v1/prices/past_year', params);
432+
return this.request<Price[]>("/v1/prices/past_year", params);
433+
}
434+
435+
/**
436+
* Get prices from your connected data sources (BYOS)
437+
*
438+
* Requires Data Connector feature enabled on your organization.
439+
*
440+
* @example
441+
* ```typescript
442+
* // Get all connected prices
443+
* const prices = await client.getDataConnectorPrices();
444+
*
445+
* // Filter by fuel type
446+
* const vlsfo = await client.getDataConnectorPrices({ fuelType: 'VLSFO' });
447+
*
448+
* // Filter by port
449+
* const singapore = await client.getDataConnectorPrices({ port: 'SINGAPORE' });
450+
* ```
451+
*/
452+
async getDataConnectorPrices(
453+
options: DataConnectorOptions = {},
454+
): Promise<DataConnectorPrice[]> {
455+
const params: Record<string, string> = {};
456+
457+
if (options.fuelType) params.fuel_type = options.fuelType;
458+
if (options.port) params.port = options.port;
459+
if (options.region) params.region = options.region;
460+
if (options.since) params.since = options.since;
461+
462+
const response = await this.request<{
463+
prices: DataConnectorPrice[];
464+
}>("/v1/prices/data-connector", params);
465+
466+
return response.prices;
430467
}
431468

432469
/**
@@ -441,7 +478,7 @@ export class OilPriceAPI {
441478
* ```
442479
*/
443480
async getCommodities(): Promise<CommoditiesResponse> {
444-
return this.request<CommoditiesResponse>('/v1/commodities', {});
481+
return this.request<CommoditiesResponse>("/v1/commodities", {});
445482
}
446483

447484
/**
@@ -457,7 +494,7 @@ export class OilPriceAPI {
457494
* ```
458495
*/
459496
async getCommodityCategories(): Promise<CategoriesResponse> {
460-
return this.request<CategoriesResponse>('/v1/commodities/categories', {});
497+
return this.request<CategoriesResponse>("/v1/commodities/categories", {});
461498
}
462499

463500
/**

src/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* @packageDocumentation
77
*/
88

9-
export { OilPriceAPI } from './client.js';
10-
export { SDK_VERSION, SDK_NAME } from './version.js';
9+
export { OilPriceAPI } from "./client.js";
10+
export { SDK_VERSION, SDK_NAME } from "./version.js";
1111
export type {
1212
OilPriceAPIConfig,
1313
RetryStrategy,
@@ -20,25 +20,27 @@ export type {
2020
CommoditiesResponse,
2121
CommodityCategory,
2222
CategoriesResponse,
23-
} from './types.js';
23+
DataConnectorPrice,
24+
DataConnectorOptions,
25+
} from "./types.js";
2426
export type {
2527
DieselPrice,
2628
DieselStation,
2729
DieselStationsResponse,
2830
GetDieselStationsOptions,
29-
} from './resources/diesel.js';
31+
} from "./resources/diesel.js";
3032
export type {
3133
PriceAlert,
3234
CreateAlertParams,
3335
UpdateAlertParams,
3436
AlertOperator,
3537
WebhookTestResponse,
36-
} from './resources/alerts.js';
38+
} from "./resources/alerts.js";
3739
export {
3840
OilPriceAPIError,
3941
AuthenticationError,
4042
RateLimitError,
4143
NotFoundError,
4244
ServerError,
4345
TimeoutError,
44-
} from './errors.js';
46+
} from "./errors.js";

0 commit comments

Comments
 (0)