Skip to content
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

docs: Add TypeScript typings to LmsApiService group methods #1426

Merged
merged 2 commits into from
Feb 19, 2025

Conversation

marlonkeating
Copy link
Contributor

@marlonkeating marlonkeating commented Feb 13, 2025

Jira Ticket

This change converts LmsApiService into a .ts file, and adds TypeScript typings to methods based around Enterprise Groups. This change is intended as a first step towards annotating all of our api contracts with TypeScript.

For all changes

  • Ensure adequate tests are in place (or reviewed existing tests cover changes)

Only if submitting a visual change

  • Ensure to attach screenshots
  • Ensure to have UX team confirm screenshots

Copy link

codecov bot commented Feb 13, 2025

Codecov Report

Attention: Patch coverage is 44.44444% with 10 lines in your changes missing coverage. Please review.

Project coverage is 86.43%. Comparing base (02cc629) to head (766a268).
Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/data/services/LmsApiService.ts 41.17% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1426      +/-   ##
==========================================
- Coverage   86.48%   86.43%   -0.06%     
==========================================
  Files         660      660              
  Lines       14936    14931       -5     
  Branches     3164     3162       -2     
==========================================
- Hits        12917    12905      -12     
- Misses       1952     1956       +4     
- Partials       67       70       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

src/types.d.ts Outdated
@@ -0,0 +1,33 @@
export type HttpResponse<Payload = {}> = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] It might be worth using AxiosResponse from the transient axios dependency via @edx/frontend-platform, e.g.:

import type { AxiosResponse } from 'axios';
export type EnterpriseGroupResponse = Promise<AxiosResponse<EnterpriseGroup>>;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I knew this had to be defined somewhere! Thanks for pointing it out.

src/types.d.ts Outdated

