Skip to content

Commit

Permalink
Introduce query helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
svemat01 committed Dec 8, 2024
1 parent cd60632 commit c096b75
Show file tree
Hide file tree
Showing 34 changed files with 530 additions and 414 deletions.
183 changes: 177 additions & 6 deletions web/src/api/core.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
/* eslint-disable sonarjs/no-duplicate-string */
import {
DefaultError,
DefinedUseQueryResult,
QueryClient,
QueryFunction,
QueryFunctionContext,
QueryKey,
QueryObserverOptions,
skipToken,
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions,
} from '@tanstack/react-query';

import { useAuth } from './auth';
import { paths } from './schema.gen';

export const BASE_URL = 'http://localhost:3000';
export const BASE_URL =
import.meta.env.VITE_API_URL || `${window.location.origin}/api/`;

type HTTPMethod =
| 'get'
Expand Down Expand Up @@ -128,7 +138,7 @@ export class ApiError extends Error {
}
}

export type ApiRequest<
export type ApiRoute<
TPath extends keyof paths,
TMethod extends PathMethods<TPath>,
TRoute extends AnyRoute = paths[TPath][TMethod] extends AnyRoute
Expand Down Expand Up @@ -256,7 +266,7 @@ export const apiRequest = async <
}
}

const url = new URL('/api' + path, BASE_URL);
const url = new URL(`.${path}`, BASE_URL);

if (query) {
for (const [key, value] of Object.entries(query)) {
Expand All @@ -272,19 +282,30 @@ export const apiRequest = async <
}
}

const { token } = useAuth.getState();
const { token, clearAuthToken } = useAuth.getState();

if (token) {
headers.set('Authorization', 'Bearer ' + token);
}

if (contentType) {
headers.set('Content-Type', contentType);
}

const response = await fetch(url, {
method,
method: method.toUpperCase(),
headers,
body: convertBody(data, contentType),
...fetchOptions,
});

if (response.status === 401) {
console.log('Token expired, clearing token');
clearAuthToken();

throw new ApiError('Token expired', 401);
}

if (!response.ok) {
throw ApiError.fromResponse(response);
}
Expand All @@ -311,6 +332,156 @@ export const apiRequest = async <
}
};

/// Query Creators

const getFullKey = (queryKey: QueryKey, variables?: any): QueryKey => {
return variables === undefined ? queryKey : [...queryKey, variables];
};

export type CreateQueryOptions<
TFunctionData,
TVariables = void,
TError = DefaultError
> = Omit<
UseQueryOptions<TFunctionData, TError>,
'queryKey' | 'queryFn' | 'select'
> & {
fetcher: (
variables: TVariables,
context: QueryFunctionContext
) => TFunctionData | Promise<TFunctionData>;
queryKey: QueryKey;
variables?: TVariables;
};

export type QueryHookOptions<
TFunctionData,
TVariables = void,
TError = DefaultError,
TData = TFunctionData
> = Omit<
UseQueryOptions<TFunctionData, TError, TData>,
'queryKey' | 'queryFn' | 'queryKeyHashFn'
> &
(TVariables extends void
? {
variables?: undefined;
}
: {
variables: TVariables;
});

export const createQuery = <
TFunctionData,
TVariables = void,
TError = DefaultError
>(
defaultOptions: CreateQueryOptions<TFunctionData, TVariables, TError>
) => {
const getQueryOptions = (
fetcherFunction: (
variables: TVariables,
context: QueryFunctionContext
) => TFunctionData | Promise<TFunctionData>,
variables: TVariables
) => {
return {
queryFn:
variables && variables === skipToken
? (skipToken as any)
: (context: QueryFunctionContext) =>
fetcherFunction(variables, context),
queryKey: getFullKey(defaultOptions.queryKey, variables),
} as {
queryFn: QueryFunction<TFunctionData, QueryKey>;
queryKey: QueryKey;
};
};

const getKey = (variables?: TVariables) =>
getFullKey(defaultOptions.queryKey, variables);

const getOptions = (variables: TVariables) => {
return {
...defaultOptions,
...getQueryOptions(defaultOptions.fetcher, variables),
};
};

const getFetchOptions = (variables: TVariables) => {
return {
...getQueryOptions(defaultOptions.fetcher, variables),
queryKeyHashFn: defaultOptions.queryKeyHashFn,
};
};

const useTQ = <TData = TFunctionData>(
options: QueryHookOptions<TFunctionData, TVariables, TError, TData>,
queryClient?: QueryClient
) => {
return useQuery(
{
...defaultOptions,
...options,
...getQueryOptions(
defaultOptions.fetcher,
// @ts-ignore
options.variables ?? defaultOptions.variables
),
},
queryClient
);
};

useTQ.getOptions = getOptions;
useTQ.getKey = getKey;
useTQ.getFetchOptions = getFetchOptions;
useTQ.fetcher = defaultOptions.fetcher;

return useTQ as typeof useTQ & {
/** Type helper to infer the data type returned by this query. **Does not exist at runtime.** */
$inferData: TFunctionData;
/** Type helper to infer the variables type accepted by this query. **Does not exist at runtime.** */
$inferVariables: TVariables;
};
};

