Skip to content

Commit

Permalink
Merge pull request #815 from yeatmanlab/ref/318/query-composables-stu…
Browse files Browse the repository at this point in the history
…dent-report

Migrate Student Report to TanStack query composables
  • Loading branch information
maximilianoertel authored Sep 24, 2024
2 parents 9bcb262 + dd680ba commit a9942fe
Show file tree
Hide file tree
Showing 11 changed files with 290 additions and 80 deletions.
2 changes: 1 addition & 1 deletion src/components/views/UserInfoView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ onMounted(() => {
// +---------+
// | Queries |
// +---------+
const { data: userData } = useUserDataQuery({
const { data: userData } = useUserDataQuery(null, {
enabled: initialized,
});
Expand Down
29 changes: 29 additions & 0 deletions src/composables/queries/useUserAdministrationAssignmentsQuery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { toValue } from 'vue';
import { useQuery } from '@tanstack/vue-query';
import { computeQueryOverrides } from '@/helpers/computeQueryOverrides';
import { fetchDocById } from '@/helpers/query/utils';
import { USER_ADMINISTRATION_ASSIGNMENTS_QUERY_KEY } from '@/constants/queryKeys';
import { FIRESTORE_COLLECTIONS } from '@/constants/firebase';

/**
* User administration assignments query.
*
* @param {string} userId – The user ID to fetch assignments for.
* @param {string} administrationId – The administration ID to fetch assignments for.
* @param {QueryOptions|undefined} queryOptions – Optional TanStack query options.
* @returns {UseQueryResult} The TanStack query result.
*/
const useUserAdministrationAssignmentsQuery = (userId, administrationId, queryOptions = undefined) => {
const queryConditions = [() => !!toValue(userId), () => !!toValue(administrationId)];
const { isQueryEnabled, options } = computeQueryOverrides(queryConditions, queryOptions);

return useQuery({
queryKey: [USER_ADMINISTRATION_ASSIGNMENTS_QUERY_KEY, userId, administrationId],
queryFn: () =>
fetchDocById(FIRESTORE_COLLECTIONS.USERS, `${toValue(userId)}/assignments/${toValue(administrationId)}`),
enabled: isQueryEnabled,
...options,
});
};

export default useUserAdministrationAssignmentsQuery;
141 changes: 141 additions & 0 deletions src/composables/queries/useUserAdministrationAssignmentsQuery.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { ref, nextTick } from 'vue';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as VueQuery from '@tanstack/vue-query';
import { nanoid } from 'nanoid';
import { withSetup } from '@/test-support/withSetup.js';
import { fetchDocById } from '@/helpers/query/utils';
import useUserAdministrationAssignmentsQuery from './useUserAdministrationAssignmentsQuery';

vi.mock('@/helpers/query/utils', () => ({
fetchDocById: vi.fn().mockImplementation(() => []),
}));

vi.mock('@tanstack/vue-query', async (getModule) => {
const original = await getModule();
return {
...original,
useQuery: vi.fn().mockImplementation(original.useQuery),
};
});

describe('useUserAdministrationAssignmentsQuery', () => {
let queryClient;

beforeEach(() => {
queryClient = new VueQuery.QueryClient();
});

afterEach(() => {
queryClient?.clear();
});

it('should call query with correct parameters', () => {
const mockUserId = nanoid();
const mockAdministrationId = nanoid();

vi.spyOn(VueQuery, 'useQuery');

withSetup(() => useUserAdministrationAssignmentsQuery(mockUserId, mockAdministrationId), {
plugins: [[VueQuery.VueQueryPlugin, { queryClient }]],
});

expect(VueQuery.useQuery).toHaveBeenCalledWith({
queryKey: ['user-administration-assignments', mockUserId, mockAdministrationId],
queryFn: expect.any(Function),
enabled: expect.objectContaining({
_value: true,
}),
});

expect(fetchDocById).toHaveBeenCalledWith('users', `${mockUserId}/assignments/${mockAdministrationId}`);
});

it('should correctly control the enabled state of the query', async () => {
const mockUserId = nanoid();
const mockAdministrationId = nanoid();

const enableQuery = ref(false);

vi.spyOn(VueQuery, 'useQuery');

const queryOptions = {
enabled: enableQuery,
};

withSetup(() => useUserAdministrationAssignmentsQuery(mockUserId, mockAdministrationId, queryOptions), {
plugins: [[VueQuery.VueQueryPlugin, { queryClient }]],
});

expect(VueQuery.useQuery).toHaveBeenCalledWith({
queryKey: ['user-administration-assignments', mockUserId, mockAdministrationId],
queryFn: expect.any(Function),
enabled: expect.objectContaining({
_value: false,
__v_isRef: true,
}),
});

expect(fetchDocById).not.toHaveBeenCalled();

enableQuery.value = true;
await nextTick();

expect(fetchDocById).toHaveBeenCalledWith('users', `${mockUserId}/assignments/${mockAdministrationId}`);
});

it('should only fetch data if the params are set', async () => {
const mockUserId = ref(null);
const mockAdministrationId = ref(null);

const queryOptions = { enabled: true };

withSetup(() => useUserAdministrationAssignmentsQuery(mockUserId, mockAdministrationId, queryOptions), {
plugins: [[VueQuery.VueQueryPlugin, { queryClient }]],
});

expect(VueQuery.useQuery).toHaveBeenCalledWith({
queryKey: ['user-administration-assignments', mockUserId, mockAdministrationId],
queryFn: expect.any(Function),
enabled: expect.objectContaining({
_value: false,
__v_isRef: true,
}),
});

expect(fetchDocById).not.toHaveBeenCalled();

mockUserId.value = nanoid();

await nextTick();

expect(fetchDocById).not.toHaveBeenCalled();

mockAdministrationId.value = nanoid();

await nextTick();

expect(fetchDocById).toHaveBeenCalledWith('users', `${mockUserId.value}/assignments/${mockAdministrationId.value}`);
});

it('should not let queryOptions override the internally computed value', async () => {
const mockUserId = ref(null);
const mockAdministrationId = ref(nanoid());

const queryOptions = { enabled: true };

withSetup(() => useUserAdministrationAssignmentsQuery(mockUserId, mockAdministrationId, queryOptions), {
plugins: [[VueQuery.VueQueryPlugin, { queryClient }]],
});

expect(VueQuery.useQuery).toHaveBeenCalledWith({
queryKey: ['user-administration-assignments', mockUserId, mockAdministrationId],
queryFn: expect.any(Function),
enabled: expect.objectContaining({
_value: false,
__v_isRef: true,
}),
});

expect(fetchDocById).not.toHaveBeenCalled();
});
});
11 changes: 7 additions & 4 deletions src/composables/queries/useUserDataQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,26 @@ import { computeQueryOverrides } from '@/helpers/computeQueryOverrides';
import { fetchDocById } from '@/helpers/query/utils';
import { USER_DATA_QUERY_KEY } from '@/constants/queryKeys';
import { FIRESTORE_COLLECTIONS } from '@/constants/firebase';
import { computed } from 'vue';

/**
* User profile data query.
*
* @param {string|undefined|null} userId – The user ID to fetch, set to a falsy value to fetch the current user.
* @param {QueryOptions|undefined} queryOptions – Optional TanStack query options.
* @returns {UseQueryResult} The TanStack query result.
*/
const useUserDataQuery = (queryOptions = undefined) => {
const useUserDataQuery = (userId = undefined, queryOptions = undefined) => {
const authStore = useAuthStore();
const { roarUid, userQueryKeyIndex } = storeToRefs(authStore);

const queryConditions = [() => !!roarUid.value];
const uid = computed(() => userId || roarUid.value);
const queryConditions = [() => !!uid.value];
const { isQueryEnabled, options } = computeQueryOverrides(queryConditions, queryOptions);

return useQuery({
queryKey: [USER_DATA_QUERY_KEY, roarUid.value, userQueryKeyIndex.value],
queryFn: () => fetchDocById(FIRESTORE_COLLECTIONS.USERS, roarUid.value),
queryKey: [USER_DATA_QUERY_KEY, uid.value, userQueryKeyIndex.value],
queryFn: () => fetchDocById(FIRESTORE_COLLECTIONS.USERS, uid.value),
enabled: isQueryEnabled,
...options,
});
Expand Down
31 changes: 28 additions & 3 deletions src/composables/queries/useUserDataQuery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,31 @@ describe('useUserDataQuery', () => {
expect(fetchDocById).toHaveBeenCalledWith('users', mockUserId);
});

it('should allow the use of a manual user ID', async () => {
const mockUserId = nanoid();
const mockStudentUserId = nanoid();

const authStore = useAuthStore(piniaInstance);
authStore.roarUid = mockUserId;
authStore.userQueryKeyIndex = 1;

vi.spyOn(VueQuery, 'useQuery');

withSetup(() => useUserDataQuery(mockStudentUserId), {
plugins: [[VueQuery.VueQueryPlugin, { queryClient }]],
});

expect(VueQuery.useQuery).toHaveBeenCalledWith({
queryKey: ['user', mockStudentUserId, 1],
queryFn: expect.any(Function),
enabled: expect.objectContaining({
_value: true,
}),
});

expect(fetchDocById).toHaveBeenCalledWith('users', mockStudentUserId);
});

it('should correctly control the enabled state of the query', async () => {
const mockUserId = nanoid();

Expand All @@ -70,7 +95,7 @@ describe('useUserDataQuery', () => {
enabled: enableQuery,
};

withSetup(() => useUserDataQuery(queryOptions), {
withSetup(() => useUserDataQuery(null, queryOptions), {
plugins: [[VueQuery.VueQueryPlugin, { queryClient }]],
});

Expand Down Expand Up @@ -100,7 +125,7 @@ describe('useUserDataQuery', () => {

const queryOptions = { enabled: true };

withSetup(() => useUserDataQuery(queryOptions), {
withSetup(() => useUserDataQuery(null, queryOptions), {
plugins: [[VueQuery.VueQueryPlugin, { queryClient }]],
});

Expand Down Expand Up @@ -128,7 +153,7 @@ describe('useUserDataQuery', () => {

const queryOptions = { enabled: true };

withSetup(() => useUserDataQuery(queryOptions), {
withSetup(() => useUserDataQuery(null, queryOptions), {
plugins: [[VueQuery.VueQueryPlugin, { queryClient }]],
});

Expand Down
63 changes: 63 additions & 0 deletions src/composables/queries/useUserRunPageQuery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { computed, toValue } from 'vue';
import { useQuery } from '@tanstack/vue-query';
import _isEmpty from 'lodash/isEmpty';
import useUserAdministrationAssignmentsQuery from '@/composables/queries/useUserAdministrationAssignmentsQuery';
import { runPageFetcher } from '@/helpers/query/runs';
import { computeQueryOverrides } from '@/helpers/computeQueryOverrides';
import { USER_RUN_PAGE_QUERY_KEY } from '@/constants/queryKeys';

/**
* User run page query
*
* @TODO: Evaluate whether this query can be replaced using more generic query that already fetches user assessments and
* scores. This query was implemented as part of the transition to query composables but might be redudant if we
* refactor the underlying database query helpers to fetch all necessary data in a single query.
*
* @param {string|undefined|null} userId – The user ID to fetch, set to a falsy value to fetch the current user.
* @param {QueryOptions|undefined} queryOptions – Optional TanStack query options.
* @returns {UseQueryResult} The TanStack query result.
*/
const useUserRunPageQuery = (userId, administrationId, orgType, orgId, queryOptions = undefined) => {
const { data: assignmentData } = useUserAdministrationAssignmentsQuery(userId, administrationId, {
enabled: queryOptions?.enabled ?? true,
});

const optionalAssessments = computed(() => {
return assignmentData?.value?.assessments.filter((assessment) => assessment.optional);
});

const queryConditions = [
() => !!toValue(userId),
() => !!toValue(administrationId),
() => !!toValue(orgType),
() => !!toValue(orgId),
() => !_isEmpty(assignmentData.value),
];
const { isQueryEnabled, options } = computeQueryOverrides(queryConditions, queryOptions);

return useQuery({
queryKey: [USER_RUN_PAGE_QUERY_KEY, toValue(userId), toValue(administrationId), toValue(orgType), toValue(orgId)],
queryFn: async () => {
const runPageData = await runPageFetcher({
administrationId: administrationId,
orgType: orgType,
orgId: orgId,
userId: userId,
select: ['scores.computed', 'taskId', 'reliable', 'engagementFlags', 'optional'],
scoreKey: 'scores.computed',
paginate: false,
});

const data = runPageData?.map((task) => {
const isOptional = optionalAssessments?.value?.some((assessment) => assessment.taskId === task.taskId);
return isOptional ? { ...task, optional: true } : task;
});

return data;
},
enabled: isQueryEnabled,
...options,
});
};

export default useUserRunPageQuery;
2 changes: 2 additions & 0 deletions src/constants/queryKeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const SURVEY_RESPONSES_QUERY_KEY = 'survey-responses';
export const TASKS_QUERY_KEY = 'tasks';
export const TASK_VARIANTS_QUERY_KEY = 'task-variants';
export const USER_DATA_QUERY_KEY = 'user';
export const USER_ADMINISTRATION_ASSIGNMENTS_QUERY_KEY = 'user-administration-assignments';
export const USER_ASSIGNMENTS_QUERY_KEY = 'user-assignments';
export const USER_CLAIMS_QUERY_KEY = 'user-claims';
export const USER_STUDENT_DATA_QUERY_KEY = 'user-student';
export const USER_RUN_PAGE_QUERY_KEY = 'user-run-page';
2 changes: 1 addition & 1 deletion src/pages/HomeParticipant.vue
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const {
isLoading: isLoadingUserData,
isFetching: isFetchingUserData,
data: userData,
} = useUserDataQuery({
} = useUserDataQuery(null, {
enabled: initialized,
});
Expand Down
2 changes: 1 addition & 1 deletion src/pages/HomeSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ unsubscribe = authStore.$subscribe(async (mutation, state) => {
if (state.roarfirekit.restConfig) init();
});
const { isLoading: isLoadingUserData, data: userData } = useUserDataQuery({
const { isLoading: isLoadingUserData, data: userData } = useUserDataQuery(null, {
enabled: initialized,
});
Expand Down
Loading

0 comments on commit a9942fe

Please sign in to comment.