From 88f7462dda88af0d6db16447aab62158673283b0 Mon Sep 17 00:00:00 2001 From: eloramirez1356 Date: Mon, 15 Jun 2026 14:54:31 -0500 Subject: [PATCH 1/2] fix: fix STAC server dataset prefix matching --- README.md | 29 +++++++++ src/stac/stac-server.ts | 35 +++++++++- tests/stac-server.test.ts | 132 +++++++++++++++++++++++++++++++++++++- 3 files changed, 192 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 907b916..a7e58f0 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,35 @@ const [finalized, metadata] = await client.loadDataset({ ``` +### ERA5 land datasets + +ERA5 and ERA5-Land datasets are separate dataset IDs within the ECMWF ERA5 collection. Use `listAvailableDatasets()` to inspect the exact names before loading. + +```typescript +// Non-land ERA5 total precipitation +const [precipitation, precipitationMetadata] = await client.loadDataset({ + request: { + organization: "ecmwf", + collection: "era5", + dataset: "precipitation_total", + variant: "finalized" + } +}); + +// ERA5-Land total precipitation +const [landPrecipitation, landPrecipitationMetadata] = await client.loadDataset({ + request: { + organization: "ecmwf", + collection: "era5", + dataset: "precipitation_total_land", + variant: "finalized" + } +}); + +// ERA5-Land wind datasets follow the same pattern: +// dataset: "wind_u_10m_land" or dataset: "wind_v_10m_land" +``` + ### Selecting while loading ```typescript diff --git a/src/stac/stac-server.ts b/src/stac/stac-server.ts index 4798cb3..de95502 100644 --- a/src/stac/stac-server.ts +++ b/src/stac/stac-server.ts @@ -37,6 +37,33 @@ export interface ResolvedCidFromServer { variant: string; } +function datasetIdFromItemId( + itemId: string, + collection: string +): string | undefined { + const prefix = `${collection}-`; + const remainder = itemId.startsWith(prefix) ? itemId.slice(prefix.length) : itemId; + const [dataset] = remainder.split("-"); + return dataset || undefined; +} + +function featureMatchesDataset( + feature: StacServerItem, + collection: string, + dataset: string +): boolean { + if (feature.collection && feature.collection !== collection) { + return false; + } + + const datasetId = getStringProperty(feature.properties, "dclimate:dataset_id"); + if (datasetId) { + return datasetId === dataset; + } + + return datasetIdFromItemId(feature.id, collection) === dataset; +} + /** * Resolve dataset CID via STAC server /search API. * @@ -75,9 +102,11 @@ export async function resolveCidFromStacServer( const data: StacServerSearchResponse = await response.json(); const features = data.features || []; - // Filter to matching dataset (item ID pattern: {collection}-{dataset}-{variant}) - const prefix = `${collection}-${dataset}`; - const matches = features.filter((f) => f.id.startsWith(prefix)); + // Filter to the exact dataset. A prefix match would conflate datasets such + // as precipitation_total and precipitation_total_land. + const matches = features.filter((f) => + featureMatchesDataset(f, collection, dataset) + ); if (matches.length === 0) { throw new Error(`No items found for ${collection}/${dataset}`); diff --git a/tests/stac-server.test.ts b/tests/stac-server.test.ts index 97fb04e..5e5f7d5 100644 --- a/tests/stac-server.test.ts +++ b/tests/stac-server.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, beforeAll } from "vitest"; +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { resolveCidFromStacServer, resolveDatasetCidFromStacServer, @@ -79,6 +79,136 @@ describe("STAC Server", () => { }); }); + describe("exact dataset matching", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + function mockSearchResponse(features: unknown[]) { + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ + ok: true, + json: async () => ({ + type: "FeatureCollection", + features, + }), + text: async () => "", + })) + ); + } + + it("does not resolve a base ERA5 dataset to a *_land prefix collision", async () => { + mockSearchResponse([ + { + type: "Feature", + id: "ecmwf_era5-precipitation_total_land-finalized", + collection: "ecmwf_era5", + properties: { + "dclimate:dataset_id": "precipitation_total_land", + "dclimate:variant": "finalized", + }, + assets: { + data: { + href: "ipfs://bafy-era5-land-precip-finalized", + }, + }, + }, + { + type: "Feature", + id: "ecmwf_era5-precipitation_total-finalized", + collection: "ecmwf_era5", + properties: { + "dclimate:dataset_id": "precipitation_total", + "dclimate:variant": "finalized", + }, + assets: { + data: { + href: "ipfs://bafy-era5-precip-finalized", + }, + }, + }, + ]); + + const result = await resolveCidFromStacServer( + "ecmwf_era5", + "precipitation_total", + "finalized", + "https://example.test" + ); + + expect(result.cid).toBe("bafy-era5-precip-finalized"); + }); + + it("rejects a request when only a prefix-related land dataset exists", async () => { + mockSearchResponse([ + { + type: "Feature", + id: "ecmwf_era5-wind_u_10m_land-finalized", + collection: "ecmwf_era5", + properties: { + "dclimate:dataset_id": "wind_u_10m_land", + "dclimate:variant": "finalized", + }, + assets: { + data: { + href: "ipfs://bafy-era5-land-wind-u", + }, + }, + }, + ]); + + await expect( + resolveCidFromStacServer( + "ecmwf_era5", + "wind_u_10m", + "finalized", + "https://example.test" + ) + ).rejects.toThrow(/No items found/); + }); + + it("keeps legacy item-id fallback exact", async () => { + mockSearchResponse([ + { + type: "Feature", + id: "ecmwf_era5-temperature_2m_land-finalized", + collection: "ecmwf_era5", + properties: { + "dclimate:variant": "finalized", + }, + assets: { + data: { + href: "ipfs://bafy-era5-land-t2m", + }, + }, + }, + { + type: "Feature", + id: "ecmwf_era5-temperature_2m-finalized", + collection: "ecmwf_era5", + properties: { + "dclimate:variant": "finalized", + }, + assets: { + data: { + href: "ipfs://bafy-era5-t2m", + }, + }, + }, + ]); + + const result = await resolveCidFromStacServer( + "ecmwf_era5", + "temperature_2m", + "finalized", + "https://example.test" + ); + + expect(result.cid).toBe("bafy-era5-t2m"); + }); + }); + describe("resolveCidFromStacServer", () => { it("returns CID as string without ipfs:// prefix", async () => { if (!serverAvailable || !availableDataset) { From a5d6a7ce32ec72b7ae73595e4b809bde28dbab1c Mon Sep 17 00:00:00 2001 From: TheGreatAlgo <37487508+TheGreatAlgo@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:00:13 -0400 Subject: [PATCH 2/2] fix: update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 957afde..278f2fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dclimate/dclimate-client-js", - "version": "0.5.8", + "version": "0.5.9", "description": "JavaScript client for dClimate datasets using jaxray and IPFS stores", "type": "module", "main": "./dist/node/index.js",