Skip to content

Commit

Permalink
feat: add query key and hooks for BFF layer (#1223)
Browse files Browse the repository at this point in the history
* feat: add query key factory and hooks for BFF layer

* chore: Add tests

* chore: PR Feedback
  • Loading branch information
brobro10000 authored Nov 22, 2024
1 parent 7f8451d commit 1077c0a
Show file tree
Hide file tree
Showing 10 changed files with 476 additions and 9 deletions.
36 changes: 36 additions & 0 deletions src/components/app/data/hooks/useBFF.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useLocation, useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { resolveBFFQuery } from '../../routes/data/utils';

/**
* Uses the route to determine which API call to make for the BFF
* Populates the queryKey with the appropriate enterprise customer uuid once BFF call is resolved
* @param queryOptions
* @returns {Types.UseQueryResult}} The query results for the routes BFF.
*/
export function useBFF(queryOptions = {}) {
const { select, ...queryOptionsRest } = queryOptions;
const location = useLocation();
const params = useParams();
// Determine the BFF query to use based on the current location
const matchedBFFQuery = resolveBFFQuery(location.pathname);
return useQuery({
...matchedBFFQuery(params),
...queryOptionsRest,
select: (data) => {
if (!data) {
return data;
}

// TODO: Determine if returned data needs further transformations
const transformedData = structuredClone(data);
if (select) {
return select({
original: data,
transformed: transformedData,
});
}
return transformedData;
},
});
}
154 changes: 154 additions & 0 deletions src/components/app/data/hooks/useBFF.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { renderHook } from '@testing-library/react-hooks';
import { QueryClientProvider } from '@tanstack/react-query';
import { v4 as uuidv4 } from 'uuid';
import { useLocation, useParams } from 'react-router-dom';
import { enterpriseCustomerFactory } from '../services/data/__factories__';
import useEnterpriseCustomer from './useEnterpriseCustomer';
import { queryClient } from '../../../../utils/tests';
import { fetchEnterpriseLearnerDashboard } from '../services';
import { useBFF } from './useBFF';
import { resolveBFFQuery } from '../../routes/data/utils';
import { queryEnterpriseLearnerDashboardBFF } from '../queries';

jest.mock('./useEnterpriseCustomer');
jest.mock('../../routes/data/utils', () => ({
...jest.requireActual('../services'),
resolveBFFQuery: jest.fn(),
}));
jest.mock('../services', () => ({
...jest.requireActual('../services'),
fetchEnterpriseLearnerDashboard: jest.fn().mockResolvedValue(null),
}));
jest.mock('react-router-dom', () => ({
useLocation: jest.fn(),
matchPath: jest.fn(),
useParams: jest.fn(),
}));

const mockEnterpriseCustomer = enterpriseCustomerFactory();
const mockCustomerAgreementUuid = uuidv4();
const mockSubscriptionCatalogUuid = uuidv4();
const mockSubscriptionLicenseUuid = uuidv4();
const mockSubscriptionPlanUuid = uuidv4();
const mockActivationKey = uuidv4();
const mockBFFDashboardData = {
enterpriseCustomerUserSubsidies: {
subscriptions: {
customerAgreement: {
uuid: mockCustomerAgreementUuid,
availableSubscriptionCatalogs: [
mockSubscriptionCatalogUuid,
],
defaultEnterpriseCatalogUuid: null,
netDaysUntilExpiration: 328,
disableExpirationNotifications: false,
enableAutoAppliedSubscriptionsWithUniversalLink: true,
subscriptionForAutoAppliedLicenses: null,
},
subscriptionLicenses: [
{
uuid: mockSubscriptionLicenseUuid,
status: 'activated',
userEmail: '[email protected]',
activationDate: '2024-04-08T20:49:57.593412Z',
lastRemindDate: '2024-04-08T20:49:57.593412Z',
revokedDate: null,
activationKey: mockActivationKey,
subscriptionPlan: {
uuid: mockSubscriptionPlanUuid,
title: 'Another Subscription Plan',
enterpriseCatalogUuid: mockSubscriptionCatalogUuid,
isActive: true,
isCurrent: true,
startDate: '2024-01-18T15:09:41Z',
expirationDate: '2025-03-31T15:09:47Z',
daysUntilExpiration: 131,
daysUntilExpirationIncludingRenewals: 131,
shouldAutoApplyLicenses: false,
},
},
],
subscriptionLicensesByStatus: {
activated: [
{
uuid: mockSubscriptionLicenseUuid,
status: 'activated',
userEmail: '[email protected]',
activationDate: '2024-04-08T20:49:57.593412Z',
lastRemindDate: '2024-04-08T20:49:57.593412Z',
revokedDate: null,
activationKey: mockActivationKey,
subscriptionPlan: {
uuid: '6e5debf9-a407-4655-98c1-d510880f5fa6',
title: 'Another Subscription Plan',
enterpriseCatalogUuid: mockSubscriptionCatalogUuid,
isActive: true,
isCurrent: true,
startDate: '2024-01-18T15:09:41Z',
expirationDate: '2025-03-31T15:09:47Z',
daysUntilExpiration: 131,
daysUntilExpirationIncludingRenewals: 131,
shouldAutoApplyLicenses: false,
},
},
],
assigned: [],
expired: [],
revoked: [],
},
},
},
enterpriseCourseEnrollments: [
{
courseRunId: 'course-v1:edX+DemoX+3T2022',
courseKey: 'edX+DemoX',
courseType: 'executive-education-2u',
orgName: 'edX',
courseRunStatus: 'completed',
displayName: 'Really original course name',
emailsEnabled: true,
certificateDownloadUrl: null,
created: '2023-06-14T15:48:31.672317Z',
startDate: '2022-10-26T00:00:00Z',
endDate: '2022-12-04T23:59:59Z',
mode: 'unpaid-executive-education',
isEnrollmentActive: true,
productSource: '2u',
enrollBy: null,
pacing: 'instructor',
courseRunUrl: 'https://fake-url.com/account?org_id=n0tr3a1',
resumeCourseRunUrl: null,
isRevoked: false,
},
],
errors: [],
warnings: [],
};
describe('useBFF', () => {
const Wrapper = ({ children }) => (
<QueryClientProvider client={queryClient()}>
{children}
</QueryClientProvider>
);
beforeEach(() => {
jest.clearAllMocks();
useEnterpriseCustomer.mockReturnValue({ data: mockEnterpriseCustomer });
fetchEnterpriseLearnerDashboard.mockResolvedValue(mockBFFDashboardData);
useLocation.mockReturnValue({ pathname: '/test-enterprise' });
useParams.mockReturnValue({ enterpriseSlug: 'test-enterprise' });
resolveBFFQuery.mockReturnValue(null);
});
it('should handle resolved value correctly for the dashboard route', async () => {
resolveBFFQuery.mockReturnValue(queryEnterpriseLearnerDashboardBFF);
const { result, waitForNextUpdate } = renderHook(() => useBFF(), { wrapper: Wrapper });
await waitForNextUpdate();

expect(result.current).toEqual(
expect.objectContaining({
data: mockBFFDashboardData,
isLoading: false,
isFetching: false,
}),
);
});
});
9 changes: 9 additions & 0 deletions src/components/app/data/queries/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,12 @@ export function queryVideoDetail(videoUUID: string, enterpriseUUID: string) {
._ctx.video
._ctx.detail(videoUUID);
}

// BFF queries
export function queryEnterpriseLearnerDashboardBFF({ enterpriseSlug }) {
return queries
.bff
.enterpriseSlug(enterpriseSlug)
._ctx.route
._ctx.dashboard;
}
20 changes: 19 additions & 1 deletion src/components/app/data/queries/queryKeyFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
fetchEnterpriseCourseEnrollments,
fetchEnterpriseCuration,
fetchEnterpriseCustomerContainsContent,
fetchEnterpriseLearnerDashboard,
fetchEnterpriseLearnerData,
fetchEnterpriseOffers,
fetchInProgressPathways,
Expand Down Expand Up @@ -263,5 +264,22 @@ const content = createQueryKeys('content', {
}),
});

