Skip to content

Commit

Permalink
♻️ refactor(mutations): improve mutations type safety
Browse files Browse the repository at this point in the history
  • Loading branch information
thrownullexception committed Mar 31, 2024
1 parent 729d161 commit 417533b
Show file tree
Hide file tree
Showing 19 changed files with 224 additions and 385 deletions.
2 changes: 1 addition & 1 deletion src/frontend/components/SchemaForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const identity = (value: unknown) => value;

interface IProps<T> {
fields: IAppliedSchemaFormConfig<T>;
onSubmit: (data: T) => Promise<void | T>;
onSubmit: (data: T) => Promise<unknown>;
initialValues?: Partial<T>;
buttonText?: (submitting: boolean) => string;
action?: string;
Expand Down
20 changes: 8 additions & 12 deletions src/frontend/hooks/auth/preferences.store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useMutation } from "@tanstack/react-query";
import { makeActionRequest } from "frontend/lib/data/makeRequest";
import { useStorageApi } from "frontend/lib/data/useApi";
import { useWaitForResponseMutationOptions } from "frontend/lib/data/useMutate/useWaitForResponseMutationOptions";
Expand Down Expand Up @@ -42,9 +41,16 @@ export function useUpsertUserPreferenceMutation<T extends UserPreferencesKeys>(
key: T,
mutationOptions?: IUpsertConfigMutationOptions
) {
const apiMutateOptions = useWaitForResponseMutationOptions<
return useWaitForResponseMutationOptions<
UserPreferencesValueType<T>,
UserPreferencesValueType<T>
>({
mutationFn: async (values) => {
await makeActionRequest("PUT", userPrefrencesApiPath(key), {
data: values,
});
return values;
},
endpoints: [
userPrefrencesApiPath(key),
...(mutationOptions?.otherEndpoints || []),
Expand All @@ -54,14 +60,4 @@ export function useUpsertUserPreferenceMutation<T extends UserPreferencesKeys>(
},
successMessage: MAKE_USER_PREFERENCE_CRUD_CONFIG(key).MUTATION_LANG.SAVED,
});

return useMutation({
mutationFn: async (values: UserPreferencesValueType<T>) => {
await makeActionRequest("PUT", userPrefrencesApiPath(key), {
data: values,
});
return values;
},
...apiMutateOptions,
});
}
20 changes: 8 additions & 12 deletions src/frontend/hooks/configuration/configuration.store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useMutation } from "@tanstack/react-query";
import {
APP_CONFIGURATION_CONFIG,
AppConfigurationKeys,
Expand Down Expand Up @@ -82,9 +81,16 @@ export function useUpsertConfigurationMutation<T extends AppConfigurationKeys>(
entity?: string,
mutationOptions?: IUpsertConfigMutationOptions
) {
const apiMutateOptions = useWaitForResponseMutationOptions<
return useWaitForResponseMutationOptions<
AppConfigurationValueType<T>,
AppConfigurationValueType<T>
>({
mutationFn: async (values) => {
await makeActionRequest("PUT", configurationApiPath(key, entity, "PUT"), {
data: values,
});
return values;
},
endpoints: [
configurationApiPath(key, entity),
...(mutationOptions?.otherEndpoints || []),
Expand All @@ -94,14 +100,4 @@ export function useUpsertConfigurationMutation<T extends AppConfigurationKeys>(
},
successMessage: MAKE_APP_CONFIGURATION_CRUD_CONFIG(key).MUTATION_LANG.SAVED,
});

return useMutation({
mutationFn: async (values: AppConfigurationValueType<T>) => {
await makeActionRequest("PUT", configurationApiPath(key, entity, "PUT"), {
data: values,
});
return values;
},
...apiMutateOptions,
});
}
48 changes: 17 additions & 31 deletions src/frontend/hooks/data/data.store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import qs from "qs";
import { useRouter } from "next/router";
import { useMutation } from "@tanstack/react-query";
import { FieldQueryFilter, FilterOperators } from "shared/types/data";
import { CRUD_CONFIG_NOT_FOUND } from "frontend/lib/crud-config";
import { makeActionRequest } from "frontend/lib/data/makeRequest";
Expand Down Expand Up @@ -142,9 +141,12 @@ export function useEntityDataCreationMutation(
) {
const entityCrudConfig = useEntityCrudConfig(entity);
const router = useRouter();
const apiMutateOptions = useWaitForResponseMutationOptions<
Record<string, string>
return useWaitForResponseMutationOptions<
Record<string, string>,
{ id: string }
>({
mutationFn: async (data) =>
await makeActionRequest("POST", `/api/data/${entity}`, { data }),
endpoints: DATA_MUTATION_ENDPOINTS_TO_CLEAR(entity),
onSuccessActionWithFormData: ({ id }) => {
option?.onSuccessActionWithFormData(id);
Expand All @@ -160,37 +162,25 @@ export function useEntityDataCreationMutation(
},
}),
});

return useMutation({
mutationFn: async (data: Record<string, string>) =>
await makeActionRequest("POST", `/api/data/${entity}`, { data }),
...apiMutateOptions,
});
}

export function useEntityDataUpdationMutation(
entity: string,
entityId: string
) {
const entityCrudConfig = useEntityCrudConfig(entity);
const apiMutateOptions = useWaitForResponseMutationOptions<
Record<string, string>
>({
endpoints: [
...SINGLE_DATA_MUTATION_ENDPOINTS_TO_CLEAR({ entity, entityId }),
...DATA_MUTATION_ENDPOINTS_TO_CLEAR(entity),
],
successMessage: entityCrudConfig.MUTATION_LANG.EDIT,
});

const metadata = useEntityMetadataDetails({ entity, entityId });

return useMutation({
mutationFn: async (data: Record<string, string>) =>
return useWaitForResponseMutationOptions<Record<string, string>>({
mutationFn: async (data) =>
await makeActionRequest("PATCH", `/api/data/${entity}/${entityId}`, {
data: { ...data, ...metadata },
}),
...apiMutateOptions,
endpoints: [
...SINGLE_DATA_MUTATION_ENDPOINTS_TO_CLEAR({ entity, entityId }),
...DATA_MUTATION_ENDPOINTS_TO_CLEAR(entity),
],
successMessage: entityCrudConfig.MUTATION_LANG.EDIT,
});
}

Expand All @@ -206,9 +196,11 @@ export function useEntityDataDeletionMutation(
) {
const router = useRouter();
const entityCrudConfig = useEntityCrudConfig(entity);
const apiMutateOptions = useWaitForResponseMutationOptions<
Record<string, string>
>({

// eyes on optimstic delete here
return useWaitForResponseMutationOptions<string>({
mutationFn: async (id) =>
await makeActionRequest("DELETE", `/api/data/${entity}/${id}`),
endpoints: [
...SINGLE_DATA_MUTATION_ENDPOINTS_TO_CLEAR({ entity, entityId }),
...DATA_MUTATION_ENDPOINTS_TO_CLEAR(entity),
Expand All @@ -220,10 +212,4 @@ export function useEntityDataDeletionMutation(
},
successMessage: entityCrudConfig.MUTATION_LANG.DELETE,
});
// eyes on optimstic delete here
return useMutation({
mutationFn: async (id: string) =>
await makeActionRequest("DELETE", `/api/data/${entity}/${id}`),
...apiMutateOptions,
});
}
9 changes: 5 additions & 4 deletions src/frontend/lib/data/useMutate/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ export type ToastMessageInput =
| { message: string; action: { label: string; action: () => void } }
| string;

export interface IApiMutateOptions<T, K, V> {
export interface IApiMutateOptions<T, V, R> {
dataQueryPath: string;
otherEndpoints?: string[];
onMutate: (oldData: T | undefined, form: K) => T;
onMutate: (oldData: T | undefined, form: V) => T;
mutationFn: (form: V) => Promise<R>;
successMessage?: ToastMessageInput;
smartSuccessMessage?: (formData: V) => ToastMessageInput;
onSuccessActionWithFormData?: (formData: V) => void;
smartSuccessMessage?: (formData: R) => ToastMessageInput;
onSuccessActionWithFormData?: (formData: R) => void;
}

export const FOR_CODE_COV = 1;
22 changes: 0 additions & 22 deletions src/frontend/lib/data/useMutate/useApiMutate.ts

This file was deleted.

38 changes: 29 additions & 9 deletions src/frontend/lib/data/useMutate/useApiMutateOptimisticOptions.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,40 @@
import { noop } from "shared/lib/noop";
import { useQueryClient } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { ToastService } from "frontend/lib/toast";
import { IApiMutateOptions } from "./types";
import { useApiMutate } from "./useApiMutate";
import { getQueryCachekey } from "../constants/getQueryCacheKey";

export function useApiMutateOptimisticOptions<T, K, V = void>(
options: IApiMutateOptions<T, K, V>
function useApiMutate<T>(endpoint: string) {
const queryClient = useQueryClient();
const queryKey = getQueryCachekey(endpoint);

return {
set: async (mutateOldData: (oldData: T | undefined) => T) => {
await queryClient.cancelQueries({ queryKey });
const previousData = queryClient.getQueryData<T>(queryKey);
queryClient.setQueryData<T>(queryKey, mutateOldData);
return previousData;
},
invalidate: () => {
queryClient.invalidateQueries({ queryKey });
},
reset: (previousData: T | undefined) => {
queryClient.setQueryData(queryKey, previousData);
},
};
}

export function useApiMutateOptimisticOptions<T, V, R = void>(
options: IApiMutateOptions<T, V, R>
) {
const apiMutate = useApiMutate<T>(options.dataQueryPath);
const queryClient = useQueryClient();

return {
onMutate: async (formData: K) =>
return useMutation({
mutationFn: options.mutationFn,
onMutate: async (formData: V) =>
apiMutate.set((oldData) => options.onMutate(oldData, formData)),
onSuccess: async (requestResponse: V) => {
onSuccess: async (requestResponse: R) => {
if (options.smartSuccessMessage) {
ToastService.success(options.smartSuccessMessage(requestResponse));
} else if (options.successMessage) {
Expand All @@ -33,7 +53,7 @@ export function useApiMutateOptimisticOptions<T, K, V = void>(
},
onError: (
error: { message: string },
formData: K,
formData: V,
oldData: T | undefined
) => {
noop(formData, error);
Expand All @@ -46,5 +66,5 @@ export function useApiMutateOptimisticOptions<T, K, V = void>(
onSettled: () => {
apiMutate.invalidate();
},
};
});
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { useQueryClient } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { ToastService } from "frontend/lib/toast";
import { getQueryCachekey } from "../constants/getQueryCacheKey";
import { ToastMessageInput } from "./types";

export interface IWaitForResponseMutationOptions<T> {
export interface IWaitForResponseMutationOptions<V, R> {
endpoints: string[];
redirect?: string;
onSuccessActionWithFormData?: (formData: T) => void;
mutationFn: (formData: V) => Promise<R>;
onSuccessActionWithFormData?: (response: R) => void;
successMessage?: ToastMessageInput;
smartSuccessMessage?: (formData: T) => ToastMessageInput;
smartSuccessMessage?: (formData: R) => ToastMessageInput;
}

const PASS_DATA_FROM_HANDLER_ERROR_MESSAGE =
"Please return in the mutation what data you want to pass to the success handlers";

export function useWaitForResponseMutationOptions<T>(
options: IWaitForResponseMutationOptions<T>
export function useWaitForResponseMutationOptions<V, R = void>(
options: IWaitForResponseMutationOptions<V, R>
) {
const queryClient = useQueryClient();

return {
onSuccess: async (formData: T | undefined) => {
return useMutation({
mutationFn: options.mutationFn,
onSuccess: async (formData: R | undefined) => {
options.endpoints.forEach((queryKey) => {
queryClient.invalidateQueries({ queryKey: getQueryCachekey(queryKey) });
});
Expand All @@ -46,5 +48,5 @@ export function useWaitForResponseMutationOptions<T>(
error.message || "Something went wrong. Please try again"
);
},
};
});
}
Loading

0 comments on commit 417533b

Please sign in to comment.