From f50da00ef31dbf8caf3ff5940c0a7d413a047012 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Bansal Date: Wed, 13 May 2020 17:12:59 +0530 Subject: [PATCH] feat: add support for path params is useMutate --- src/Mutate.tsx | 23 +- src/scripts/import-open-api.ts | 24 +- .../import-open-api.test.ts.snap | 28 +- src/scripts/tests/import-open-api.test.ts | 762 +++++++++--------- src/useGet.test.tsx | 64 +- src/useGet.tsx | 59 +- src/useMutate.test.tsx | 58 +- src/useMutate.tsx | 55 +- 8 files changed, 637 insertions(+), 436 deletions(-) diff --git a/src/Mutate.tsx b/src/Mutate.tsx index 52c2a799..521b50b3 100644 --- a/src/Mutate.tsx +++ b/src/Mutate.tsx @@ -16,16 +16,20 @@ export interface States { error?: GetState["error"]; } -export interface MutateRequestOptions extends RequestInit { +export interface MutateRequestOptions extends RequestInit { /** * Query parameters */ queryParams?: TQueryParams; + /** + * Path parameters + */ + pathParams?: TPathParams; } -export type MutateMethod = ( +export type MutateMethod = ( data: TRequestBody, - mutateRequestOptions?: MutateRequestOptions, + mutateRequestOptions?: MutateRequestOptions, ) => Promise; /** @@ -40,7 +44,7 @@ export interface Meta { /** * Props for the component. */ -export interface MutateProps { +export interface MutateProps { /** * The path at which to request data, * typically composed by parents or the RestfulProvider. @@ -83,7 +87,7 @@ export interface MutateProps { * @param actions - a key/value map of HTTP verbs, aliasing destroy to DELETE. */ children: ( - mutate: MutateMethod, + mutate: MutateMethod, states: States, meta: Meta, ) => React.ReactNode; @@ -111,8 +115,8 @@ export interface MutateState { * is a named class because it is useful in * debugging. */ -class ContextlessMutate extends React.Component< - MutateProps & InjectedProps, +class ContextlessMutate extends React.Component< + MutateProps & InjectedProps, MutateState > { public readonly state: Readonly> = { @@ -137,7 +141,10 @@ class ContextlessMutate extends React this.abortController.abort(); } - public mutate = async (body: TRequestBody, mutateRequestOptions?: MutateRequestOptions) => { + public mutate = async ( + body: TRequestBody, + mutateRequestOptions?: MutateRequestOptions, + ) => { const { __internal_hasExplicitBase, base, diff --git a/src/scripts/import-open-api.ts b/src/scripts/import-open-api.ts index 6d88ce45..1c9d74eb 100644 --- a/src/scripts/import-open-api.ts +++ b/src/scripts/import-open-api.ts @@ -349,7 +349,7 @@ export const generateRestfulComponent = ( throw new Error(`The path params ${p} can't be found in parameters (${operation.operationId})`); } }) - .join("; "); + .join(";\n "); const queryParamsType = queryParams .map(p => { @@ -399,7 +399,7 @@ export const generateRestfulComponent = ( verb === "get" ? `${needAResponseComponent ? componentName + "Response" : responseTypes}, ${ queryParamsType ? componentName + "QueryParams" : "void" - }` + }, ${paramsInPath.length ? componentName + "PathParams" : "void"}` : `${needAResponseComponent ? componentName + "Response" : responseTypes}, ${ queryParamsType ? componentName + "QueryParams" : "void" }, ${ @@ -408,7 +408,7 @@ export const generateRestfulComponent = ( : needARequestBodyComponent ? componentName + "RequestBody" : requestBodyTypes - }`; + }, ${paramsInPath.length ? componentName + "PathParams" : "void"}`; const customPropsEntries = Object.entries(customProps); @@ -432,6 +432,14 @@ export ${ export interface ${componentName}QueryParams { ${queryParamsType}; } +` + : "" + }${ + paramsInPath.length + ? ` +export interface ${componentName}PathParams { + ${paramsTypes} +} ` : "" }${ @@ -468,19 +476,19 @@ ${description}export const ${componentName} = (${ // Hooks version output += `export type Use${componentName}Props = Omit, "path"${ verb === "get" ? "" : ` | "verb"` - }>${paramsInPath.length ? ` & {${paramsTypes}}` : ""}; + }>${paramsInPath.length ? ` & ${componentName}PathParams` : ""}; ${description}export const use${componentName} = (${ paramsInPath.length ? `{${paramsInPath.join(", ")}, ...props}` : "props" }: Use${componentName}Props) => use${Component}<${genericsTypes}>(${ verb === "get" ? "" : `"${verb.toUpperCase()}", ` - }\`${route}\`, ${ + }${paramsInPath.length ? `({ ${paramsInPath.join(", ")} }) => \`${route}\`` : `\`${route}\``}, { ${ customPropsEntries.length ? `{ ${customPropsEntries .map(([key, value]) => `${key}:${reactPropsValueToObjectValue(value || "")}`) - .join(", ")}, ...props}` - : "props" - }); + .join(", ")}, ` + : "" + }${paramsInPath.length ? `pathParams: { ${paramsInPath.join(", ")} }, ` : ""}...props }); `; diff --git a/src/scripts/tests/__snapshots__/import-open-api.test.ts.snap b/src/scripts/tests/__snapshots__/import-open-api.test.ts.snap index 45948486..499ebe9f 100644 --- a/src/scripts/tests/__snapshots__/import-open-api.test.ts.snap +++ b/src/scripts/tests/__snapshots__/import-open-api.test.ts.snap @@ -88,7 +88,7 @@ export const FindPets = (props: FindPetsProps) => ( /> ); -export type UseFindPetsProps = Omit, \\"path\\">; +export type UseFindPetsProps = Omit, \\"path\\">; /** * Returns all pets from the system that the user has access to @@ -97,7 +97,7 @@ export type UseFindPetsProps = Omit, \\" * Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. * */ -export const useFindPets = (props: UseFindPetsProps) => useGet(\`/pets\`, props); +export const useFindPets = (props: UseFindPetsProps) => useGet(\`/pets\`, { ...props }); export type AddPetProps = Omit, \\"path\\" | \\"verb\\">; @@ -113,14 +113,18 @@ export const AddPet = (props: AddPetProps) => ( /> ); -export type UseAddPetProps = Omit, \\"path\\" | \\"verb\\">; +export type UseAddPetProps = Omit, \\"path\\" | \\"verb\\">; /** * Creates a new pet in the store. Duplicates are allowed */ -export const useAddPet = (props: UseAddPetProps) => useMutate(\\"POST\\", \`/pets\`, props); +export const useAddPet = (props: UseAddPetProps) => useMutate(\\"POST\\", \`/pets\`, { ...props }); +export interface FindPetByIdPathParams { + id: number +} + export type FindPetByIdProps = Omit, \\"path\\"> & {id: number}; /** @@ -133,12 +137,12 @@ export const FindPetById = ({id, ...props}: FindPetByIdProps) => ( /> ); -export type UseFindPetByIdProps = Omit, \\"path\\"> & {id: number}; +export type UseFindPetByIdProps = Omit, \\"path\\"> & FindPetByIdPathParams; /** * Returns a user based on a single ID, if the user does not have access to the pet */ -export const useFindPetById = ({id, ...props}: UseFindPetByIdProps) => useGet(\`/pets/\${id}\`, props); +export const useFindPetById = ({id, ...props}: UseFindPetByIdProps) => useGet(({ id }) => \`/pets/\${id}\`, { pathParams: { id }, ...props }); export type DeletePetProps = Omit, \\"path\\" | \\"verb\\">; @@ -154,14 +158,18 @@ export const DeletePet = (props: DeletePetProps) => ( /> ); -export type UseDeletePetProps = Omit, \\"path\\" | \\"verb\\">; +export type UseDeletePetProps = Omit, \\"path\\" | \\"verb\\">; /** * deletes a single pet based on the ID supplied */ -export const useDeletePet = (props: UseDeletePetProps) => useMutate(\\"DELETE\\", \`/pets\`, props); +export const useDeletePet = (props: UseDeletePetProps) => useMutate(\\"DELETE\\", \`/pets\`, { ...props }); +export interface UpdatePetPathParams { + id: number +} + export type UpdatePetProps = Omit, \\"path\\" | \\"verb\\"> & {id: number}; /** @@ -175,12 +183,12 @@ export const UpdatePet = ({id, ...props}: UpdatePetProps) => ( /> ); -export type UseUpdatePetProps = Omit, \\"path\\" | \\"verb\\"> & {id: number}; +export type UseUpdatePetProps = Omit, \\"path\\" | \\"verb\\"> & UpdatePetPathParams; /** * Updates a pet in the store. */ -export const useUpdatePet = ({id, ...props}: UseUpdatePetProps) => useMutate(\\"PATCH\\", \`/pets/\${id}\`, props); +export const useUpdatePet = ({id, ...props}: UseUpdatePetProps) => useMutate(\\"PATCH\\", ({ id }) => \`/pets/\${id}\`, { pathParams: { id }, ...props }); " `; diff --git a/src/scripts/tests/import-open-api.test.ts b/src/scripts/tests/import-open-api.test.ts index dc34b922..e6697a64 100644 --- a/src/scripts/tests/import-open-api.test.ts +++ b/src/scripts/tests/import-open-api.test.ts @@ -842,28 +842,28 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "get", "/fields", [])).toMatchInlineSnapshot(` - " - export type ListFieldsProps = Omit, \\"path\\">; - - /** - * List all fields for the use case schema - */ - export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> - ); - - export type UseListFieldsProps = Omit, \\"path\\">; - - /** - * List all fields for the use case schema - */ - export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); - - " - `); + " + export type ListFieldsProps = Omit, \\"path\\">; + + /** + * List all fields for the use case schema + */ + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); + + export type UseListFieldsProps = Omit, \\"path\\">; + + /** + * List all fields for the use case schema + */ + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, { ...props }); + + " + `); }); it("should have a nice documentation with summary and description", () => { @@ -881,32 +881,32 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "get", "/fields", [])).toMatchInlineSnapshot(` - " - export type ListFieldsProps = Omit, \\"path\\">; - - /** - * List all fields for the use case schema - * - * This is a longer description to describe my endpoint - */ - export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> - ); - - export type UseListFieldsProps = Omit, \\"path\\">; - - /** - * List all fields for the use case schema - * - * This is a longer description to describe my endpoint - */ - export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); - - " - `); + " + export type ListFieldsProps = Omit, \\"path\\">; + + /** + * List all fields for the use case schema + * + * This is a longer description to describe my endpoint + */ + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); + + export type UseListFieldsProps = Omit, \\"path\\">; + + /** + * List all fields for the use case schema + * + * This is a longer description to describe my endpoint + */ + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, { ...props }); + + " + `); }); it("should add a fallback if the error is not defined", () => { @@ -923,28 +923,28 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "get", "/fields", [])).toMatchInlineSnapshot(` - " - export type ListFieldsProps = Omit, \\"path\\">; - - /** - * List all fields for the use case schema - */ - export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> - ); - - export type UseListFieldsProps = Omit, \\"path\\">; - - /** - * List all fields for the use case schema - */ - export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); - - " - `); + " + export type ListFieldsProps = Omit, \\"path\\">; + + /** + * List all fields for the use case schema + */ + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); + + export type UseListFieldsProps = Omit, \\"path\\">; + + /** + * List all fields for the use case schema + */ + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, { ...props }); + + " + `); }); it("should remove duplicate types", () => { @@ -974,28 +974,28 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "get", "/fields", [])).toMatchInlineSnapshot(` - " - export type ListFieldsProps = Omit, \\"path\\">; - - /** - * List all fields for the use case schema - */ - export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> - ); - - export type UseListFieldsProps = Omit, \\"path\\">; - - /** - * List all fields for the use case schema - */ - export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); - - " - `); + " + export type ListFieldsProps = Omit, \\"path\\">; + + /** + * List all fields for the use case schema + */ + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); + + export type UseListFieldsProps = Omit, \\"path\\">; + + /** + * List all fields for the use case schema + */ + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, { ...props }); + + " + `); }); it("should deal with parameters in query", () => { @@ -1064,12 +1064,12 @@ describe("scripts/import-open-api", () => { /> ); - export type UseListFieldsProps = Omit, \\"path\\">; + export type UseListFieldsProps = Omit, \\"path\\">; /** * List all fields for the use case schema */ - export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, { ...props }); " `); @@ -1147,12 +1147,12 @@ describe("scripts/import-open-api", () => { /> ); - export type UseListFieldsProps = Omit, \\"path\\">; + export type UseListFieldsProps = Omit, \\"path\\">; /** * List all fields for the use case schema */ - export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, { ...props }); " `); @@ -1201,28 +1201,32 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "get", "/fields/{id}", [])).toMatchInlineSnapshot(` - " - export type ListFieldsProps = Omit, \\"path\\"> & {id: string}; - - /** - * List all fields for the use case schema - */ - export const ListFields = ({id, ...props}: ListFieldsProps) => ( - - path={\`/fields/\${id}\`} - {...props} - /> - ); - - export type UseListFieldsProps = Omit, \\"path\\"> & {id: string}; - - /** - * List all fields for the use case schema - */ - export const useListFields = ({id, ...props}: UseListFieldsProps) => useGet(\`/fields/\${id}\`, props); - - " - `); + " + export interface ListFieldsPathParams { + id: string + } + + export type ListFieldsProps = Omit, \\"path\\"> & {id: string}; + + /** + * List all fields for the use case schema + */ + export const ListFields = ({id, ...props}: ListFieldsProps) => ( + + path={\`/fields/\${id}\`} + {...props} + /> + ); + + export type UseListFieldsProps = Omit, \\"path\\"> & ListFieldsPathParams; + + /** + * List all fields for the use case schema + */ + export const useListFields = ({id, ...props}: UseListFieldsProps) => useGet(({ id }) => \`/fields/\${id}\`, { pathParams: { id }, ...props }); + + " + `); }); it("should deal with parameters in path (root level)", () => { @@ -1275,28 +1279,32 @@ describe("scripts/import-open-api", () => { ], ), ).toMatchInlineSnapshot(` - " - export type ListFieldsProps = Omit, \\"path\\"> & {id: string}; - - /** - * List all fields for the use case schema - */ - export const ListFields = ({id, ...props}: ListFieldsProps) => ( - - path={\`/fields/\${id}\`} - {...props} - /> - ); - - export type UseListFieldsProps = Omit, \\"path\\"> & {id: string}; - - /** - * List all fields for the use case schema - */ - export const useListFields = ({id, ...props}: UseListFieldsProps) => useGet(\`/fields/\${id}\`, props); - - " - `); + " + export interface ListFieldsPathParams { + id: string + } + + export type ListFieldsProps = Omit, \\"path\\"> & {id: string}; + + /** + * List all fields for the use case schema + */ + export const ListFields = ({id, ...props}: ListFieldsProps) => ( + + path={\`/fields/\${id}\`} + {...props} + /> + ); + + export type UseListFieldsProps = Omit, \\"path\\"> & ListFieldsPathParams; + + /** + * List all fields for the use case schema + */ + export const useListFields = ({id, ...props}: UseListFieldsProps) => useGet(({ id }) => \`/fields/\${id}\`, { pathParams: { id }, ...props }); + + " + `); }); it("should generate a Mutate type component", () => { @@ -1342,29 +1350,33 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "put", "/use-cases/{useCaseId}", [])).toMatchInlineSnapshot(` - " - export type UpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; - - /** - * Update use case details - */ - export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( - - verb=\\"PUT\\" - path={\`/use-cases/\${useCaseId}\`} - {...props} - /> - ); - - export type UseUpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; - - /** - * Update use case details - */ - export const useUpdateUseCase = ({useCaseId, ...props}: UseUpdateUseCaseProps) => useMutate(\\"PUT\\", \`/use-cases/\${useCaseId}\`, props); - - " - `); + " + export interface UpdateUseCasePathParams { + useCaseId: string + } + + export type UpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + + /** + * Update use case details + */ + export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( + + verb=\\"PUT\\" + path={\`/use-cases/\${useCaseId}\`} + {...props} + /> + ); + + export type UseUpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & UpdateUseCasePathParams; + + /** + * Update use case details + */ + export const useUpdateUseCase = ({useCaseId, ...props}: UseUpdateUseCaseProps) => useMutate(\\"PUT\\", ({ useCaseId }) => \`/use-cases/\${useCaseId}\`, { pathParams: { useCaseId }, ...props }); + + " + `); }); it("should generate a request body type if it's inline in the specs", () => { @@ -1420,6 +1432,10 @@ describe("scripts/import-open-api", () => { expect(generateRestfulComponent(operation, "put", "/use-cases/{useCaseId}", [])).toMatchInlineSnapshot(` " + export interface UpdateUseCasePathParams { + useCaseId: string + } + export interface UpdateUseCaseRequestBody { /** * The use case name @@ -1441,12 +1457,12 @@ describe("scripts/import-open-api", () => { /> ); - export type UseUpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + export type UseUpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & UpdateUseCasePathParams; /** * Update use case details */ - export const useUpdateUseCase = ({useCaseId, ...props}: UseUpdateUseCaseProps) => useMutate(\\"PUT\\", \`/use-cases/\${useCaseId}\`, props); + export const useUpdateUseCase = ({useCaseId, ...props}: UseUpdateUseCaseProps) => useMutate(\\"PUT\\", ({ useCaseId }) => \`/use-cases/\${useCaseId}\`, { pathParams: { useCaseId }, ...props }); " `); @@ -1510,34 +1526,38 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "put", "/use-cases/{useCaseId}", [])).toMatchInlineSnapshot(` - " - export interface UpdateUseCaseResponse { - id: string; - name?: string; - } - - export type UpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; - - /** - * Update use case details - */ - export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( - - verb=\\"PUT\\" - path={\`/use-cases/\${useCaseId}\`} - {...props} - /> - ); - - export type UseUpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; - - /** - * Update use case details - */ - export const useUpdateUseCase = ({useCaseId, ...props}: UseUpdateUseCaseProps) => useMutate(\\"PUT\\", \`/use-cases/\${useCaseId}\`, props); - - " - `); + " + export interface UpdateUseCaseResponse { + id: string; + name?: string; + } + + export interface UpdateUseCasePathParams { + useCaseId: string + } + + export type UpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + + /** + * Update use case details + */ + export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( + + verb=\\"PUT\\" + path={\`/use-cases/\${useCaseId}\`} + {...props} + /> + ); + + export type UseUpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & UpdateUseCasePathParams; + + /** + * Update use case details + */ + export const useUpdateUseCase = ({useCaseId, ...props}: UseUpdateUseCaseProps) => useMutate(\\"PUT\\", ({ useCaseId }) => \`/use-cases/\${useCaseId}\`, { pathParams: { useCaseId }, ...props }); + + " + `); }); it("should ignore 3xx responses", () => { @@ -1601,34 +1621,38 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "put", "/use-cases/{useCaseId}", [])).toMatchInlineSnapshot(` - " - export interface UpdateUseCaseResponse { - id: string; - name?: string; - } - - export type UpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; - - /** - * Update use case details - */ - export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( - - verb=\\"PUT\\" - path={\`/use-cases/\${useCaseId}\`} - {...props} - /> - ); - - export type UseUpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; - - /** - * Update use case details - */ - export const useUpdateUseCase = ({useCaseId, ...props}: UseUpdateUseCaseProps) => useMutate(\\"PUT\\", \`/use-cases/\${useCaseId}\`, props); - - " - `); + " + export interface UpdateUseCaseResponse { + id: string; + name?: string; + } + + export interface UpdateUseCasePathParams { + useCaseId: string + } + + export type UpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + + /** + * Update use case details + */ + export const UpdateUseCase = ({useCaseId, ...props}: UpdateUseCaseProps) => ( + + verb=\\"PUT\\" + path={\`/use-cases/\${useCaseId}\`} + {...props} + /> + ); + + export type UseUpdateUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & UpdateUseCasePathParams; + + /** + * Update use case details + */ + export const useUpdateUseCase = ({useCaseId, ...props}: UseUpdateUseCaseProps) => useMutate(\\"PUT\\", ({ useCaseId }) => \`/use-cases/\${useCaseId}\`, { pathParams: { useCaseId }, ...props }); + + " + `); }); it("should ignore the last param of a delete call (the id is give after)", () => { @@ -1669,29 +1693,29 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "delete", "/use-cases/{useCaseId}", [])).toMatchInlineSnapshot(` - " - export type DeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; - - /** - * Delete use case - */ - export const DeleteUseCase = (props: DeleteUseCaseProps) => ( - - verb=\\"DELETE\\" - path={\`/use-cases\`} - {...props} - /> - ); - - export type UseDeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; - - /** - * Delete use case - */ - export const useDeleteUseCase = (props: UseDeleteUseCaseProps) => useMutate(\\"DELETE\\", \`/use-cases\`, props); - - " - `); + " + export type DeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; + + /** + * Delete use case + */ + export const DeleteUseCase = (props: DeleteUseCaseProps) => ( + + verb=\\"DELETE\\" + path={\`/use-cases\`} + {...props} + /> + ); + + export type UseDeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; + + /** + * Delete use case + */ + export const useDeleteUseCase = (props: UseDeleteUseCaseProps) => useMutate(\\"DELETE\\", \`/use-cases\`, { ...props }); + + " + `); }); it("should only remove the last params in delete operation", () => { const operation: OperationObject = { @@ -1731,29 +1755,33 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "delete", "/use-cases/{useCaseId}/secret", [])).toMatchInlineSnapshot(` - " - export type DeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; - - /** - * Delete use case - */ - export const DeleteUseCase = ({useCaseId, ...props}: DeleteUseCaseProps) => ( - - verb=\\"DELETE\\" - path={\`/use-cases/\${useCaseId}/secret\`} - {...props} - /> - ); - - export type UseDeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; - - /** - * Delete use case - */ - export const useDeleteUseCase = ({useCaseId, ...props}: UseDeleteUseCaseProps) => useMutate(\\"DELETE\\", \`/use-cases/\${useCaseId}/secret\`, props); - - " - `); + " + export interface DeleteUseCasePathParams { + useCaseId: string + } + + export type DeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & {useCaseId: string}; + + /** + * Delete use case + */ + export const DeleteUseCase = ({useCaseId, ...props}: DeleteUseCaseProps) => ( + + verb=\\"DELETE\\" + path={\`/use-cases/\${useCaseId}/secret\`} + {...props} + /> + ); + + export type UseDeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\"> & DeleteUseCasePathParams; + + /** + * Delete use case + */ + export const useDeleteUseCase = ({useCaseId, ...props}: UseDeleteUseCaseProps) => useMutate(\\"DELETE\\", ({ useCaseId }) => \`/use-cases/\${useCaseId}/secret\`, { pathParams: { useCaseId }, ...props }); + + " + `); }); it("should take the type from open-api specs", () => { @@ -1794,29 +1822,29 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "delete", "/use-cases/{useCaseId}", [])).toMatchInlineSnapshot(` - " - export type DeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; - - /** - * Delete use case - */ - export const DeleteUseCase = (props: DeleteUseCaseProps) => ( - - verb=\\"DELETE\\" - path={\`/use-cases\`} - {...props} - /> - ); - - export type UseDeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; - - /** - * Delete use case - */ - export const useDeleteUseCase = (props: UseDeleteUseCaseProps) => useMutate(\\"DELETE\\", \`/use-cases\`, props); - - " - `); + " + export type DeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; + + /** + * Delete use case + */ + export const DeleteUseCase = (props: DeleteUseCaseProps) => ( + + verb=\\"DELETE\\" + path={\`/use-cases\`} + {...props} + /> + ); + + export type UseDeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; + + /** + * Delete use case + */ + export const useDeleteUseCase = (props: UseDeleteUseCaseProps) => useMutate(\\"DELETE\\", \`/use-cases\`, { ...props }); + + " + `); }); it("should take the type from open-api specs (ref)", () => { @@ -1852,29 +1880,29 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "delete", "/use-cases/{useCaseId}", [])).toMatchInlineSnapshot(` - " - export type DeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; - - /** - * Delete use case - */ - export const DeleteUseCase = (props: DeleteUseCaseProps) => ( - - verb=\\"DELETE\\" - path={\`/use-cases\`} - {...props} - /> - ); - - export type UseDeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; - - /** - * Delete use case - */ - export const useDeleteUseCase = (props: UseDeleteUseCaseProps) => useMutate(\\"DELETE\\", \`/use-cases\`, props); - - " - `); + " + export type DeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; + + /** + * Delete use case + */ + export const DeleteUseCase = (props: DeleteUseCaseProps) => ( + + verb=\\"DELETE\\" + path={\`/use-cases\`} + {...props} + /> + ); + + export type UseDeleteUseCaseProps = Omit, \\"path\\" | \\"verb\\">; + + /** + * Delete use case + */ + export const useDeleteUseCase = (props: UseDeleteUseCaseProps) => useMutate(\\"DELETE\\", \`/use-cases\`, { ...props }); + + " + `); }); it("should generate a Poll compoment if the `prefer` token is present", () => { @@ -1909,38 +1937,38 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "get", "/fields", [])).toMatchInlineSnapshot(` - " - export type ListFieldsProps = Omit, \\"path\\">; - - /** - * List all fields for the use case schema - */ - export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> - ); - - export type UseListFieldsProps = Omit, \\"path\\">; - - /** - * List all fields for the use case schema - */ - export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); - - export type PollListFieldsProps = Omit, \\"path\\">; - - // List all fields for the use case schema (long polling) - export const PollListFields = (props: PollListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> - ); - - " - `); + " + export type ListFieldsProps = Omit, \\"path\\">; + + /** + * List all fields for the use case schema + */ + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); + + export type UseListFieldsProps = Omit, \\"path\\">; + + /** + * List all fields for the use case schema + */ + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, { ...props }); + + export type PollListFieldsProps = Omit, \\"path\\">; + + // List all fields for the use case schema (long polling) + export const PollListFields = (props: PollListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); + + " + `); }); }); it("should deal with no 2xx response case", () => { @@ -1964,28 +1992,28 @@ describe("scripts/import-open-api", () => { }; expect(generateRestfulComponent(operation, "get", "/fields", [])).toMatchInlineSnapshot(` - " - export type ListFieldsProps = Omit, \\"path\\">; - - /** - * List all fields for the use case schema - */ - export const ListFields = (props: ListFieldsProps) => ( - - path={\`/fields\`} - {...props} - /> - ); - - export type UseListFieldsProps = Omit, \\"path\\">; - - /** - * List all fields for the use case schema - */ - export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, props); - - " - `); + " + export type ListFieldsProps = Omit, \\"path\\">; + + /** + * List all fields for the use case schema + */ + export const ListFields = (props: ListFieldsProps) => ( + + path={\`/fields\`} + {...props} + /> + ); + + export type UseListFieldsProps = Omit, \\"path\\">; + + /** + * List all fields for the use case schema + */ + export const useListFields = (props: UseListFieldsProps) => useGet(\`/fields\`, { ...props }); + + " + `); }); describe("reactPropsValueToObjectValue", () => { diff --git a/src/useGet.test.tsx b/src/useGet.test.tsx index b8400988..314f5c80 100644 --- a/src/useGet.test.tsx +++ b/src/useGet.test.tsx @@ -4,6 +4,7 @@ import "isomorphic-fetch"; import times from "lodash/times"; import nock from "nock"; import React, { useState } from "react"; +import { renderHook } from "@testing-library/react-hooks"; import { RestfulProvider, useGet } from "./index"; import { Omit, UseGetProps } from "./useGet"; @@ -1117,7 +1118,10 @@ describe("useGet hook", () => { code: number; } - type UseGetMyCustomEndpoint = Omit, "path">; + type UseGetMyCustomEndpoint = Omit< + UseGetProps, + "path" + >; const useGetMyCustomEndpoint = (props: UseGetMyCustomEndpoint) => useGet("/", props); @@ -1140,4 +1144,62 @@ describe("useGet hook", () => { expect(children.mock.calls[1][0].data).toEqual({ id: 42 }); }); }); + + describe("with pathParams", () => { + it("should resolve path parameters if specified", async () => { + nock("https://my-awesome-api.fake") + .get("/plop/one") + .reply(200, { id: 1 }); + + const wrapper: React.FC = ({ children }) => ( + {children} + ); + const { result } = renderHook( + () => + useGet<{ id: number }, {}, {}, { id: string }>(({ id }) => `plop/${id}`, { + pathParams: { id: "two" }, + lazy: true, + }), + { + wrapper, + }, + ); + await result.current.refetch({ pathParams: { id: "one" } }); + + await wait(() => + expect(result.current).toMatchObject({ + error: null, + loading: false, + }), + ); + expect(result.current.data).toEqual({ id: 1 }); + }); + + it("should override path parameters if specified in refetch method", async () => { + nock("https://my-awesome-api.fake") + .get("/plop/one") + .reply(200, { id: 1 }); + + const wrapper: React.FC = ({ children }) => ( + {children} + ); + const { result } = renderHook( + () => + useGet<{ id: number }, {}, {}, { id: string }>(({ id }) => `plop/${id}`, { + pathParams: { id: "two" }, + lazy: true, + }), + { + wrapper, + }, + ); + await result.current.refetch({ pathParams: { id: "one" } }); + + expect(result.current).toMatchObject({ + error: null, + loading: false, + }); + expect(result.current.data).toEqual({ id: 1 }); + }); + }); }); diff --git a/src/useGet.tsx b/src/useGet.tsx index fd22c608..91e33f1d 100644 --- a/src/useGet.tsx +++ b/src/useGet.tsx @@ -13,12 +13,16 @@ import { useAbort } from "./useAbort"; export type Omit = Pick>; -export interface UseGetProps { +export interface UseGetProps { /** * The path at which to request data, * typically composed by parent Gets or the RestfulProvider. */ - path: string; + path: string | ((pathParams: TPathParams) => string); + /** + * Path Parameters + */ + pathParams?: TPathParams; /** Options passed into the fetch call. */ requestOptions?: RestfulReactProviderProps["requestOptions"]; /** @@ -76,15 +80,22 @@ export function resolvePath( ); } -async function _fetchData( - props: UseGetProps, +async function _fetchData( + props: UseGetProps, state: GetState, setState: (newState: GetState) => void, context: RestfulReactProviderProps, abort: () => void, getAbortSignal: () => AbortSignal | undefined, ) { - const { base = context.base, path, resolve = (d: any) => d as TData, queryParams = {} } = props; + const { + base = context.base, + path, + resolve = (d: any) => d as TData, + queryParams = {}, + requestOptions, + pathParams = {}, + } = props; if (state.loading) { // Abort previous requests @@ -95,8 +106,9 @@ async function _fetchData( setState({ ...state, error: null, loading: true }); } - const requestOptions = - (typeof props.requestOptions === "function" ? await props.requestOptions() : props.requestOptions) || {}; + const pathStr = typeof path === "function" ? path(pathParams as TPathParams) : path; + + const propsRequestOptions = (typeof requestOptions === "function" ? await requestOptions() : requestOptions) || {}; const contextRequestOptions = (typeof context.requestOptions === "function" ? await context.requestOptions() : context.requestOptions) || {}; @@ -104,8 +116,8 @@ async function _fetchData( const signal = getAbortSignal(); const request = new Request( - resolvePath(base, path, { ...context.queryParams, ...queryParams }, props.queryParamStringifyOptions || {}), - merge({}, contextRequestOptions, requestOptions, { signal }), + resolvePath(base, pathStr, { ...context.queryParams, ...queryParams }, props.queryParamStringifyOptions || {}), + merge({}, contextRequestOptions, propsRequestOptions, { signal }), ); if (context.onRequest) context.onRequest(request); @@ -160,13 +172,15 @@ async function _fetchData( type FetchData = typeof _fetchData; type CancellableFetchData = FetchData | (FetchData & Cancelable); -type RefetchOptions = Partial, "lazy">>; +type RefetchOptions = Partial< + Omit, "lazy"> +>; const isCancellable = any>(func: T): func is T & Cancelable => { return typeof (func as any).cancel === "function" && typeof (func as any).flush === "function"; }; -export interface UseGetReturn extends GetState { +export interface UseGetReturn extends GetState { /** * Absolute path resolved from `base` and `path` (context & local) */ @@ -178,23 +192,24 @@ export interface UseGetReturn extends GetState /** * Refetch */ - refetch: (options?: RefetchOptions) => Promise; + refetch: (options?: RefetchOptions) => Promise; } -export function useGet( - path: string, - props?: Omit, "path">, +export function useGet( + path: UseGetProps["path"], + props?: Omit, "path">, ): UseGetReturn; -export function useGet( - props: UseGetProps, +export function useGet( + props: UseGetProps, ): UseGetReturn; -export function useGet() { - const props: UseGetProps = +export function useGet() { + const props: UseGetProps = typeof arguments[0] === "object" ? arguments[0] : { ...arguments[1], path: arguments[0] }; const context = useContext(Context); + const { path, pathParams = {} } = props; const fetchData = useCallback( typeof props.debounce === "object" @@ -219,6 +234,8 @@ export function useGet { if (!props.lazy) { fetchData(props, state, setState, context, abort, getAbortSignal); @@ -233,7 +250,7 @@ export function useGet = {}) => + refetch: (options: RefetchOptions = {}) => fetchData({ ...props, ...options }, state, setState, context, abort, getAbortSignal), }; } diff --git a/src/useMutate.test.tsx b/src/useMutate.test.tsx index 237db58e..7168070c 100644 --- a/src/useMutate.test.tsx +++ b/src/useMutate.test.tsx @@ -270,6 +270,60 @@ describe("useMutate", () => { }); }); + describe("Path Params", () => { + it("should resolve path parameters if specified", async () => { + nock("https://my-awesome-api.fake") + .post("/plop/one") + .reply(200, { id: 1 }); + + const wrapper: React.FC = ({ children }) => ( + {children} + ); + const { result } = renderHook( + () => + useMutate<{ id: number }, unknown, {}, {}, { id: string }>("POST", ({ id }) => `plop/${id}`, { + pathParams: { id: "one" }, + }), + { + wrapper, + }, + ); + const res = await result.current.mutate({}); + + expect(result.current).toMatchObject({ + error: null, + loading: false, + }); + expect(res).toEqual({ id: 1 }); + }); + + it("should override path parameters if specified in mutate method", async () => { + nock("https://my-awesome-api.fake") + .post("/plop/one") + .reply(200, { id: 1 }); + + const wrapper: React.FC = ({ children }) => ( + {children} + ); + const { result } = renderHook( + () => + useMutate<{ id: number }, unknown, {}, {}, { id: string }>("POST", ({ id }) => `plop/${id}`, { + pathParams: { id: "two" }, + }), + { + wrapper, + }, + ); + const res = await result.current.mutate({}, { pathParams: { id: "one" } }); + + expect(result.current).toMatchObject({ + error: null, + loading: false, + }); + expect(res).toEqual({ id: 1 }); + }); + }); + describe("POST", () => { it("should set loading to true after a call", async () => { nock("https://my-awesome-api.fake") @@ -593,7 +647,7 @@ describe("useMutate", () => { } type UseDeleteMyCustomEndpoint = Omit< - UseMutateProps, + UseMutateProps, "path" | "verb" >; const useDeleteMyCustomEndpoint = (props?: UseDeleteMyCustomEndpoint) => @@ -640,7 +694,7 @@ describe("useMutate", () => { } type UseDeleteMyCustomEndpoint = Omit< - UseMutateProps, + UseMutateProps, "path" | "verb" >; const useDeleteMyCustomEndpoint = (props?: UseDeleteMyCustomEndpoint) => diff --git a/src/useMutate.tsx b/src/useMutate.tsx index ccdbec9b..96d77d4c 100644 --- a/src/useMutate.tsx +++ b/src/useMutate.tsx @@ -6,8 +6,8 @@ import { Omit, resolvePath, UseGetProps } from "./useGet"; import { processResponse } from "./util/processResponse"; import { useAbort } from "./useAbort"; -export interface UseMutateProps - extends Omit, "lazy" | "debounce"> { +export interface UseMutateProps + extends Omit, "lazy" | "debounce"> { /** * What HTTP verb are we using? */ @@ -21,7 +21,8 @@ export interface UseMutateProps onMutate?: (body: TRequestBody, data: TData) => void; } -export interface UseMutateReturn extends MutateState { +export interface UseMutateReturn + extends MutateState { /** * Cancel the current fetch */ @@ -29,30 +30,43 @@ export interface UseMutateReturn exte /** * Call the mutate endpoint */ - mutate: MutateMethod; + mutate: MutateMethod; } -export function useMutate( - props: UseMutateProps, -): UseMutateReturn; +export function useMutate< + TData = any, + TError = any, + TQueryParams = { [key: string]: any }, + TRequestBody = any, + TPathParams = unknown +>( + props: UseMutateProps, +): UseMutateReturn; -export function useMutate( - verb: UseMutateProps["verb"], - path: string, - props?: Omit, "path" | "verb">, -): UseMutateReturn; +export function useMutate< + TData = any, + TError = any, + TQueryParams = { [key: string]: any }, + TRequestBody = any, + TPathParams = unknown +>( + verb: UseMutateProps["verb"], + path: UseMutateProps["path"], + props?: Omit, "path" | "verb">, +): UseMutateReturn; export function useMutate< TData = any, TError = any, TQueryParams = { [key: string]: any }, - TRequestBody = any ->(): UseMutateReturn { - const props: UseMutateProps = + TRequestBody = any, + TPathParams = unknown +>(): UseMutateReturn { + const props: UseMutateProps = typeof arguments[0] === "object" ? arguments[0] : { ...arguments[2], path: arguments[1], verb: arguments[0] }; const context = useContext(Context); - const { verb, base = context.base, path, queryParams = {}, resolve } = props; + const { verb, base = context.base, path, queryParams = {}, resolve, pathParams = {} } = props; const isDelete = verb === "DELETE"; const [state, setState] = useState>({ @@ -65,8 +79,8 @@ export function useMutate< // Cancel the fetch on unmount useEffect(() => () => abort(), [abort]); - const mutate = useCallback>( - async (body: TRequestBody, mutateRequestOptions?: MutateRequestOptions) => { + const mutate = useCallback>( + async (body: TRequestBody, mutateRequestOptions?: MutateRequestOptions) => { if (state.error || !state.loading) { setState(prevState => ({ ...prevState, loading: true, error: null })); } @@ -76,6 +90,9 @@ export function useMutate< abort(); } + const pathStr = + typeof path === "function" ? path(mutateRequestOptions?.pathParams || (pathParams as TPathParams)) : path; + const propsRequestOptions = (typeof props.requestOptions === "function" ? await props.requestOptions() : props.requestOptions) || {}; @@ -106,7 +123,7 @@ export function useMutate< const request = new Request( resolvePath( base, - isDelete ? `${path}/${body}` : path, + isDelete ? `${pathStr}/${body}` : pathStr, { ...context.queryParams, ...queryParams, ...mutateRequestOptions?.queryParams }, props.queryParamStringifyOptions, ),