export type EnterpriseGroup = {
/* uuid of enterprise customer */
enterprise_customer: string,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[curious/consideration] Given we typically camelCase API response data properties in service functions, e.g.:

const response = await getAuthenticatedHttpClient().get(url);
return camelCaseObject(response.data);

Should the interface represent the API response (snake_case), or how we typically consume it within hooks/components/etc. (camelCase)? For instance, when the interface is defined with snake_case, there's a greater likelihood it could discourage the pattern of camelCasing API data to make working with the data in the frontend feel more like JavaScript (camelCase is the norm in JavaScript/TypeScript).

Worth noting that looking at the resulting types within getEnterpriseGroup on your current branch, the types are lost after the camelCaseObject (i.e., const enterpriseGroup = camelCaseObject(response.data); resolves to type any).

Would it make sense to move the camelCaseObject into the service function, and define the type with camelCase properties?

Copy link
Member

@adamstankiewicz adamstankiewicz Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[food for thought / aside] FWIW, while I don't think it's being used, frontend-platform technically supports MFEs using custom middleware with axios (ADR), e.g. integrating the http client with axios-case-converter(docs) within the MFE's initialize call:

initialize({
    messages: [appMessages],
    requireAuthenticatedUser: true,
    hydrateAuthenticatedUser: true,
    authMiddleware: [axiosCaseConverter],
});

See frontend-platform's implementation where the middleware function (axiosCaseConverter in example above) is called for the client here.

Maybe this might be an interesting topic to discuss in an upcoming Enterprise Solution Review? E.g., if we want to adopt the axios-case-converter auth middleware to be consistent about camelCasing API responses within our Enterprise MFEs 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I would agree that camelCase/snake_case conversions ideally happen in the api libraries, and so I've made that change to the group methods and adjusted their typings accordingly.

In regards to adopting an axios plugin that automatically converts snake_case to camelCase values, I would agree it's a more elegant solution, but don't know if there's a graceful way to integrate it into our code base at this point. The only way I can think to do it without modifying every point in the code that does camelCase conversion, is to carry around two axios client objects during the transition period: where one client does the automatic conversion and the legacy client does not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, adopting it gradually would be ideal, though not the worst to tackle all existing usages in one go (N = ~65 usages). Especially given doing so would probably be less overall effort than carrying around two axios clients.

That said, if the axios plugin was enabled without any changes, the camelCaseObject usages we have today would largely be camelCasing data that's already been camelCased, aka largely a no-op operation that doesn't necessarily break anything either (where usages could be removed incrementally, even while the axios plugin is enabled). The primary concern would be if we ever access snake_case properties prior to passing the response data to camelCaseObject, but I don't believe we do on a cursory search through usages.

@@ -3,6 +3,22 @@ import { camelCaseObject } from '@edx/frontend-platform/utils';

import { configuration } from '../../config';
import generateFormattedStatusUrl from './apiServiceUtils';
import { EnterpriseGroup, HttpResponse, Paginated } from '../../types';

export type CreateEnterpriseGroupArgs = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[curious] I'm interested in your thoughts on when to use type vs. interface for object type definitions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, types are for data specifications, while interfaces are for class/function contracts. In the case of a TypeScript project that is a web app front-end (as opposed to a library or back-end api), I don't see what I consider the ideal use cases for interfaces coming up all that often.

Also, I find the operations available to types more generally useful, especially given the heavily composition-oriented (rather than inheritance-oriented/OOP) paradigm coming from our Javascript codebase.

@marlonkeating marlonkeating force-pushed the mkeating/ENT-9742 branch 3 times, most recently from 5c960a5 to 3c9a427 Compare February 18, 2025 19:53
@@ -1,26 +1,8 @@
{
"extends": "@edx/typescript-config",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

src/types.d.ts Outdated
next: string?,
previous: string?,
count: number,
current_page: number,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: any reason not to camelCase the entire paginated response (i.e., currentPage) vs. only the results list?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None that I could see! Edited.

src/types.d.ts Outdated
next: string?,
previous: string?,
count: number,
current_page: number,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[consideration] fwiw, not all paginated responses will have current_page or start in their response. For instance, if an API endpoint doesn't utilize DefaultPagination from edx-drf-extensions.

Some APIs use the base PageNumberPagination from DRF (source):

{
    "count": 1023,
    "next": "https://api.example.org/accounts/?page=5",
    "previous": "https://api.example.org/accounts/?page=3",
    "results": [
       …
    ]
}

[suggestion] Is it worth creating both a base paginated response and a paginated response conforming to DefaultPagination (with current_page and start)?

  • Paginated (base)
  • PaginatedCurrentPage (or similar)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call-out! I was not aware.

package.json Outdated
@@ -38,6 +38,7 @@
"@edx/frontend-enterprise-utils": "9.1.0",
"@edx/frontend-platform": "8.1.5",
"@edx/openedx-atlas": "^0.6.0",
"@edx/typescript-config": "1.1.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should @edx/typescript-config be in devDependencies, since TypeScript things happen during build/dev, not at runtime (e.g., in learner portal)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I meant to put it there!


import { configuration } from '../../config';
import generateFormattedStatusUrl from './apiServiceUtils';
import { EnterpriseGroup, Paginated } from '../../types';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit/curious] When is it best to use the Types.* namespace vs. explicitly including imports for the types?

Types.EnterpriseGroup
Types.Paginated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started out using Types. instead of importing, but then saw that some of the deeply nested template types were losing readability. For instance:
export type EnterpriseGroupListResponse = Promise<Types.AxiosResponse<Types.Paginated<Types.EnterpriseGroup>>>;

src/types.d.ts Outdated

export type EnterpriseGroup = {
/* uuid of enterprise customer */
enterprise_customer: string,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, adopting it gradually would be ideal, though not the worst to tackle all existing usages in one go (N = ~65 usages). Especially given doing so would probably be less overall effort than carrying around two axios clients.

That said, if the axios plugin was enabled without any changes, the camelCaseObject usages we have today would largely be camelCasing data that's already been camelCased, aka largely a no-op operation that doesn't necessarily break anything either (where usages could be removed incrementally, even while the axios plugin is enabled). The primary concern would be if we ever access snake_case properties prior to passing the response data to camelCaseObject, but I don't believe we do on a cursory search through usages.

docs: use AxiosResponse type

refactor: LmsApiService group methods convert response to camelCase

build: use @edx/typescript-config compilerOptions

build: move typescript-config to devDependencies

docs: separate pagination types and camelCase fields

build: add trailing comma to tsconfig
@marlonkeating marlonkeating merged commit d5fef09 into master Feb 19, 2025
4 of 6 checks passed
@marlonkeating marlonkeating deleted the mkeating/ENT-9742 branch February 19, 2025 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants