Skip to content

Commit

Permalink
feat: add geolocation addressLabel and parent item (#630)
Browse files Browse the repository at this point in the history
  • Loading branch information
pyphilia authored Feb 14, 2024
1 parent c7c8c97 commit d620371
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 44 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"Alexandre Chau"
],
"dependencies": {
"@graasp/sdk": "3.7.0",
"@graasp/sdk": "3.8.0",
"@graasp/translations": "1.23.0",
"axios": "0.27.2",
"crypto-js": "4.2.0",
Expand Down
33 changes: 25 additions & 8 deletions src/api/itemGeolocation.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
import { Item, ItemGeolocation, UUID } from '@graasp/sdk';
import { DiscriminatedItem, Item, ItemGeolocation, UUID } from '@graasp/sdk';

import { PartialQueryConfigForApi } from '../types';
import { verifyAuthentication } from './axios';
import {
buildDeleteItemGeolocationRoute,
buildGetAddressFromCoordinatesRoute,
buildGetItemGeolocationRoute,
buildGetItemsInMapRoute,
buildPutItemGeolocationRoute,
} from './routes';

// eslint-disable-next-line import/prefer-default-export
export const getItemGeolocation = async (
{ API_HOST, axios }: PartialQueryConfigForApi,
id: UUID,
) =>
axios
.get<ItemGeolocation>(`${API_HOST}/${buildGetItemGeolocationRoute(id)}`)
.get<ItemGeolocation | null>(
`${API_HOST}/${buildGetItemGeolocationRoute(id)}`,
)
.then(({ data }) => data);

export const putItemGeolocation = async (
payload: {
itemId: Item['id'];
geolocation: Pick<ItemGeolocation, 'lat' | 'lng'>;
geolocation: Pick<ItemGeolocation, 'lat' | 'lng'> &
Pick<Partial<ItemGeolocation>, 'country' | 'addressLabel'>;
},
{ API_HOST, axios }: PartialQueryConfigForApi,
) =>
Expand All @@ -36,10 +39,11 @@ export const putItemGeolocation = async (

export const getItemsInMap = async (
payload: {
lat1: ItemGeolocation['lat'];
lat2: ItemGeolocation['lat'];
lng1: ItemGeolocation['lng'];
lng2: ItemGeolocation['lng'];
lat1?: ItemGeolocation['lat'];
lat2?: ItemGeolocation['lat'];
lng1?: ItemGeolocation['lng'];
lng2?: ItemGeolocation['lng'];
parentItemId?: DiscriminatedItem['id'];
keywords?: string[];
},
{ API_HOST, axios }: PartialQueryConfigForApi,
Expand All @@ -61,3 +65,16 @@ export const deleteItemGeolocation = async (
)
.then(({ data }) => data),
);

export const getAddressFromCoordinates = async (
{ lat, lng }: Pick<ItemGeolocation, 'lat' | 'lng'>,
{ axios }: PartialQueryConfigForApi,
) =>
axios
.get<{ display_name: string }>(
buildGetAddressFromCoordinatesRoute({ lat, lng }),
{
responseType: 'json',
},
)
.then(({ data }) => data);
36 changes: 28 additions & 8 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AggregateBy,
DiscriminatedItem,
ItemGeolocation,
ItemTag,
ItemTagType,
ItemType,
Expand Down Expand Up @@ -462,24 +463,42 @@ export const buildGetItemsInMapRoute = ({
lng1,
lng2,
keywords,
parentItemId,
}: {
lat1: number;
lat2: number;
lng1: number;
lng2: number;
keywords?: string[];
lat1?: ItemGeolocation['lat'];
lat2?: ItemGeolocation['lat'];
lng1?: ItemGeolocation['lng'];
lng2?: ItemGeolocation['lng'];
parentItemId?: DiscriminatedItem['id'];
}) => {
const params = new URLSearchParams();
params.append('lat1', lat1.toString());
params.append('lat2', lat2.toString());
params.append('lng1', lng1.toString());
params.append('lng2', lng2.toString());
if (lat1 || lat1 === 0) {
params.append('lat1', lat1.toString());
}
if (lat2 || lat2 === 0) {
params.append('lat2', lat2.toString());
}
if (lng1 || lng1 === 0) {
params.append('lng1', lng1.toString());
}
if (lng2 || lng2 === 0) {
params.append('lng2', lng2.toString());
}
if (parentItemId) {
params.append('parentItemId', parentItemId);
}
keywords?.forEach((s) => params.append('keywords', s));

const searchString = params.toString();

return `${ITEMS_ROUTE}/geolocation?${searchString}`;
};
export const buildGetAddressFromCoordinatesRoute = ({
lat,
lng,
}: Pick<ItemGeolocation, 'lat' | 'lng'>) =>
`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`;

export const API_ROUTES = {
APPS_ROUTE,
Expand Down Expand Up @@ -597,4 +616,5 @@ export const API_ROUTES = {
buildGetItemGeolocationRoute,
buildDeleteItemGeolocationRoute,
buildPutItemGeolocationRoute,
buildGetAddressFromCoordinatesRoute,
};
24 changes: 19 additions & 5 deletions src/config/keys.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
AggregateBy,
Category,
DiscriminatedItem,
ItemGeolocation,
ItemType,
UUID,
UnionOfConst,
Expand Down Expand Up @@ -292,16 +294,18 @@ export const itemsWithGeolocationKeys = {
lat2,
lng1,
lng2,
parentItemId,
keywords,
}: {
lat1: number;
lat2: number;
lng1: number;
lng2: number;
keywords?: string[];
lat1?: ItemGeolocation['lat'];
lat2?: ItemGeolocation['lat'];
lng1?: ItemGeolocation['lng'];
lng2?: ItemGeolocation['lng'];
parentItemId?: DiscriminatedItem['id'];
}) => [
...itemsWithGeolocationKeys.allBounds,
{ lat1, lat2, lng1, lng2, keywords },
{ lat1, lat2, lng1, lng2, parentItemId, keywords },
],
};

Expand All @@ -311,6 +315,15 @@ export const buildItemGeolocationKey = (itemId?: UUID) => [
'geolocation',
];

export const buildAddressFromCoordinatesKey = ({
lat,
lng,
}: Pick<ItemGeolocation, 'lat' | 'lng'>) => [
ITEMS_CONTEXT,
{ lat, lng },
'address',
];

export const DATA_KEYS = {
APPS_KEY,
buildItemKey,
Expand Down Expand Up @@ -354,4 +367,5 @@ export const DATA_KEYS = {
buildPublicProfileKey,
itemsWithGeolocationKeys,
buildItemGeolocationKey,
buildAddressFromCoordinatesKey,
};
66 changes: 65 additions & 1 deletion src/hooks/itemGeolocation.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { StatusCodes } from 'http-status-codes';
import nock from 'nock';
import { URL } from 'url';

import { ITEM_GEOLOCATION, UNAUTHORIZED_RESPONSE } from '../../test/constants';
import { mockHook, setUpTest } from '../../test/utils';
import { ITEMS_ROUTE } from '../api/routes';
import {
ITEMS_ROUTE,
buildGetAddressFromCoordinatesRoute,
} from '../api/routes';
import {
buildAddressFromCoordinatesKey,
buildItemGeolocationKey,
itemsWithGeolocationKeys,
} from '../config/keys';
Expand Down Expand Up @@ -63,6 +69,64 @@ describe('useItemGeolocation', () => {
});
});

