diff --git a/src/hooks/api/authenticated-query-v2.ts b/src/hooks/api/authenticated-query-v2.ts index 2c2a136e7..9b1fe644c 100644 --- a/src/hooks/api/authenticated-query-v2.ts +++ b/src/hooks/api/authenticated-query-v2.ts @@ -1,14 +1,15 @@ -import { NextApiRequest } from 'next'; import { useRouter } from 'next/router'; +import { GetServerSidePropsContext } from 'next/types'; import { Cookies } from 'react-cookie'; import { - FetchQueryOptions, QueryClient, QueryFunctionContext, QueryKey, useQuery, UseQueryOptions, + UseQueryResult, } from 'react-query'; +import { QueryState } from 'react-query/types/core/query'; import { FetchError } from '@/utils/fetchFromApi'; import { isTokenValid } from '@/utils/isTokenValid'; @@ -19,67 +20,68 @@ import { createHeaders, useHeaders } from './useHeaders'; type QueryArguments = Record; -type GenerateQueryKeyArguments = { - queryKey: QueryKey; - queryArguments: QueryArguments; -}; - type GeneratedQueryKey = readonly [QueryKey, QueryArguments]; type AuthenticatedQueryFunctionContext = QueryFunctionContext & { headers: HeadersInit; - queryArguments?: TQueryArguments; + queryArguments: TQueryArguments; }; type ServerSideOptions = { - req: NextApiRequest; + req: GetServerSidePropsContext['req']; queryClient: QueryClient; }; -type PrefetchAuthenticatedQueryOptions = { - queryArguments?: QueryArguments; -} & ServerSideOptions & - FetchQueryOptions; - -type UseAuthenticatedQueryOptions< - TQueryFnData, - TQueryArguments = QueryArguments, +export type UseAuthenticatedQueryOptions< + TQueryFnData = unknown, + TQueryArguments extends Object = {}, > = { - queryArguments?: TQueryArguments; -} & Omit< - UseQueryOptions, - 'queryFn' -> & { - queryFn: ( - context: AuthenticatedQueryFunctionContext, + queryArguments: TQueryArguments; +} & Omit, 'queryFn'> & { + queryFn?: ( + context: AuthenticatedQueryFunctionContext, ) => TQueryFnData | Promise; }; +export type UseServerSideAuthenticatedQueryOptions< + TQueryFnData = unknown, + TQueryArguments = unknown, +> = UseAuthenticatedQueryOptions & + ServerSideOptions; + +export type UseAuthenticatedQueryWrapperOptions< + TQueryData = unknown, + TQueryArguments = unknown, +> = Omit, 'queryFn'>; + const isUnAuthorized = (status: number) => [401, 403].includes(status); -const generateQueryKey = ({ +const generateQueryKey = ({ queryKey, queryArguments, -}: GenerateQueryKeyArguments): GeneratedQueryKey => { +}: { + queryKey: K; + queryArguments: A; +}): [K, A] => { if (Object.keys(queryArguments ?? {}).length > 0) { return [queryKey, queryArguments]; } - return [queryKey, {}]; + return [queryKey, {} as A]; }; -type GetPreparedOptionsArguments = { - options: UseAuthenticatedQueryOptions; +type GetPreparedOptionsArguments = { + options: UseAuthenticatedQueryOptions; isTokenPresent: boolean; headers: Headers; }; -const getPreparedOptions = ({ +const getPreparedOptions = ({ options, isTokenPresent, headers, -}: GetPreparedOptionsArguments) => { +}: GetPreparedOptionsArguments) => { const { queryKey, queryArguments, queryFn, ...restOptions } = options; const generatedQueryKey = generateQueryKey({ queryKey, @@ -101,7 +103,7 @@ const prefetchAuthenticatedQuery = async ({ req, queryClient, ...options -}: PrefetchAuthenticatedQueryOptions) => { +}: UseServerSideAuthenticatedQueryOptions) => { if (typeof window !== 'undefined') { throw new Error('Only use prefetchAuthenticatedQuery in server-side code'); } @@ -110,7 +112,6 @@ const prefetchAuthenticatedQuery = async ({ const headers = createHeaders(cookies.get('token')); const { queryKey, queryFn } = getPreparedOptions({ - // @ts-expect-error options, isTokenPresent: isTokenValid(cookies.get('token')), headers, @@ -123,12 +124,38 @@ const prefetchAuthenticatedQuery = async ({ ); } catch {} - return await queryClient.getQueryData(queryKey); + return queryClient.getQueryState(queryKey); }; -const useAuthenticatedQuery = ( - options: UseAuthenticatedQueryOptions, -) => { +function useAuthenticatedQuery< + TQueryFnData = unknown, + TQueryArguments = unknown, +>( + options: UseServerSideAuthenticatedQueryOptions< + TQueryFnData, + TQueryArguments + >, +): Promise>; +function useAuthenticatedQuery< + TQueryFnData = unknown, + TQueryArguments = unknown, +>( + options: UseAuthenticatedQueryOptions, +): UseQueryResult; +function useAuthenticatedQuery< + TQueryFnData = unknown, + TQueryArguments = unknown, +>( + options: + | UseServerSideAuthenticatedQueryOptions + | UseAuthenticatedQueryOptions, +): + | Promise> + | UseQueryResult { + if ('req' in options) { + return prefetchAuthenticatedQuery(options); + } + const headers = useHeaders(); const { cookies, removeAuthenticationCookies } = useCookiesWithOptions([ 'token', @@ -156,7 +183,7 @@ const useAuthenticatedQuery = ( } return result; -}; +} export { prefetchAuthenticatedQuery, useAuthenticatedQuery }; export type { AuthenticatedQueryFunctionContext }; diff --git a/src/hooks/api/authenticated-query.ts b/src/hooks/api/authenticated-query.ts index 66aeb6791..13296edfc 100644 --- a/src/hooks/api/authenticated-query.ts +++ b/src/hooks/api/authenticated-query.ts @@ -2,6 +2,7 @@ import { isEqual } from 'lodash'; import flatten from 'lodash/flatten'; import type { NextApiRequest } from 'next'; import { useRouter } from 'next/router'; +import { GetServerSidePropsContext } from 'next/types'; import { useCallback } from 'react'; import { Cookies } from 'react-cookie'; import { @@ -21,7 +22,7 @@ import { isTokenValid } from '@/utils/isTokenValid'; import { createHeaders, useHeaders } from './useHeaders'; type ServerSideQueryOptions = { - req?: NextApiRequest; + req?: GetServerSidePropsContext['req']; queryClient?: QueryClient; }; diff --git a/src/hooks/api/offers.ts b/src/hooks/api/offers.ts index a45af9ac7..e3738d342 100644 --- a/src/hooks/api/offers.ts +++ b/src/hooks/api/offers.ts @@ -94,8 +94,13 @@ const useGetOffersByCreatorQuery = ( const useGetOfferByIdQuery = ({ scope, id }, configuration = {}) => { const query = scope === OfferTypes.EVENTS ? useGetEventByIdQuery : useGetPlaceByIdQuery; + const args = + scope === OfferTypes.EVENTS + ? [{ id, scope }, configuration] + : [{ queryArguments: { id, scope }, ...configuration }]; - return query({ id, scope }, configuration); + // @ts-expect-error + return query(...args); }; const changeOfferName = async ({ headers, id, lang, name, scope }) => { diff --git a/src/hooks/api/places.ts b/src/hooks/api/places.ts index 81951d4a5..ede805ddf 100644 --- a/src/hooks/api/places.ts +++ b/src/hooks/api/places.ts @@ -26,10 +26,16 @@ import { useAuthenticatedMutation, useAuthenticatedQuery, } from './authenticated-query'; +import { + AuthenticatedQueryFunctionContext, + useAuthenticatedQuery as useAuthenticatedQueryV2, + UseAuthenticatedQueryOptions, + UseAuthenticatedQueryWrapperOptions, +} from './authenticated-query-v2'; import type { Headers } from './types/Headers'; import type { User } from './user'; -const getPlaceById = async ({ headers, id }) => { +const getPlaceById = async ({ headers, queryArguments: { id } }) => { const res = await fetchFromApi({ path: `/places/${id.toString()}`, options: { @@ -38,9 +44,10 @@ const getPlaceById = async ({ headers, id }) => { }); if (isErrorObject(res)) { // eslint-disable-next-line no-console - return console.error(res); + console.error(res); + return; } - return await res.json(); + return (await res.json()) as Place; }; type UseGetPlaceByIdArguments = ServerSideQueryOptions & { @@ -48,19 +55,14 @@ type UseGetPlaceByIdArguments = ServerSideQueryOptions & { scope?: Values; }; -const useGetPlaceByIdQuery = ( - { req, queryClient, id, scope }: UseGetPlaceByIdArguments, - configuration: UseQueryOptions = {}, -) => - useAuthenticatedQuery({ - req, - queryClient, +const useGetPlaceByIdQuery = wrap( + useAuthenticatedQueryV2, + ({ queryArguments: { id, scope } }) => ({ queryKey: ['places'], queryFn: getPlaceById, - queryArguments: { id }, enabled: !!id && scope === OfferTypes.PLACES, - ...configuration, - }); + }), +); const getPlacesByCreator = async ({ headers, ...queryData }) => { delete headers['Authorization']; @@ -123,7 +125,7 @@ const useGetPlacesByCreatorQuery = ( type GetPlacesByQueryArguments = { name: string; - terms: Array>; + terms: Values[]; zip?: string; addressLocality?: string; addressCountry?: Country; @@ -131,12 +133,14 @@ type GetPlacesByQueryArguments = { const getPlacesByQuery = async ({ headers, - name, - terms, - zip, - addressLocality, - addressCountry, -}: Headers & GetPlacesByQueryArguments) => { + queryArguments: { + name, + terms, + zip = null, + addressLocality = null, + addressCountry = null, + }, +}) => { const termsString = terms.reduce( (acc, currentTerm) => `${acc}terms.id:${currentTerm}`, '', @@ -169,34 +173,30 @@ const getPlacesByQuery = async ({ if (isErrorObject(res)) { // eslint-disable-next-line no-console - return console.error(res); + console.error(res); + return; } + return await res.json(); }; -const useGetPlacesByQuery = ( - { - name, - terms, - zip, - addressLocality, - addressCountry, - }: GetPlacesByQueryArguments, - configuration = {}, -) => - useAuthenticatedQuery({ +function wrap( + fn: T, + outerArgs: any = {}, +): T { + return function (args) { + return fn({ ...args, ...outerArgs(args) }); + }; +} + +const useGetPlacesByQuery = wrap( + useAuthenticatedQueryV2<{ member: Place[] }, GetPlacesByQueryArguments>, + ({ enabled, queryArguments: { name, terms } }) => ({ queryKey: ['places'], queryFn: getPlacesByQuery, - queryArguments: { - name, - terms, - zip, - addressCountry, - addressLocality, - }, - enabled: !!name || terms.length, - ...configuration, - }); + enabled: enabled && (!!name || terms.length > 0), + }), +); const changeAddress = async ({ headers, id, address, language }) => fetchFromApi({ diff --git a/src/pages/PlaceAddModal.tsx b/src/pages/PlaceAddModal.tsx index 07bab2fc5..e03fb480b 100644 --- a/src/pages/PlaceAddModal.tsx +++ b/src/pages/PlaceAddModal.tsx @@ -86,7 +86,10 @@ const PlaceAddModal = ({ if (!resp?.placeId) return; - const newPlace = await getPlaceById({ headers, id: resp.placeId }); + const newPlace = await getPlaceById({ + headers, + queryArguments: { id: resp.placeId }, + }); onConfirmSuccess(newPlace); })(); }; diff --git a/src/pages/dashboard/index.page.tsx b/src/pages/dashboard/index.page.tsx index bd20003df..7bbf0d93b 100644 --- a/src/pages/dashboard/index.page.tsx +++ b/src/pages/dashboard/index.page.tsx @@ -730,23 +730,24 @@ const Dashboard = (): any => { const getServerSideProps = getApplicationServerSideProps( async ({ req, query, cookies: rawCookies, queryClient }) => { - const user = (await useGetUserQueryServerSide({ + const user = await useGetUserQueryServerSide({ req, queryClient, - })) as User; + }); await Promise.all( Object.entries(UseGetItemsByCreatorMap).map(([key, hook]) => { const page = - query.tab === key ? (query.page ? parseInt(query.page) : 1) : 1; + query.tab === key ? parseInt((query.page as string) ?? '1') : 1; - const sortingField = query?.sort?.split('_')[0] ?? SortingField.CREATED; - const sortingOrder = query?.sort?.split('_')[1] ?? SortingOrder.DESC; + const sort = (query?.sort ?? '') as string; + const sortingField = sort.split('_')[0] ?? SortingField.CREATED; + const sortingOrder = sort.split('_')[1] ?? SortingOrder.DESC; return hook({ req, queryClient, - creator: user, + creator: user as User, ...(key === 'events' && { sortOptions: { field: sortingField, diff --git a/src/pages/events/[eventId]/duplicate/index.page.tsx b/src/pages/events/[eventId]/duplicate/index.page.tsx index 4b57e5d2e..27924e3e1 100644 --- a/src/pages/events/[eventId]/duplicate/index.page.tsx +++ b/src/pages/events/[eventId]/duplicate/index.page.tsx @@ -10,7 +10,7 @@ export const getServerSideProps = getApplicationServerSideProps( const { eventId } = query; await useGetEventByIdQuery({ - id: eventId, + id: eventId as string, req, queryClient, }); diff --git a/src/pages/events/[eventId]/edit/index.page.tsx b/src/pages/events/[eventId]/edit/index.page.tsx index fbd82928f..27924e3e1 100644 --- a/src/pages/events/[eventId]/edit/index.page.tsx +++ b/src/pages/events/[eventId]/edit/index.page.tsx @@ -1,7 +1,6 @@ import { dehydrate } from 'react-query/hydration'; import { useGetEventByIdQuery } from '@/hooks/api/events'; -import { Event } from '@/types/Event'; import { getApplicationServerSideProps } from '@/utils/getApplicationServerSideProps'; import { OfferForm } from '../../../create/OfferForm'; @@ -10,11 +9,11 @@ export const getServerSideProps = getApplicationServerSideProps( async ({ req, query, queryClient, cookies }) => { const { eventId } = query; - const event = (await useGetEventByIdQuery({ - id: eventId, + await useGetEventByIdQuery({ + id: eventId as string, req, queryClient, - })) as Event; + }); return { props: { diff --git a/src/pages/manage/movies/[eventId]/edit.page.tsx b/src/pages/manage/movies/[eventId]/edit.page.tsx index fe1276cba..e6a8054d7 100644 --- a/src/pages/manage/movies/[eventId]/edit.page.tsx +++ b/src/pages/manage/movies/[eventId]/edit.page.tsx @@ -12,7 +12,7 @@ export const getServerSideProps = getApplicationServerSideProps( await useGetEventByIdQuery({ req, queryClient, - id: eventId, + id: eventId as string, }); return { diff --git a/src/pages/places/[placeId]/availability.page.js b/src/pages/places/[placeId]/availability.page.tsx similarity index 80% rename from src/pages/places/[placeId]/availability.page.js rename to src/pages/places/[placeId]/availability.page.tsx index c12b6dbfa..f49eb86e8 100644 --- a/src/pages/places/[placeId]/availability.page.js +++ b/src/pages/places/[placeId]/availability.page.tsx @@ -16,13 +16,16 @@ const Availability = () => { const { placeId } = router.query; const getPlaceByIdQuery = useGetPlaceByIdQuery({ - id: placeId, - scope: OfferTypes.PLACES, + queryArguments: { + id: placeId as string, + scope: OfferTypes.PLACES, + }, }); - if (getPlaceByIdQuery.status === QueryStatus.LOADING) { + if (getPlaceByIdQuery.status !== QueryStatus.SUCCESS) { return ; } + return ( { export const getServerSideProps = getApplicationServerSideProps( async ({ req, query, cookies, queryClient }) => { - const { placeId } = query; - await useGetPlaceByIdQuery({ req, queryClient, id: placeId }); + await useGetPlaceByIdQuery({ + req, + queryClient, + queryArguments: { id: query.placeId as string }, + }); return { props: { diff --git a/src/pages/steps/PlaceStep.tsx b/src/pages/steps/PlaceStep.tsx index 90abda507..d0eff85e6 100644 --- a/src/pages/steps/PlaceStep.tsx +++ b/src/pages/steps/PlaceStep.tsx @@ -63,24 +63,20 @@ const PlaceStep = ({ const isMovie = terms.includes(EventTypes.Bioscoop); - const useGetPlacesQuery = useGetPlacesByQuery( - { + const useGetPlacesQuery = useGetPlacesByQuery({ + enabled: !!searchInput, + queryArguments: { name: searchInput, terms, zip: municipality?.zip, addressLocality: municipality?.name, addressCountry: country, }, - { enabled: !!searchInput }, - ); + }); - const places = useMemo( - // @ts-expect-error + const places = useMemo( () => useGetPlacesQuery.data?.member ?? [], - [ - // @ts-expect-error - useGetPlacesQuery.data?.member, - ], + [useGetPlacesQuery.data?.member], ); const place = useWatch({ control, name: 'location.place' }); @@ -156,7 +152,6 @@ const PlaceStep = ({ } Component={ { - const getPlaceByIdQuery = useGetPlaceByIdQuery( - { id, scope: OfferTypes.PLACES }, - { onSuccess, enabled: !!id && !!enabled }, - ); - - // @ts-expect-error - return getPlaceByIdQuery?.data; -}; +const useGetPlace = ({ id, onSuccess, enabled }) => + useGetPlaceByIdQuery({ + onSuccess, + enabled: !!id && !!enabled, + queryArguments: { id, scope: OfferTypes.PLACES }, + })?.data; export { useGetPlace }; diff --git a/src/utils/getApplicationServerSideProps.js b/src/utils/getApplicationServerSideProps.ts similarity index 74% rename from src/utils/getApplicationServerSideProps.js rename to src/utils/getApplicationServerSideProps.ts index 26e1be804..413e7b722 100644 --- a/src/utils/getApplicationServerSideProps.js +++ b/src/utils/getApplicationServerSideProps.ts @@ -1,17 +1,18 @@ import * as Sentry from '@sentry/nextjs'; import getConfig from 'next/config'; +import { GetServerSidePropsContext } from 'next/types'; import absoluteUrl from 'next-absolute-url'; +import { ParsedUrlQuery } from 'querystring'; import { QueryClient } from 'react-query'; import { generatePath, matchPath } from 'react-router'; import UniversalCookies from 'universal-cookie'; -import { useGetUserQuery, useGetUserQueryServerSide } from '@/hooks/api/user'; +import { useGetUserQueryServerSide } from '@/hooks/api/user'; import { defaultCookieOptions } from '@/hooks/useCookiesWithOptions'; import { isFeatureFlagEnabledInCookies } from '@/hooks/useFeatureFlag'; import { getRedirects } from '../redirects'; import { FetchError } from './fetchFromApi'; -import { isTokenValid } from './isTokenValid'; class Cookies extends UniversalCookies { toString() { @@ -19,7 +20,7 @@ class Cookies extends UniversalCookies { return cookieEntries.reduce((previous, [key, value], currentIndex) => { const end = currentIndex !== cookieEntries.length - 1 ? '; ' : ''; - const pair = `${key}=${encodeURIComponent(value)}${end}`; + const pair = `${key}=${encodeURIComponent(value as string)}${end}`; return `${previous}${pair}`; }, ''); } @@ -61,7 +62,13 @@ const getRedirect = (originalPath, environment, cookies) => { .find((match) => !!match); }; -const redirectToLogin = (cookies, req, resolvedUrl) => { +const redirectToLogin = ({ + cookies, + req, + resolvedUrl, +}: Pick & { + cookies: Cookies; +}) => { Sentry.setUser(null); cookies.remove('token'); @@ -76,22 +83,31 @@ const redirectToLogin = (cookies, req, resolvedUrl) => { }; }; +type ExtendedGetServerSidePropsContext = GetServerSidePropsContext & { + cookies: string; + queryClient: QueryClient; +}; + const getApplicationServerSideProps = - (callbackFn) => - async ({ req, query, resolvedUrl }) => { + (callbackFn?: (context: ExtendedGetServerSidePropsContext) => any) => + async ({ + req, + query, + resolvedUrl, + ...context + }: GetServerSidePropsContext) => { const { publicRuntimeConfig } = getConfig(); if (publicRuntimeConfig.environment === 'development') { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0; + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; } - const rawCookies = req?.headers?.cookie ?? ''; - - const cookies = new Cookies(rawCookies, defaultCookieOptions); + const cookies = new Cookies(req.cookies); req.headers.cookie = cookies.toString(); - const isDynamicUrl = !!query.params; - const path = isDynamicUrl ? `/${query.params.join('/')}` : resolvedUrl; + const params = (query.params as string[]) ?? []; + const isDynamicUrl = Object.keys(params).length > 0; + const path = isDynamicUrl ? `/${params.join('/')}` : resolvedUrl; const redirect = getRedirect( path, @@ -102,7 +118,9 @@ const getApplicationServerSideProps = if (redirect) { // Don't include the `params` in the redirect URL's query. delete query.params; - const queryParameters = new URLSearchParams(query); + const queryParameters = new URLSearchParams( + query as Record, + ); // Return the redirect as-is if there are no additional query parameters // to append. @@ -124,7 +142,11 @@ const getApplicationServerSideProps = await useGetUserQueryServerSide({ req, queryClient }); } catch (error) { if (error instanceof FetchError) { - return redirectToLogin(cookies, req, resolvedUrl); + return redirectToLogin({ + cookies, + req, + resolvedUrl, + }); } } @@ -133,6 +155,8 @@ const getApplicationServerSideProps = if (!callbackFn) return { props: { cookies: cookies.toString() } }; return await callbackFn({ + ...context, + resolvedUrl, req, query, queryClient,