Created
February 10, 2021 00:28
-
-
Save sergiodxa/583421f07ce4e743f820b0758333e7d0 to your computer and use it in GitHub Desktop.
CRUD mutations (not read) for React Query + TS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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