Skip to content

Commit c785dea

Browse files
committed
refactor: fetch single collection directly from /collections/{id} endpoint
- Add getCollection(collectionId) method to StacApi class - Refactor useCollection to query /collections/{collectionId} directly - Add generateCollectionQueryKey for independent collection caching - Update tests to mock single collection endpoint instead of filtering Previously, useCollection fetched all collections and filtered to find one. Now it uses the STAC spec's dedicated endpoint, improving efficiency for large catalogs and enabling independent collection caching.
1 parent 3236166 commit c785dea

File tree

5 files changed

+80
-61
lines changed

5 files changed

+80
-61
lines changed

src/hooks/useCollection.test.ts

Lines changed: 18 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,30 @@ describe('useCollection', () => {
1111
it('queries collection', async () => {
1212
fetch
1313
.mockResponseOnce(JSON.stringify({ links: [] }), { url: 'https://fake-stac-api.net' })
14-
.mockResponseOnce(
15-
JSON.stringify({
16-
collections: [
17-
{ id: 'abc', title: 'Collection A' },
18-
{ id: 'def', title: 'Collection B' },
19-
],
20-
})
21-
);
14+
.mockResponseOnce(JSON.stringify({ id: 'abc', title: 'Collection A' }));
2215

2316
const { result } = renderHook(() => useCollection('abc'), { wrapper });
2417
await waitFor(() => expect(result.current.isLoading).toEqual(false));
2518
await waitFor(() =>
2619
expect(result.current.collection).toEqual({ id: 'abc', title: 'Collection A' })
2720
);
21+
expect(fetch.mock.calls[1][0]).toEqual('https://fake-stac-api.net/collections/abc');
2822
});
2923

3024
it('returns error if collection does not exist', async () => {
3125
fetch
3226
.mockResponseOnce(JSON.stringify({ links: [] }), { url: 'https://fake-stac-api.net' })
33-
.mockResponseOnce(
34-
JSON.stringify({
35-
collections: [
36-
{ id: 'abc', title: 'Collection A' },
37-
{ id: 'def', title: 'Collection B' },
38-
],
39-
})
40-
);
27+
.mockResponseOnce(JSON.stringify({ error: 'Collection not found' }), {
28+
status: 404,
29+
statusText: 'Not Found',
30+
});
4131

42-
const { result } = renderHook(() => useCollection('ghi'), { wrapper });
32+
const { result } = renderHook(() => useCollection('nonexistent'), { wrapper });
4333
await waitFor(() =>
4434
expect(result.current.error).toEqual({
4535
status: 404,
46-
statusText: 'Not found',
47-
detail: 'Collection does not exist',
36+
statusText: 'Not Found',
37+
detail: { error: 'Collection not found' },
4838
})
4939
);
5040
});
@@ -73,34 +63,20 @@ describe('useCollection', () => {
7363
.mockResponseOnce('Wrong query', { status: 400, statusText: 'Bad Request' });
7464

7565
const { result } = renderHook(() => useCollection('abc'), { wrapper });
76-
await waitFor(() => expect(result.current.error).toBeDefined());
77-
78-
expect(result.current.error).toEqual({
79-
status: 400,
80-
statusText: 'Bad Request',
81-
detail: 'Wrong query',
82-
});
66+
await waitFor(() =>
67+
expect(result.current.error).toEqual({
68+
status: 400,
69+
statusText: 'Bad Request',
70+
detail: 'Wrong query',
71+
})
72+
);
8373
});
8474

8575
it('reloads collection', async () => {
8676
fetch
8777
.mockResponseOnce(JSON.stringify({ links: [] }), { url: 'https://fake-stac-api.net' })
88-
.mockResponseOnce(
89-
JSON.stringify({
90-
collections: [
91-
{ id: 'abc', title: 'Collection A' },
92-
{ id: 'def', title: 'Collection B' },
93-
],
94-
})
95-
)
96-
.mockResponseOnce(
97-
JSON.stringify({
98-
collections: [
99-
{ id: 'abc', title: 'Collection A - Updated' },
100-
{ id: 'def', title: 'Collection B' },
101-
],
102-
})
103-
);
78+
.mockResponseOnce(JSON.stringify({ id: 'abc', title: 'Collection A' }))
79+
.mockResponseOnce(JSON.stringify({ id: 'abc', title: 'Collection A - Updated' }));
10480

10581
const { result } = renderHook(() => useCollection('abc'), { wrapper });
10682
await waitFor(() =>

src/hooks/useCollection.ts

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { useMemo } from 'react';
2-
1+
import { useQuery } from '@tanstack/react-query';
32
import type { ApiErrorType } from '../types';
43
import type { Collection } from '../types/stac';
5-
import useCollections from './useCollections';
4+
import { ApiError } from '../utils/ApiError';
5+
import { generateCollectionQueryKey } from '../utils/queryKeys';
6+
import { useStacApiContext } from '../context/useStacApiContext';
67

78
type StacCollectionHook = {
89
collection?: Collection;
@@ -13,29 +14,43 @@ type StacCollectionHook = {
1314
};
1415

1516
function useCollection(collectionId: string): StacCollectionHook {
16-
const { collections, isLoading, isFetching, error: requestError, reload } = useCollections();
17+
const { stacApi } = useStacApiContext();
1718

18-
const collection = useMemo(() => {
19-
return collections?.collections.find(({ id }) => id === collectionId);
20-
}, [collectionId, collections]);
19+
const fetchCollection = async (): Promise<Collection> => {
20+
if (!stacApi) throw new Error('No STAC API configured');
21+
const response: Response = await stacApi.getCollection(collectionId);
22+
if (!response.ok) {
23+
let detail;
24+
try {
25+
detail = await response.json();
26+
} catch {
27+
detail = await response.text();
28+
}
2129

22-
// Determine error: prefer requestError, else local 404 if collection not found
23-
const error: ApiErrorType | undefined = requestError
24-
? requestError
25-
: !collection && collections
26-
? {
27-
status: 404,
28-
statusText: 'Not found',
29-
detail: 'Collection does not exist',
30-
}
31-
: undefined;
30+
throw new ApiError(response.statusText, response.status, detail);
31+
}
32+
return await response.json();
33+
};
34+
35+
const {
36+
data: collection,
37+
error,
38+
isLoading,
39+
isFetching,
40+
refetch,
41+
} = useQuery<Collection, ApiErrorType>({
42+
queryKey: generateCollectionQueryKey(collectionId),
43+
queryFn: fetchCollection,
44+
enabled: !!stacApi,
45+
retry: false,
46+
});
3247

3348
return {
3449
collection,
3550
isLoading,
3651
isFetching,
37-
error,
38-
reload,
52+
error: error as ApiErrorType,
53+
reload: refetch as () => void,
3954
};
4055
}
4156

src/stac-api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ class StacApi {
152152
return this.fetch(`${this.baseUrl}/collections`);
153153
}
154154

155+
getCollection(collectionId: string): Promise<Response> {
156+
return this.fetch(`${this.baseUrl}/collections/${collectionId}`);
157+
}
158+
155159
get(href: string, headers = {}): Promise<Response> {
156160
return this.fetch(href, { headers });
157161
}

src/utils/queryKeys.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
generateStacApiQueryKey,
55
generateItemQueryKey,
66
generateCollectionsQueryKey,
7+
generateCollectionQueryKey,
78
} from './queryKeys';
89
import type { SearchRequestPayload, Sortby } from '../types/stac';
910

@@ -15,6 +16,21 @@ describe('Query Key Generators', () => {
1516
});
1617
});
1718

19+
describe('generateCollectionQueryKey', () => {
20+
it('should generate key with collection ID', () => {
21+
const key = generateCollectionQueryKey('my-collection');
22+
expect(key).toEqual(['collection', 'my-collection']);
23+
});
24+
25+
it('should handle different collection IDs', () => {
26+
const key1 = generateCollectionQueryKey('collection-a');
27+
const key2 = generateCollectionQueryKey('collection-b');
28+
expect(key1).not.toEqual(key2);
29+
expect(key1).toEqual(['collection', 'collection-a']);
30+
expect(key2).toEqual(['collection', 'collection-b']);
31+
});
32+
});
33+
1834
describe('generateItemQueryKey', () => {
1935
it('should generate key with item URL', () => {
2036
const url = 'https://example.com/collections/test/items/item1';

src/utils/queryKeys.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ export function generateCollectionsQueryKey(): [string] {
4545
return ['collections'];
4646
}
4747

48+
/**
49+
* Generates a query key for a single STAC collection request.
50+
* Collections are fetched by ID from /collections/{collectionId}.
51+
*/
52+
export function generateCollectionQueryKey(collectionId: string): [string, string] {
53+
return ['collection', collectionId];
54+
}
55+
4856
/**
4957
* Generates a query key for STAC item requests.
5058
* Items are fetched by URL.

0 commit comments

Comments
 (0)