const queries = mergeQueryKeys(enterprise, user, content);
const bff = createQueryKeys('bff', {
enterpriseSlug: (enterpriseSlug) => ({
queryKey: [enterpriseSlug],
contextQueries: {
route: {
queryKey: null,
contextQueries: {
dashboard: ({
queryKey: null,
queryFn: ({ queryKey }) => fetchEnterpriseLearnerDashboard({ enterpriseSlug: queryKey[2] }),
}),
},
},
},
}),
});

const queries = mergeQueryKeys(enterprise, user, content, bff);
export default queries;
38 changes: 38 additions & 0 deletions src/components/app/data/services/bffs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getConfig } from '@edx/frontend-platform/config';
import { logError } from '@edx/frontend-platform/logging';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { camelCaseObject } from '@edx/frontend-platform/utils';

export const baseLearnerBFFResponse = {
enterpriseCustomerUserSubsidies: {
subscriptions: {
customerAgreement: {},
subscriptionLicenses: [],
subscriptionLicensesByStatus: {},
},
},
errors: [],
warnings: [],
};

export const learnerDashboardBFFResponse = {
...baseLearnerBFFResponse,
enterpriseCourseEnrollments: [],
};

export async function fetchEnterpriseLearnerDashboard(customerIdentifiers) {
const { ENTERPRISE_ACCESS_BASE_URL } = getConfig();
const url = `${ENTERPRISE_ACCESS_BASE_URL}/api/v1/bffs/learner/dashboard/`;
try {
const params = {
enterprise_customer_uuid: customerIdentifiers?.enterpriseId,
enterprise_customer_slug: customerIdentifiers?.enterpriseSlug,
};

const result = await getAuthenticatedHttpClient().post(url, params);
return camelCaseObject(result.data);
} catch (error) {
logError(error);
return learnerDashboardBFFResponse;
}
}
Loading

0 comments on commit 1077c0a

Please sign in to comment.