Skip to content

Instantly share code, notes, and snippets.

@sergiodxa
Created February 10, 2021 00:28
Show Gist options
  • Save sergiodxa/583421f07ce4e743f820b0758333e7d0 to your computer and use it in GitHub Desktop.
Save sergiodxa/583421f07ce4e743f820b0758333e7d0 to your computer and use it in GitHub Desktop.
CRUD mutations (not read) for React Query + TS
import { useMutation, UseMutationOptions } from "react-query";
import { pluralize, singularize } from "inflected";
import { generatePath } from "react-router-dom";
type Config<Input, Output, Error> = {
/**
* Any React Query option for useMutation
* @type {UseMutationOptions<Output, APIError<Error>, Input>}
*/
options?: UseMutationOptions<Output, APIError<Error>, Input>;
};
type ConfigWithParent<Input, Output, Error> = Config<Input, Output, Error> & {
/**
* The parent resource with the type (`resource`) and id.
* If defined, they are prepended to the URL
* @type {({ resource: string; id: string | number })}
*/
parent?: {
/**
* The resource type, it will be pluralized
* @type {string}
*/
resource: string;
/**
* The ID of the entity
* @type {(string | number)}
*/
id: string | number;
};
};
const NO_CONTENT_RESPONSE = 204;
const BASE_URL = new URL("/api/", window.location.toString());
export class APIError<Body = unknown> extends Error {
constructor(message: string, public response: Response, public body: Body) {
super(message);
}
}
export class APIClientError<Body = unknown> extends APIError<Body> {}
export class APIServerError<Body = unknown> extends APIError<Body> {}
export async function api<Data, Error>(
url: URL,
// eslint-disable-next-line no-undef
request: RequestInit,
) {
const res = await fetch(url.toString(), request);
if (!res.ok && res.status >= 500) {
throw new APIServerError<Error>(res.statusText, res, await res.json());
}
if (!res.ok && res.status >= 400) {
throw new APIClientError<Error>(res.statusText, res, await res.json());
}
if (res.status == NO_CONTENT_RESPONSE) {
return { res, data: {} } as { res: Response; data: Data };
}
const data: Data = await res.json();
return { res, data };
}
async function create<Params, Data, Error>(
resource: string,
params: Params,
parent?: { resource: string; id: string | number },
) {
const url = new URL(
parent
? generatePath(":parent/:id/:resource", {
resource,
parent: pluralize(parent.resource),
id: parent.id,
})
: generatePath(":resource", { resource }),
BASE_URL,
);
const { data } = await api<Data, Error>(url, {
method: "POST",
body: JSON.stringify({
[singularize(resource).replace("/", "_")]: params,
}),
headers: { "Content-Type": "application/json; charset=utf-8" },
});
return data;
}
async function update<Params, Data, Error>(
resource: string,
id: string | number,
params: Params,
) {
const { data } = await api<Data, Error>(
new URL(generatePath(":resource/:id", { resource, id }), BASE_URL),
{
method: "PATCH",
body: JSON.stringify({
[singularize(resource).replace("/", "_")]: params,
}),
headers: { "Content-Type": "application/json; charset=utf-8" },
},
);
return data;
}
async function destroy<Error>(resource: string, id: string | number) {
await api<never, Error>(
new URL(generatePath(":resource/:id", { resource, id }), BASE_URL),
{
method: "DELETE",
headers: { "Content-Type": "application/json; charset=utf-8" },
},
);
}
/**
* This hook lets you run a mutation to create a new entity of a resource.
* The resource name and the params required are configured in the hook call.
*
* @example
* const { mutate: createProject } = useCreate<
* { title: string },
* Project,
* { type: "invalid_title" } | { type: "title_too_long" }
* >("project", {
* onError(error) {
* if (error.body.type === "invalid_title") // do something
* if (error.body.type === "title_too_long") // do something
* },
* });
* @template Input - The parameters required to create the resource
* @template Output - The output of the API if it's a success
* @template Error - The possible expected error messages
* @param {string} resource - The resource type, it will be pluralized
* @param {ConfigWithParent<Input, Output, Error>} [{ parent, options = {} }={}] - Extra options and the parent resource
*/
export function useCreate<Input = unknown, Output = unknown, Error = unknown>(
resource: string,
{ parent, options = {} }: ConfigWithParent<Input, Output, Error> = {},
) {
return useMutation<Output, APIError<Error>, Input>((input: Input) => {
return create<Input, Output, Error>(pluralize(resource), input, parent);
}, options);
}
/**
* This hook lets you run a mutation against a specific entity to update any
* field of the given entity.
*
* @example
* const { mutate: updateProject } = useUpdate<
* { title: string },
* Project,
* { type: "invalid_title" } | { type: "title_too_long" }
* >("project", project.id, {
* onError(error) {
* if (error.body.type === "invalid_title") // do something
* if (error.body.type === "title_too_long") // do something
* },
* });
* @template Input - The parameters required to create the resource
* @template Output - The output of the API if it's a success
* @template Error - The possible expected error messages
* @param {string} resource - The resource type, it will be pluralized
* @param {(number | string)} id - The ID of the entity to update
* @param {Config<Input, Output, Error>} [{ options = {} }={}] - Extra options
*/
export function useUpdate<Input = unknown, Output = unknown, Error = unknown>(
resource: string,
id: number | string,
{ options = {} }: Config<Input, Output, Error> = {},
) {
return useMutation((input: Input) => {
return update<Input, Output, Error>(pluralize(resource), id, input);
}, options);
}
/**
* This hook lets you run a mutation against a specific entity to delete.
*
* @example
* const { mutate: destroyProject } = useDestroy<
* { type: "unauthorized" } | { type: "not_found" }
* >("project", project.id, {
* onError(error) {
* if (error.body.type === "unauthorized") // do something
* if (error.body.type === "not_found") // do something
* },
* });
* @template Error
* @param {string} resource - The resource type, it will be pluralized
* @param {(number | string)} id - The ID of the entity to destroy
* @param {Config<void, void, Error>} [{ options = {} }={}] - Extra options
*/
export function useDestroy<Error = unknown>(
resource: string,
id: number | string,
{ options = {} }: Config<void, void, Error> = {},
) {
return useMutation(() => {
return destroy<Error>(pluralize(resource), id);
}, options);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment