Skip to content

Query membership endpoints for resources belonging to user lists and learning paths #1846

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Nov 25, 2024
Merged
148 changes: 23 additions & 125 deletions frontends/api/src/hooks/learningPaths/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { LearningResource } from "../../generated/v1"
import * as factories from "../../test-utils/factories"
import { setupReactQueryTest } from "../test-utils"
import { setMockResponse, urls, makeRequest } from "../../test-utils"
import { useFeaturedLearningResourcesList } from "../learningResources"
import { invalidateResourceQueries } from "../learningResources/invalidation"
import keyFactory from "../learningResources/keyFactory"
import {
useLearningPathsDetail,
Expand All @@ -15,24 +13,11 @@ import {
useLearningPathCreate,
useLearningPathDestroy,
useLearningPathUpdate,
useLearningPathRelationshipMove,
useLearningPathRelationshipCreate,
useLearningPathRelationshipDestroy,
} from "./index"
import learningPathKeyFactory from "./keyFactory"

const factory = factories.learningResources

jest.mock("../learningResources/invalidation", () => {
const actual = jest.requireActual("../learningResources/invalidation")
return {
__esModule: true,
...actual,
invalidateResourceQueries: jest.fn(),
invalidateUserListQueries: jest.fn(),
}
})

/**
* Assert that `hook` queries the API with the correct `url`, `method`, and
* exposes the API's data.
Expand Down Expand Up @@ -172,6 +157,7 @@ describe("LearningPath CRUD", () => {

await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(makeRequest).toHaveBeenCalledWith("post", url, requestData)

expect(queryClient.invalidateQueries).toHaveBeenCalledWith([
"learningPaths",
"list",
Expand All @@ -184,13 +170,23 @@ describe("LearningPath CRUD", () => {
setMockResponse.delete(url, null)

const { wrapper, queryClient } = setupReactQueryTest()
jest.spyOn(queryClient, "invalidateQueries")

const { result } = renderHook(useLearningPathDestroy, {
wrapper,
})
result.current.mutate({ id: path.id })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(makeRequest).toHaveBeenCalledWith("delete", url, undefined)
expect(invalidateResourceQueries).toHaveBeenCalledWith(queryClient, path.id)

expect(queryClient.invalidateQueries).toHaveBeenCalledWith([
"learningPaths",
"list",
])
expect(queryClient.invalidateQueries).toHaveBeenCalledWith([
"learningPaths",
"membershipList",
])
})

test("useLearningPathUpdate calls correct API", async () => {
Expand All @@ -200,120 +196,22 @@ describe("LearningPath CRUD", () => {
setMockResponse.patch(url, path)

const { wrapper, queryClient } = setupReactQueryTest()
jest.spyOn(queryClient, "invalidateQueries")

const { result } = renderHook(useLearningPathUpdate, { wrapper })
result.current.mutate(patch)

await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(makeRequest).toHaveBeenCalledWith("patch", url, patch)

expect(invalidateResourceQueries).toHaveBeenCalledWith(queryClient, path.id)
})

test("useLearningPathRelationshipMove calls correct API", async () => {
const { relationship, pathUrls, keys } = makeData()
const url = pathUrls.relationshipDetails
setMockResponse.patch(url, null)

const { wrapper, queryClient } = setupReactQueryTest()
jest.spyOn(queryClient, "invalidateQueries")
const { result } = renderHook(useLearningPathRelationshipMove, { wrapper })
result.current.mutate(relationship)

await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(makeRequest).toHaveBeenCalledWith(
"patch",
url,
expect.objectContaining({ position: relationship.position }),
)

expect(queryClient.invalidateQueries).toHaveBeenCalledWith(
keys.relationshipListing,
)
expect(queryClient.invalidateQueries).toHaveBeenCalledWith([
"learningPaths",
"list",
])
expect(queryClient.invalidateQueries).toHaveBeenCalledWith([
"learningPaths",
"detail",
path.id,
])
})

test.each([{ isChildFeatured: false }, { isChildFeatured: true }])(
"useLearningPathRelationshipCreate calls correct API and patches featured resources",
async ({ isChildFeatured }) => {
const { relationship, pathUrls, resourceWithoutList } = makeData()

const featured = factory.resources({ count: 3 })
if (isChildFeatured) {
featured.results[0] = resourceWithoutList
}
setMockResponse.get(urls.learningResources.featured(), featured)

const url = pathUrls.relationshipList
const requestData = {
child: relationship.child,
parent: relationship.parent,
position: relationship.position,
}
setMockResponse.post(url, relationship)

const { wrapper, queryClient } = setupReactQueryTest()
const { result } = renderHook(useLearningPathRelationshipCreate, {
wrapper,
})
const { result: featuredResult } = renderHook(
useFeaturedLearningResourcesList,
{ wrapper },
)
await waitFor(() => expect(featuredResult.current.data).toBe(featured))

result.current.mutate(requestData)

await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(makeRequest).toHaveBeenCalledWith("post", url, requestData)

expect(invalidateResourceQueries).toHaveBeenCalledWith(
queryClient,
relationship.child,
{ skipFeatured: false },
)
expect(invalidateResourceQueries).toHaveBeenCalledWith(
queryClient,
relationship.parent,
)
},
)

test.each([{ isChildFeatured: false }, { isChildFeatured: true }])(
"useLearningPathRelationshipDestroy calls correct API and patches child resource cache (isChildFeatured=$isChildFeatured)",
async ({ isChildFeatured }) => {
const { relationship, pathUrls } = makeData()
const url = pathUrls.relationshipDetails

const featured = factory.resources({ count: 3 })
if (isChildFeatured) {
featured.results[0] = relationship.resource
}
setMockResponse.get(urls.learningResources.featured(), featured)

setMockResponse.delete(url, null)
const { wrapper, queryClient } = setupReactQueryTest()

const { result } = renderHook(useLearningPathRelationshipDestroy, {
wrapper,
})
const { result: featuredResult } = renderHook(
useFeaturedLearningResourcesList,
{ wrapper },
)

await waitFor(() => expect(featuredResult.current.data).toBe(featured))
result.current.mutate(relationship)
await waitFor(() => expect(result.current.isSuccess).toBe(true))

expect(makeRequest).toHaveBeenCalledWith("delete", url, undefined)
expect(invalidateResourceQueries).toHaveBeenCalledWith(
queryClient,
relationship.child,
{ skipFeatured: false },
)
expect(invalidateResourceQueries).toHaveBeenCalledWith(
queryClient,
relationship.parent,
)
},
)
})
86 changes: 24 additions & 62 deletions frontends/api/src/hooks/learningPaths/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@ import type {
LearningpathsApiLearningpathsItemsListRequest as ItemsListRequest,
LearningpathsApiLearningpathsCreateRequest as CreateRequest,
LearningpathsApiLearningpathsDestroyRequest as DestroyRequest,
LearningPathRelationshipRequest,
MicroLearningPathRelationship,
LearningPathResource,
} from "../../generated/v1"
import { learningPathsApi } from "../../clients"
import learningPaths from "./keyFactory"
import { invalidateResourceQueries } from "../learningResources/invalidation"
import { useUserIsAuthenticated } from "api/hooks/user"

const useLearningPathsList = (
params: ListRequest = {},
Expand Down Expand Up @@ -74,8 +72,9 @@ const useLearningPathUpdate = () => {
id: params.id,
PatchedLearningPathResourceRequest: params,
}),
onSettled: (_data, _err, vars) => {
invalidateResourceQueries(queryClient, vars.id)
onSettled: (data, err, vars) => {
queryClient.invalidateQueries(learningPaths.list._def)
queryClient.invalidateQueries(learningPaths.detail(vars.id).queryKey)
},
})
}
Expand All @@ -85,8 +84,9 @@ const useLearningPathDestroy = () => {
return useMutation({
mutationFn: (params: DestroyRequest) =>
learningPathsApi.learningpathsDestroy(params),
onSettled: (_data, _err, vars) => {
invalidateResourceQueries(queryClient, vars.id)
onSettled: () => {
queryClient.invalidateQueries(learningPaths.list._def)
queryClient.invalidateQueries(learningPaths.membershipList._def)
},
})
}
Expand All @@ -96,22 +96,6 @@ interface ListItemMoveRequest {
id: number
position?: number
}
const useLearningPathRelationshipMove = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ parent, id, position }: ListItemMoveRequest) =>
learningPathsApi.learningpathsItemsPartialUpdate({
learning_resource_id: parent,
id,
PatchedLearningPathRelationshipRequest: { position },
}),
onSettled: (_data, _err, vars) => {
queryClient.invalidateQueries(
learningPaths.detail(vars.parent)._ctx.infiniteItems._def,
)
},
})
}

const useLearningPathListItemMove = () => {
const queryClient = useQueryClient()
Expand All @@ -131,47 +115,26 @@ const useLearningPathListItemMove = () => {
})
}

const useLearningPathRelationshipCreate = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (params: LearningPathRelationshipRequest) =>
learningPathsApi.learningpathsItemsCreate({
learning_resource_id: params.parent,
LearningPathRelationshipRequest: params,
}),
onSettled: (_response, _err, vars) => {
invalidateResourceQueries(
queryClient,
vars.child,
// do NOT skip invalidating the /featured/ lists,
// Changing a learning path might change the members of the featured
// lists.
{ skipFeatured: false },
)
invalidateResourceQueries(queryClient, vars.parent)
const useIsLearningPathMember = (resourceId?: number) => {
return useQuery({
...learningPaths.membershipList(),
select: (data) => {
return !!data.find((relationship) => relationship.child === resourceId)
},
enabled: useUserIsAuthenticated() && !!resourceId,
})
}

const useLearningPathRelationshipDestroy = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (params: MicroLearningPathRelationship) =>
learningPathsApi.learningpathsItemsDestroy({
id: params.id,
learning_resource_id: params.parent,
}),
onSettled: (_response, _err, vars) => {
invalidateResourceQueries(
queryClient,
vars.child,
// do NOT skip invalidating the /featured/ lists,
// Changing a learning path might change the members of the featured
// lists.
{ skipFeatured: false },
)
invalidateResourceQueries(queryClient, vars.parent)
const useLearningPathMemberList = (resourceId?: number) => {
return useQuery({
...learningPaths.membershipList(),

select: (data) => {
return data
.filter((relationship) => relationship.child === resourceId)
.map((relationship) => relationship.parent.toString())
},
enabled: useUserIsAuthenticated() && !!resourceId,
})
}

Expand All @@ -182,8 +145,7 @@ export {
useLearningPathCreate,
useLearningPathUpdate,
useLearningPathDestroy,
useLearningPathRelationshipMove,
useLearningPathListItemMove,
useLearningPathRelationshipCreate,
useLearningPathRelationshipDestroy,
useIsLearningPathMember,
useLearningPathMemberList,
}
19 changes: 17 additions & 2 deletions frontends/api/src/hooks/learningPaths/keyFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
LearningpathsApiLearningpathsListRequest as ListRequest,
PaginatedLearningPathRelationshipList,
} from "../../generated/v1"
import { clearListMemberships } from "../learningResources/keyFactory"

const learningPaths = createQueryKeys("learningPaths", {
detail: (id: number) => ({
Expand All @@ -18,15 +19,22 @@ const learningPaths = createQueryKeys("learningPaths", {
contextQueries: {
infiniteItems: (itemsP: ItemsListRequest) => ({
queryKey: [itemsP],
queryFn: ({ pageParam }: { pageParam?: string } = {}) => {
queryFn: async ({ pageParam }: { pageParam?: string } = {}) => {
// Use generated API for first request, then use next parameter
const request = pageParam
? axiosInstance.request<PaginatedLearningPathRelationshipList>({
method: "get",
url: pageParam,
})
: learningPathsApi.learningpathsItemsList(itemsP)
return request.then((res) => res.data)
const { data } = await request
return {
...data,
results: data.results.map((relation) => ({
...relation,
resource: clearListMemberships(relation.resource),
})),
}
},
}),
},
Expand All @@ -37,6 +45,13 @@ const learningPaths = createQueryKeys("learningPaths", {
return learningPathsApi.learningpathsList(params).then((res) => res.data)
},
}),
membershipList: () => ({
queryKey: ["membershipList"],
queryFn: async () => {
const { data } = await learningPathsApi.learningpathsMembershipList()
return data
},
}),
})

export default learningPaths
10 changes: 0 additions & 10 deletions frontends/api/src/hooks/learningResources/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,6 @@ import { UseQueryResult } from "@tanstack/react-query"

const factory = factories.learningResources

jest.mock("./invalidation", () => {
const actual = jest.requireActual("./invalidation")
return {
__esModule: true,
...actual,
invalidateResourceQueries: jest.fn(),
invalidateUserListQueries: jest.fn(),
}
})

/**
* Assert that `hook` queries the API with the correct `url`, `method`, and
* exposes the API's data.
Expand Down
Loading