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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.1.0] - 2026-10-03

Four methods called routes the API has never served, so every call returned HTTP 404 (#125). They now reject immediately with `OilPriceAPIError` code `ENDPOINT_NOT_AVAILABLE`, name the supported alternative, and make no request. They are deprecated and will be removed in the next major.

### Deprecated

- `client.drilling.trends()`: no trends endpoint exists. Use `summary()`, or `completions()` / `wellsDrilled()` / `ducWells()` for basin-level series.
- `client.drilling.basin(name)`: no per-basin endpoint exists. Read the `by_basin` maps from `completions()`, `wellsDrilled()`, `ducWells()` or `fracSpreads()`.
- `client.futures.spreads(contract1, contract2)`: no arbitrary two-contract spread endpoint exists. Use `client.futures.family(slug).spreads()`, e.g. `client.futures.brent().spreads()`.
- `client.futures.continuous(contract, months)`: no per-contract continuous endpoint exists. Use `continuousFrontMonth()`.

### Added

- `client.futures.continuousFrontMonth("brent" | "wti")` for `GET /v1/futures/continuous/{family}`, returning `ContinuousFrontMonth`.
- `tests/api-path-contract.test.ts`, which fails CI when any SDK path is missing from a snapshot of the API's routes. Refresh the snapshot with `scripts/refresh-api-paths.mjs`.

### Security

- Dev-only lockfile updates: fast-uri 3.1.8 (GHSA-hrr3-gc8f-f4qj) and markdown-it 14.3.2 (GHSA-253c-mchw-3w2r).

## [2.0.0] - 2026-09-14

A major release: several resources were typed and called in ways production never supported. The changes below are breaking at compile time or runtime, and in nearly every case the old call could not work correctly against the live API.
Expand Down
22 changes: 11 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "oilpriceapi",
"version": "2.0.0",
"version": "2.1.0",
"description": "Official Node.js SDK for source-timestamped OilPriceAPI energy data",
"type": "module",
"main": "./dist/cjs/index.js",
Expand Down
79 changes: 79 additions & 0 deletions scripts/refresh-api-paths.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env node
// Regenerate tests/fixtures/api-paths.json, the route snapshot that
// tests/api-path-contract.test.ts checks every SDK path against (#125).
//
// Run from an oilpriceapi-api checkout of origin/main:
// bundle exec rails routes > /tmp/routes.txt
// node scripts/refresh-api-paths.mjs \
// --routes /tmp/routes.txt \
// --swagger <api>/swagger/v1/swagger.json \
// --policy <api>/config/openapi_route_policy.yml \
// --commit "$(git -C <api> rev-parse --short HEAD)"

import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

const args = Object.fromEntries(
process.argv.slice(2).reduce((pairs, arg, i, all) => {
if (arg.startsWith("--")) pairs.push([arg.slice(2), all[i + 1]]);
return pairs;
}, []),
);
for (const required of ["routes", "swagger", "policy"]) {
if (!args[required]) {
console.error(`Missing --${required}. See the header of this script.`);
process.exit(2);
}
}

// Every path parameter, however spelled (:code, {code}), becomes {param}.
const normalize = (path) =>
path
.replace(/\(\.:format\)$/, "")
.replace(/:[A-Za-z_]+/g, "{param}")
.replace(/\{[^}]+\}/g, "{param}")
.replace(/\/$/, "");

const swaggerPaths = new Set(
Object.keys(JSON.parse(readFileSync(args.swagger, "utf8")).paths).map(normalize),
);

// The policy file is a flat list of `- prefix:` / `audience:` pairs.
const exclusions = [];
let current;
for (const line of readFileSync(args.policy, "utf8").split("\n")) {
const prefix = line.match(/^\s*-\s*prefix:\s*(\S+)/);
if (prefix) exclusions.push((current = { prefix: prefix[1] }));
const audience = line.match(/^\s*audience:\s*(\S+)/);
if (audience && current) current.audience = audience[1];
}
exclusions.sort((a, b) => b.prefix.length - a.prefix.length);

const routes = new Map();
for (const line of readFileSync(args.routes, "utf8").split("\n")) {
const match = line.match(/\b(GET|POST|PUT|PATCH|DELETE)\s+(\/v1\/\S+)/);
if (!match) continue;
const path = normalize(match[2]);
const key = `${match[1]} ${path}`;
if (routes.has(key)) continue;
const audience = swaggerPaths.has(path)
? "openapi"
: (exclusions.find((e) => path === e.prefix || path.startsWith(`${e.prefix}/`) || path.startsWith(e.prefix))
?.audience ?? "unlisted");
routes.set(key, { method: match[1], path, audience });
}

const snapshot = {
generatedFrom: {
api: "OilpriceAPI/oilpriceapi-api",
commit: args.commit ?? "unknown",
generatedAt: new Date().toISOString().slice(0, 10),
},
routes: [...routes.values()].sort((a, b) =>
`${a.path} ${a.method}`.localeCompare(`${b.path} ${b.method}`),
),
};