describe('useAddressFromGeolocation', () => {
const response = { display_name: 'display_name' };
const payload = { lat: 1, lng: 1 };

const url = new URL(buildGetAddressFromCoordinatesRoute(payload));

nock(url.origin)
.get(url.pathname + url.search)
.reply(200, response);

const hook = () => hooks.useAddressFromGeolocation(payload);
const key = buildAddressFromCoordinatesKey(payload);

it(`Retrieve address for coordinates`, async () => {
const { data, isSuccess } = await mockHook({
endpoints: [],
hook,
wrapper,
});

expect(isSuccess).toBeTruthy();
expect(data).toEqual(response);

// verify cache keys
expect(queryClient.getQueryData(key)).toMatchObject(response);
});

it(`Undefined lat does not fetch`, async () => {
const { data, isFetched } = await mockHook({
endpoints: [],
hook,
wrapper,
enabled: false,
});

expect(isFetched).toBeFalsy();
expect(data).toBeFalsy();

// verify cache keys
expect(queryClient.getQueryData(key)).toBeFalsy();
});

it(`Undefined lng does not fetch`, async () => {
const { data, isFetched } = await mockHook({
endpoints: [],
hook,
wrapper,
enabled: false,
});

expect(isFetched).toBeFalsy();
expect(data).toBeFalsy();

// verify cache keys
expect(queryClient.getQueryData(key)).toBeFalsy();
});
});