export const createMutation = <
TData = unknown,
TVariables = void,
TError = DefaultError,
TContext = unknown
>(
defaultOptions: UseMutationOptions<TData, TError, TVariables, TContext>
) => {
const useTM = (
options: Omit<
UseMutationOptions<TData, TError, TVariables, TContext>,
'mutationFn'
>,
queryClient?: QueryClient
) => {
return useMutation(
{
...defaultOptions,
...options,
},
queryClient
);
};

useTM.getKey = () => defaultOptions.mutationKey;
useTM.getOptions = () => defaultOptions;
useTM.mutationFn = defaultOptions.mutationFn;

return useTM as typeof useTM & {
/** Type helper to infer the data type returned by this mutation. **Does not exist at runtime.** */
$inferData: TData;
/** Type helper to infer the variables type accepted by this mutation. **Does not exist at runtime.** */
$inferVariables: TVariables;
};
};

/**
* @deprecated Use `ApiRequest` instead
*/
Expand Down Expand Up @@ -374,7 +545,7 @@ export const getHttp =
}

try {
const response = await fetch(BASE_URL + url, { headers });
const response = await fetch(new URL(url, BASE_URL), { headers });

if (response.status === 401) {
console.log('Token expired, clearing token');
Expand Down
15 changes: 10 additions & 5 deletions web/src/api/generate_id.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { useHttp } from './core';
import { apiRequest, createQuery } from './core';

export const isValidId = (id?: string) => id && /^[\dA-Za-z]+$/.test(id);

export type ItemIdResponse = {
item_id: string;
};
export type ItemIdResponse = typeof useGenerateId.$inferData;

export const useGenerateId = () => useHttp<ItemIdResponse>('/api/item/next');
export const useGenerateId = createQuery({
queryKey: ['generate-id'],
fetcher: async () => {
const response = await apiRequest('/item/next', 'get', {});

return response.data;
},
});
24 changes: 9 additions & 15 deletions web/src/api/instance_settings.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import { queryOptions, useQuery } from '@tanstack/react-query';
import { apiRequest, createQuery } from './core';

import { getHttp } from './core';
export type InstanceSettings = typeof useInstanceSettings.$inferData;

export type IdCasingPreference = 'upper' | 'lower';
export type IdCasingPreference = InstanceSettings['id_casing_preference'];

export type InstanceSettings = {
id_casing_preference: IdCasingPreference;
};

export const instanceSettingsQueryOptions = queryOptions({
export const useInstanceSettings = createQuery({
queryKey: ['instance_settings'],
queryFn: getHttp<InstanceSettings>('/api/instance/settings', {
auth: 'include',
}),
});
fetcher: async () => {
const response = await apiRequest('/instance/settings', 'get', {});

export const useInstanceSettings = () => {
return useQuery(instanceSettingsQueryOptions);
};
return response.data;
},
});

export const formatIdCasing = (
id: string | undefined,
Expand Down
7 changes: 2 additions & 5 deletions web/src/api/item/generate.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { ApiRequest, useHttp } from '../core';
import { ApiRoute, useHttp } from '../core';

export const isValidId = (id?: string) => id && /^[\dA-Za-z]+$/.test(id);

export type ItemIdResponse = ApiRequest<
'/item/next',
'get'
>['response']['data'];
export type ItemIdResponse = ApiRoute<'/item/next', 'get'>['response']['data'];

// TODO: migrate away from useHttp (to mutation)
export const useGenerateId = () => useHttp<ItemIdResponse>('/api/item/next');
Loading

0 comments on commit c096b75

Please sign in to comment.