const out = fileURLToPath(new URL("../tests/fixtures/api-paths.json", import.meta.url));
writeFileSync(out, `${JSON.stringify(snapshot, null, 2)}\n`);
console.log(`Wrote ${snapshot.routes.length} routes to ${out}`);
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export type {
FuturesCurveData,
ContinuousContractPrice,
ContinuousFuturesData,
ContinuousFrontMonth,
ContinuousFuturesFamily,
FuturesSpreadHistoryPoint,
FuturesSpreadHistory,
FuturesContractFamilySlug,
Expand Down
82 changes: 28 additions & 54 deletions src/resources/drilling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import type { OilPriceAPI } from "../client.js";
import { ValidationError } from "../errors.js";
import { OilPriceAPIError } from "../errors.js";

/**
* Drilling intelligence data point
Expand Down Expand Up @@ -205,15 +205,8 @@ export interface BasinDrillingData {
* const latest = await client.drilling.latest();
* console.log(`Active rigs: ${latest.total_rigs}`);
*
* // Get drilling trends
* const trends = await client.drilling.trends();
* trends.forEach(trend => {
* console.log(`${trend.date}: ${trend.metric} = ${trend.value}`);
* });
*
* // Get basin-specific data
* const permian = await client.drilling.basin('Permian');
* console.log(`Permian rigs: ${permian.active_rigs}`);
* // Basin-level completions (by_basin keyed by basin code)
* const completions = await client.drilling.completions();
* ```
*/
export class DrillingIntelligenceResource {
Expand Down Expand Up @@ -298,29 +291,22 @@ export class DrillingIntelligenceResource {
}

/**
* Get drilling activity trends
*
* Returns time series data showing trends in drilling metrics.
*
* @returns Array of drilling trend data points
* Drilling activity trends.
*
* @throws {OilPriceAPIError} If API request fails
* @throws {AuthenticationError} If API key is invalid
* @deprecated The API has no `/v1/drilling-intelligence/trends` route; every
* call returned HTTP 404 (#125). Use {@link summary} for the current
* aggregate, or {@link completions} / {@link wellsDrilled} /
* {@link ducWells} for basin-level series. Removed in the next major.
*
* @example
* ```typescript
* const trends = await client.drilling.trends();
* trends.forEach(point => {
* console.log(`${point.date}: ${point.metric} = ${point.value} (${point.trend})`);
* });
* ```
* @throws {OilPriceAPIError} Always, with code `ENDPOINT_NOT_AVAILABLE`.
*/
async trends(): Promise<DrillingTrend[]> {
const response = await this.client["request"]<
DrillingTrend[] | { trends: DrillingTrend[] }
>("/v1/drilling-intelligence/trends", {});

return Array.isArray(response) ? response : response.trends;
throw new OilPriceAPIError(
"client.drilling.trends() is not supported: the API has no drilling trends endpoint. " +
"Use client.drilling.summary(), or completions() / wellsDrilled() / ducWells() for basin-level data.",
undefined,
"ENDPOINT_NOT_AVAILABLE",
);
}

/**
Expand Down Expand Up @@ -454,34 +440,22 @@ export class DrillingIntelligenceResource {
}

/**
* Get basin-specific drilling intelligence
*
* Returns comprehensive drilling data for a specific basin.
*
* @param name - Basin name (e.g., "Permian", "Eagle Ford", "Bakken")
* @returns Basin drilling intelligence data
* Basin-specific drilling intelligence.
*
* @throws {NotFoundError} If basin not found
* @throws {OilPriceAPIError} If API request fails
* @throws {AuthenticationError} If API key is invalid
* @deprecated The API has no `/v1/drilling-intelligence/basin/{name}` route;
* every call returned HTTP 404 (#125). Basin-level values are returned in the
* `by_basin` maps of {@link completions}, {@link wellsDrilled},
* {@link ducWells} and {@link fracSpreads}. Removed in the next major.
*
* @example
* ```typescript
* const permian = await client.drilling.basin('Permian');
* console.log(`Permian Basin as of ${permian.as_of_date}:`);
* console.log(` Active rigs: ${permian.active_rigs}`);
* console.log(` Frac spreads: ${permian.frac_spreads}`);
* console.log(` DUC wells: ${permian.duc_wells}`);
* ```
* @throws {OilPriceAPIError} Always, with code `ENDPOINT_NOT_AVAILABLE`.
*/
async basin(name: string): Promise<BasinDrillingData> {
if (!name || typeof name !== "string") {
throw new ValidationError("Basin name must be a non-empty string");
}

return this.client["request"]<BasinDrillingData>(
`/v1/drilling-intelligence/basin/${encodeURIComponent(name)}`,
{},
async basin(_name: string): Promise<BasinDrillingData> {
throw new OilPriceAPIError(
"client.drilling.basin() is not supported: the API has no per-basin endpoint. " +
"Read the by_basin maps from completions(), wellsDrilled(), ducWells() or fracSpreads().",
undefined,
"ENDPOINT_NOT_AVAILABLE",
);
}

}
Loading
Loading