demarches-normaliennes/app/javascript/components/shared/queryClient.ts

104 lines
3 KiB
TypeScript
Raw Normal View History

import { QueryClient, QueryFunction } from 'react-query';
import { getJSON, isNumeric } from '@utils';
2021-02-16 13:17:36 +01:00
import { matchSorter } from 'match-sorter';
2020-10-07 17:41:20 +02:00
2022-02-23 13:24:51 +01:00
type Gon = {
gon: {
autocomplete?: {
api_geo_url?: string;
api_adresse_url?: string;
api_education_url?: string;
};
};
};
declare const window: Window & typeof globalThis & Gon;
const API_EDUCATION_QUERY_LIMIT = 5;
const API_GEO_QUERY_LIMIT = 5;
const API_ADRESSE_QUERY_LIMIT = 5;
// When searching for short strings like "mer", le exact match shows up quite far in
// the ordering (~50).
//
// That's why we deliberately fetch a lot of results, and then let the local matching
// (match-sorter) do the work.
//
// NB: 60 is arbitrary, we may add more if needed.
const API_GEO_COMMUNES_QUERY_LIMIT = 60;
2021-01-14 17:30:55 +01:00
const { api_geo_url, api_adresse_url, api_education_url } =
2022-02-23 13:24:51 +01:00
window.gon.autocomplete || {};
2020-10-07 17:41:20 +02:00
type QueryKey = readonly [
scope: string,
term: string,
extra: string | undefined
];
2020-10-07 17:41:20 +02:00
function buildURL(scope: string, term: string, extra?: string) {
term = encodeURIComponent(term.replace(/\(|\)/g, ''));
2020-10-07 17:41:20 +02:00
if (scope === 'adresse') {
return `${api_adresse_url}/search?q=${term}&limit=${API_ADRESSE_QUERY_LIMIT}`;
2021-01-14 17:30:55 +01:00
} else if (scope === 'annuaire-education') {
return `${api_education_url}/search?dataset=fr-en-annuaire-education&q=${term}&rows=${API_EDUCATION_QUERY_LIMIT}`;
2021-04-20 12:55:07 +02:00
} else if (scope === 'communes') {
const limit = `limit=${API_GEO_COMMUNES_QUERY_LIMIT}`;
const url = extra
? `${api_geo_url}/communes?codeDepartement=${extra}&${limit}&`
: `${api_geo_url}/communes?${limit}&`;
2021-04-20 12:55:07 +02:00
if (isNumeric(term)) {
return `${url}codePostal=${term}`;
2021-04-20 12:55:07 +02:00
}
return `${url}nom=${term}&boost=population`;
2020-10-07 17:41:20 +02:00
} else if (isNumeric(term)) {
const code = term.padStart(2, '0');
return `${api_geo_url}/${scope}?code=${code}&limit=${API_GEO_QUERY_LIMIT}`;
2020-10-07 17:41:20 +02:00
}
return `${api_geo_url}/${scope}?nom=${term}&limit=${API_GEO_QUERY_LIMIT}`;
2020-10-07 17:41:20 +02:00
}
function buildOptions(): [RequestInit, AbortController | null] {
2020-10-07 17:41:20 +02:00
if (window.AbortController) {
const controller = new AbortController();
const signal = controller.signal;
return [{ signal }, controller];
}
return [{}, null];
}
const defaultQueryFn: QueryFunction<unknown, QueryKey> = async ({
queryKey: [scope, term, extra]
}) => {
2020-11-25 15:17:59 +01:00
if (scope == 'pays') {
return matchSorter(await getPays(), term, { keys: ['label'] });
2020-11-25 15:17:59 +01:00
}
const url = buildURL(scope, term, extra);
2020-10-07 17:41:20 +02:00
const [options, controller] = buildOptions();
const promise = fetch(url, options).then((response) => {
if (response.ok) {
return response.json();
}
throw new Error(`Error fetching from "${scope}" API`);
});
return Object.assign(promise, {
cancel: () => controller && controller.abort()
});
};
2020-11-30 10:53:55 +01:00
let paysCache: { label: string }[];
async function getPays(): Promise<{ label: string }[]> {
2020-11-30 10:53:55 +01:00
if (!paysCache) {
paysCache = await getJSON('/api/pays', null);
2020-11-30 10:53:55 +01:00
}
return paysCache;
}
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
queryFn: defaultQueryFn as any
}
}
});