describe('useItemsInMap', () => {
const response = [ITEM_GEOLOCATION];
const values = { lat1: 1, lat2: 1, lng1: 1, lng2: 1 };
Expand Down
59 changes: 48 additions & 11 deletions src/hooks/itemGeolocation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Item, ItemGeolocation } from '@graasp/sdk';
import { DiscriminatedItem, Item, ItemGeolocation } from '@graasp/sdk';

import { useQuery } from 'react-query';

import * as Api from '../api';
import { UndefinedArgument } from '../config/errors';
import {
buildAddressFromCoordinatesKey,
buildItemGeolocationKey,
itemsWithGeolocationKeys,
} from '../config/keys';
import { getItemGeolocationRoutine } from '../routines/itemGeolocation';
import {
getAddressFromCoordinatesRoutine,
getItemGeolocationRoutine,
} from '../routines/itemGeolocation';
import { QueryClientConfig } from '../types';

export default (queryConfig: QueryClientConfig) => {
Expand Down Expand Up @@ -39,18 +43,21 @@ export default (queryConfig: QueryClientConfig) => {
lng1,
lng2,
keywords,
parentItemId,
}: {
lat1: ItemGeolocation['lat'];
lat2: ItemGeolocation['lat'];
lng1: ItemGeolocation['lng'];
lng2: ItemGeolocation['lng'];
lat1?: ItemGeolocation['lat'];
lat2?: ItemGeolocation['lat'];
lng1?: ItemGeolocation['lng'];
lng2?: ItemGeolocation['lng'];
keywords?: string[];
parentItemId?: DiscriminatedItem['id'];
}) => {
const enabled = Boolean(
(lat1 || lat1 === 0) &&
((lat1 || lat1 === 0) &&
(lat2 || lat2 === 0) &&
(lng1 || lng1 === 0) &&
(lng2 || lng2 === 0),
(lng2 || lng2 === 0)) ||
parentItemId,
);

return useQuery({
Expand All @@ -60,14 +67,22 @@ export default (queryConfig: QueryClientConfig) => {
lng1,
lng2,
keywords,
parentItemId,
}),
queryFn: () => {
if (!enabled) {
throw new UndefinedArgument({ lat1, lat2, lng1, lng2, keywords });
throw new UndefinedArgument({
lat1,
lat2,
lng1,
lng2,
parentItemId,
keywords,
});
}

return Api.getItemsInMap(
{ lat1, lat2, lng1, lng2, keywords },
{ lat1, lat2, lng1, lng2, keywords, parentItemId },
queryConfig,
);
},
Expand All @@ -76,5 +91,27 @@ export default (queryConfig: QueryClientConfig) => {
});
};

return { useItemGeolocation, useItemsInMap };
const useAddressFromGeolocation = ({
lat,
lng,
}: Pick<ItemGeolocation, 'lat' | 'lng'>) =>
useQuery({
queryKey: buildAddressFromCoordinatesKey({ lat, lng }),
queryFn: () => {
if (!(lat || lat === 0) || !(lng || lng === 0)) {
throw new UndefinedArgument();
}
return Api.getAddressFromCoordinates({ lat, lng }, queryConfig);
},
...defaultQueryOptions,
enabled: Boolean((lat || lat === 0) && (lng || lng === 0)),
onError: (error) => {
notifier?.({
type: getAddressFromCoordinatesRoutine.FAILURE,
payload: { error },
});
},
});

return { useItemGeolocation, useItemsInMap, useAddressFromGeolocation };
};
Loading

0 comments on commit d620371

Please sign in to comment.