Skip to content

Commit 0b6a69a

Browse files
committed
feat: Add useStacApi hook
Adds a new hook to initialise a StacAPI instance. This was necessary so we can query the stac endpoint and deal with any redirects.
1 parent 900c2f6 commit 0b6a69a

File tree

10 files changed

+125
-58
lines changed

10 files changed

+125
-58
lines changed

example/src/pages/Main/QueryBuilder.jsx

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import T from 'prop-types';
22
import { useCallback, useMemo } from 'react';
33

4-
import { useCollections, StacApi } from "stac-react";
5-
64
import { PrimaryButton } from "../../components/buttons";
75
import { Checkbox, Legend } from '../../components/form';
86
import { H2 } from "../../components/headers";
@@ -11,7 +9,8 @@ import Section from '../../layout/Section';
119

1210
function QueryBuilder ({
1311
setIsBboxDrawEnabled,
14-
collections: selectedCollections,
12+
collections,
13+
selectedCollections,
1514
setCollections,
1615
handleSubmit,
1716
dateRangeFrom,
@@ -24,15 +23,8 @@ function QueryBuilder ({
2423
const handleRangeFromChange = useCallback((e) => setDateRangeFrom(e.target.value), [setDateRangeFrom]);
2524
const handleRangeToChange = useCallback((e) => setDateRangeTo(e.target.value), [setDateRangeTo]);
2625

27-
const headers = useMemo(() => ({
28-
Authorization: "Basic " + btoa(process.env.REACT_APP_STAC_API_TOKEN + ":")
29-
}), []);
30-
31-
const stacApi = useMemo(() => new StacApi(process.env.REACT_APP_STAC_API, { headers }), [headers]);
32-
const { collections } = useCollections(stacApi);
33-
3426
const collectionOptions = useMemo(
35-
() => collections ? collections.collections.map(({ id, title }) => ({ value: id, label: title})) : [],
27+
() => collections.collections ? collections.collections.map(({ id, title }) => ({ value: id, label: title})) : [],
3628
[collections]
3729
);
3830

@@ -45,7 +37,7 @@ function QueryBuilder ({
4537
label="Select Collections"
4638
name="collections"
4739
options={collectionOptions}
48-
values={selectedCollections}
40+
values={selectedCollections || []}
4941
onChange={setCollections}
5042
/>
5143
</Section>
@@ -74,7 +66,8 @@ function QueryBuilder ({
7466
QueryBuilder.propTypes = {
7567
setIsBboxDrawEnabled: T.func.isRequired,
7668
handleSubmit: T.func.isRequired,
77-
collections: T.arrayOf(T.string),
69+
collections: T.object,
70+
selectedCollections: T.arrayOf(T.string),
7871
setCollections: T.func.isRequired,
7972
dateRangeFrom: T.string.isRequired,
8073
setDateRangeFrom: T.func.isRequired,

example/src/pages/Main/index.jsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
import { useCallback, useState, useMemo } from "react";
2-
import { useStacSearch, StacApi } from "stac-react";
1+
import { useCallback, useState } from "react";
2+
import { useStacSearch, useCollections, useStacApi } from "stac-react";
33

44
import ItemList from "./ItemList";
55
import Map from "./Map";
66
import QueryBuilder from "./QueryBuilder";
77

8+
const options = {
9+
headers: {
10+
Authorization: "Basic " + btoa(process.env.REACT_APP_STAC_API_TOKEN + ":")
11+
}
12+
};
13+
814
function Main() {
915
const [isBboxDrawEnabled, setIsBboxDrawEnabled] = useState(false);
10-
const headers = useMemo(() => ({
11-
Authorization: "Basic " + btoa(process.env.REACT_APP_STAC_API_TOKEN + ":")
12-
}), []);
16+
const { stacApi } = useStacApi(process.env.REACT_APP_STAC_API, options);
1317

14-
const stacApi = useMemo(() => new StacApi(process.env.REACT_APP_STAC_API, { headers }), [headers]);
18+
const { collections } = useCollections(stacApi);
1519

1620
const {
1721
setBbox,
18-
collections,
22+
collections: selectedCollections,
1923
setCollections,
2024
dateRangeFrom,
2125
setDateRangeFrom,
@@ -43,6 +47,7 @@ function Main() {
4347
setIsBboxDrawEnabled={setIsBboxDrawEnabled}
4448
handleSubmit={submit}
4549
collections={collections}
50+
selectedCollections={selectedCollections}
4651
setCollections={setCollections}
4752
dateRangeFrom={dateRangeFrom}
4853
setDateRangeFrom={setDateRangeFrom}

src/hooks/useCollections.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import fetch from 'jest-fetch-mock';
22
import { renderHook, act } from '@testing-library/react-hooks';
33
import useCollections from './useCollections';
4-
import StacApi from '../stac-api';
4+
import StacApi, { SearchMode } from '../stac-api';
55

66
describe('useCollections', () => {
77
fetch.mockResponseOnce(JSON.stringify({ links: [] }));
8-
const stacApi = new StacApi('https://fake-stac-api.net');
8+
const stacApi = new StacApi('https://fake-stac-api.net', SearchMode.POST);
99
beforeEach(() => fetch.resetMocks());
1010

1111
it('queries collections', async () => {
1212
fetch.mockResponseOnce(JSON.stringify({ data: '12345' }));
13-
1413
const { result, waitForNextUpdate } = renderHook(
1514
() => useCollections(stacApi)
1615
);
@@ -22,7 +21,6 @@ describe('useCollections', () => {
2221

2322
it('reloads collections', async () => {
2423
fetch.mockResponseOnce(JSON.stringify({ data: 'original' }));
25-
2624
const { result, waitForNextUpdate } = renderHook(
2725
() => useCollections(stacApi)
2826
);

src/hooks/useCollections.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,21 @@ type StacCollectionsHook = {
1010
state: LoadingState
1111
};
1212

13-
function useCollections(stacApi: StacApi): StacCollectionsHook {
13+
function useCollections(stacApi?: StacApi): StacCollectionsHook {
1414
const [ collections, setCollections ] = useState<CollectionsResponse>();
1515
const [ state, setState ] = useState<LoadingState>('IDLE');
1616

1717
const _getCollections = useCallback(
1818
() => {
19-
setState('LOADING');
20-
setCollections(undefined);
19+
if (stacApi) {
20+
setState('LOADING');
21+
setCollections(undefined);
2122

22-
stacApi.getCollections()
23-
.then(response => response.json())
24-
.then(setCollections)
25-
.finally(() => setState('IDLE'));
23+
stacApi.getCollections()
24+
.then(response => response.json())
25+
.then(setCollections)
26+
.finally(() => setState('IDLE'));
27+
}
2628
},
2729
[stacApi]
2830
);

src/hooks/useStacApi.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import fetch from 'jest-fetch-mock';
2+
import { renderHook } from '@testing-library/react-hooks';
3+
import useStacApi from './useStacApi';
4+
import useCollections from './useCollections';
5+
6+
describe('useStacApi', () => {
7+
beforeEach(() => fetch.resetMocks());
8+
9+
it('initilises StacAPI', async () => {
10+
fetch.mockResponseOnce(JSON.stringify({ links: [] }), { url: 'https://fake-stac-api.net' });
11+
const { result: stacApiResult, waitForNextUpdate: waitForApiUpdate } = renderHook(
12+
() => useStacApi('https://fake-stac-api.net')
13+
);
14+
await waitForApiUpdate();
15+
16+
fetch.mockResponseOnce(JSON.stringify({ data: '12345' }));
17+
const { waitForNextUpdate: waitForCollectionsUpdate } = renderHook(
18+
() => useCollections(stacApiResult.current.stacApi)
19+
);
20+
await waitForCollectionsUpdate();
21+
expect(fetch.mock.calls[1][0]).toEqual('https://fake-stac-api.net/collections');
22+
});
23+
24+
it('initilises StacAPI with redirect URL', async () => {
25+
fetch.mockResponseOnce(JSON.stringify({ links: [] }), { url: 'https://fake-stac-api.net/redirect/' });
26+
const { result: stacApiResult, waitForNextUpdate: waitForApiUpdate } = renderHook(
27+
() => useStacApi('https://fake-stac-api.net')
28+
);
29+
await waitForApiUpdate();
30+
31+
fetch.mockResponseOnce(JSON.stringify({ data: '12345' }));
32+
const { waitForNextUpdate: waitForCollectionsUpdate } = renderHook(
33+
() => useCollections(stacApiResult.current.stacApi)
34+
);
35+
await waitForCollectionsUpdate();
36+
expect(fetch.mock.calls[1][0]).toEqual('https://fake-stac-api.net/redirect/collections');
37+
});
38+
});

src/hooks/useStacApi.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useEffect, useState } from 'react';
2+
import StacApi, { SearchMode } from '../stac-api';
3+
import { Link } from '../types/stac';
4+
import { GenericObject } from '../types';
5+
6+
type StacApiHook = {
7+
stacApi?: StacApi
8+
}
9+
10+
function useStacApi(url: string, options?: GenericObject): StacApiHook {
11+
const [ stacApi, setStacApi ] = useState<StacApi>();
12+
13+
useEffect(() => {
14+
let baseUrl: string;
15+
let searchMode = SearchMode.GET;
16+
17+
fetch(url, {
18+
headers: {
19+
'Content-Type': 'application/json',
20+
...options?.headers
21+
}
22+
})
23+
.then(response => {
24+
baseUrl = response.url;
25+
return response;
26+
})
27+
.then(response => response.json())
28+
.then(response => {
29+
const doesPost = response.links.find(({ rel, method }: Link) => rel === 'search' && method === 'POST');
30+
if (doesPost) {
31+
searchMode = SearchMode.POST;
32+
}
33+
})
34+
.then(() => setStacApi(new StacApi(baseUrl, searchMode, options)));
35+
}, [url, options]);
36+
37+
return { stacApi };
38+
}
39+
40+
export default useStacApi;

src/hooks/useStacSearch.test.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fetch from 'jest-fetch-mock';
22
import { renderHook, act } from '@testing-library/react-hooks';
33
import useStacSearch from './useStacSearch';
4-
import StacApi from '../stac-api';
4+
import StacApi, { SearchMode } from '../stac-api';
55
import { Bbox } from '../types/stac';
66

77
function parseRequestPayload(mockApiCall?: RequestInit) {
@@ -12,8 +12,7 @@ function parseRequestPayload(mockApiCall?: RequestInit) {
1212
}
1313

1414
describe('useStacSearch — API supports POST', () => {
15-
fetch.mockResponseOnce(JSON.stringify({ links: [{ rel: 'search', method: 'POST' }] }));
16-
const stacApi = new StacApi('https://fake-stac-api.net');
15+
const stacApi = new StacApi('https://fake-stac-api.net', SearchMode.POST);
1716
beforeEach(() => fetch.resetMocks());
1817

1918
it('includes Bbox in search', async () => {
@@ -411,17 +410,15 @@ describe('useStacSearch — API supports POST', () => {
411410
expect(result.current.results).toEqual({ data: '12345' });
412411
expect(result.current.bbox).toEqual(bbox);
413412

414-
fetch.mockResponseOnce(JSON.stringify({ links: [{ rel: 'search', method: 'POST' }] }));
415-
const newStac = new StacApi('https://otherstack.com');
413+
const newStac = new StacApi('https://otherstack.com', SearchMode.POST);
416414
rerender({ stacApi: newStac });
417415
expect(result.current.results).toBeUndefined();
418416
expect(result.current.bbox).toBeUndefined();
419417
});
420418
});
421419

422420
describe('useStacSearch — API supports GET', () => {
423-
fetch.mockResponseOnce(JSON.stringify({ links: [{ rel: 'search', method: 'GET' }] }));
424-
const stacApi = new StacApi('https://fake-stac-api.net');
421+
const stacApi = new StacApi('https://fake-stac-api.net', SearchMode.GET);
425422
beforeEach(() => fetch.resetMocks());
426423

427424
it('includes Bbox in search', async () => {

src/hooks/useStacSearch.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function useStacSearch(stacApi: StacApi): StacSearchHook {
5151
};
5252

5353
/**
54-
* Reset state when stacApu changes
54+
* Reset state when stacApi changes
5555
*/
5656
useEffect(reset, [stacApi]);
5757

@@ -103,15 +103,15 @@ function useStacSearch(stacApi: StacApi): StacSearchHook {
103103
* Executes a POST request against the `search` endpoint using the provided payload and headers
104104
*/
105105
const executeSearch = useCallback(
106-
(payload: SearchPayload, headers = {}) => processRequest(stacApi.search(payload, headers)),
106+
(payload: SearchPayload, headers = {}) => stacApi && processRequest(stacApi.search(payload, headers)),
107107
[stacApi, processRequest]
108108
);
109109

110110
/**
111111
* Execute a GET request against the provided URL
112112
*/
113113
const getItems = useCallback(
114-
(url: string) => processRequest(stacApi.get(url)),
114+
(url: string) => stacApi && processRequest(stacApi.get(url)),
115115
[stacApi, processRequest]
116116
);
117117

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import useStacSearch from './hooks/useStacSearch';
22
import useCollections from './hooks/useCollections';
3-
import StacApi from './stac-api';
3+
import useStacApi from './hooks/useStacApi';
44

55
export {
66
useCollections,
77
useStacSearch,
8-
StacApi
8+
useStacApi
99
};

src/stac-api/index.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ApiError, GenericObject } from '../types';
2-
import type { Bbox, SearchPayload, DateRange, CollectionIdList, Link } from '../types/stac';
2+
import type { Bbox, SearchPayload, DateRange, CollectionIdList } from '../types/stac';
33

44
type RequestPayload = SearchPayload;
55
type FetchOptions = {
@@ -8,26 +8,20 @@ type FetchOptions = {
88
headers?: GenericObject
99
}
1010

11+
export enum SearchMode {
12+
GET = 'GET',
13+
POST = 'POST'
14+
}
15+
1116
class StacApi {
1217
baseUrl: string;
1318
options?: GenericObject;
14-
searchMode = 'GET';
19+
searchMode = SearchMode.GET;
1520

16-
constructor(baseUrl: string, options?: GenericObject) {
21+
constructor(baseUrl: string, searchMode: SearchMode, options?: GenericObject) {
1722
this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
23+
this.searchMode = searchMode;
1824
this.options = options;
19-
this.fetchApiMeta();
20-
}
21-
22-
fetchApiMeta(): void {
23-
this.fetch(this.baseUrl)
24-
.then(r => r.json())
25-
.then(r => {
26-
const doesPost = r.links.find(({ rel, method }: Link) => rel === 'search' && method === 'POST');
27-
if (doesPost) {
28-
this.searchMode = 'POST';
29-
}
30-
});
3125
}
3226

3327
fixBboxCoordinateOrder(bbox?: Bbox): Bbox | undefined {

0 commit comments

Comments
 (0)