You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue was investigated and written by Claude Code, at the request of a maintainer.
Summary
joi is a runtime schema-validation dependency that is used in exactly one place in the codebase: src/course-home/courseware-search/map-search-response.js, to validate the courseware-search endpoint response before mapping it. It is the repository's only runtime validator (no zod/yup/superstruct/ajv), so it exists solely to support this one function. This issue proposes removing the dependency in favor of a TypeScript-native approach.
Findings
joi is a single-use dependency
Declared in package.json ("joi": "^17.11.0").
Imported once, at src/course-home/courseware-search/map-search-response.js:1 (const Joi = require('joi');).
endpointSchema declares each results[] item with top-level fields id / contentType / location / url / content, but the real payload nests all of that under result.data.*. In the fixture (test-data/mocked-response.json) a result item's keys are _index, _type, _id, data, score — so the fields the mapper actually reads (result.data.content.displayName, result.data.contentType, result.score, …) live under the data key, not at the item's top level.
Combined with:
every item field being optional (no .required()), and
.unknown(true) on the item object,
…the item-level schema never actually validates the content shape — those fields simply pass as unknown keys. The effective runtime guarantees are only:
took is a required number,
total is a required number,
maxScore is a number or null,
results is an array of objects.
The single "wrong format" test (mapSearchResponse({ foo: 'bar' }) throws) passes purely because required took/total are missing — none of the content-shape rules are exercised.
(Casing is not the issue: searchCourseContentFromAPI camelCases via camelCaseObject, so the top-level keys line up. The mismatch is the nesting level.)
Options
Three approaches seem reasonable; this issue intentionally does not pick one.
Option A — hand-written type guard (no new dependency)
Convert map-search-response.js to TypeScript, define SearchResponse interfaces, and add a small guard that checks took/total are numbers and results is an array, throwing NonRetryableError otherwise, returning a typed value. Removes joi and adds nothing. Matches the small amount of validation that is actually effective today.
Option B — swap joi → zod
Replace the schema with a TS-native validator whose types are inferred from the schema. Trades one runtime dependency for another; most compelling if the intent is genuinely thorough runtime validation (which would also mean fixing the nesting-level mismatch so item content is really validated).
Option C — type and cast, no runtime validation
Define the response interface and cast, dropping runtime validation entirely. Smallest change; removes joi; loses the throw-on-malformed-response guarantee (a downstream error would surface via React Query instead). One test would need to change.
Decisions to resolve
Which option (A / B / C).
Preserve the current loose behavior, or fix the dead item-level validation? A faithful port keeps validation to took/total/results; a "fix it properly" port validates result.data.* for real. These are different-sized changes.
Scope notes
The file currently uses CommonJS require(); converting it to TypeScript aligns it with the surrounding code.
Existing tests to keep green: src/course-home/courseware-search/map-search-response.test.js and the mapSearchResponse mock usage in src/course-home/courseware-search/data/apiHooks.test.tsx.
Note
This issue was investigated and written by Claude Code, at the request of a maintainer.
Summary
joiis a runtime schema-validation dependency that is used in exactly one place in the codebase:src/course-home/courseware-search/map-search-response.js, to validate the courseware-search endpoint response before mapping it. It is the repository's only runtime validator (nozod/yup/superstruct/ajv), so it exists solely to support this one function. This issue proposes removing the dependency in favor of a TypeScript-native approach.Findings
joiis a single-use dependencypackage.json("joi": "^17.11.0").src/course-home/courseware-search/map-search-response.js:1(const Joi = require('joi');).useCoursewareSearchResults(src/course-home/courseware-search/data/apiHooks.ts) →mapSearchResponse(data, keyword)→endpointSchema.validate(response).The schema validates far less than it appears to
endpointSchemadeclares eachresults[]item with top-level fieldsid / contentType / location / url / content, but the real payload nests all of that underresult.data.*. In the fixture (test-data/mocked-response.json) a result item's keys are_index, _type, _id, data, score— so the fields the mapper actually reads (result.data.content.displayName,result.data.contentType,result.score, …) live under thedatakey, not at the item's top level.Combined with:
.required()), and.unknown(true)on the item object,…the item-level schema never actually validates the content shape — those fields simply pass as unknown keys. The effective runtime guarantees are only:
tookis a required number,totalis a required number,maxScoreis a number ornull,resultsis an array of objects.The single "wrong format" test (
mapSearchResponse({ foo: 'bar' })throws) passes purely because requiredtook/totalare missing — none of the content-shape rules are exercised.(Casing is not the issue:
searchCourseContentFromAPIcamelCases viacamelCaseObject, so the top-level keys line up. The mismatch is the nesting level.)Options
Three approaches seem reasonable; this issue intentionally does not pick one.
Option A — hand-written type guard (no new dependency)
Convert
map-search-response.jsto TypeScript, defineSearchResponseinterfaces, and add a small guard that checkstook/totalare numbers andresultsis an array, throwingNonRetryableErrorotherwise, returning a typed value. Removesjoiand adds nothing. Matches the small amount of validation that is actually effective today.Option B — swap
joi→zodReplace the schema with a TS-native validator whose types are inferred from the schema. Trades one runtime dependency for another; most compelling if the intent is genuinely thorough runtime validation (which would also mean fixing the nesting-level mismatch so item content is really validated).
Option C — type and cast, no runtime validation
Define the response interface and cast, dropping runtime validation entirely. Smallest change; removes
joi; loses the throw-on-malformed-response guarantee (a downstream error would surface via React Query instead). One test would need to change.Decisions to resolve
took/total/results; a "fix it properly" port validatesresult.data.*for real. These are different-sized changes.Scope notes
require(); converting it to TypeScript aligns it with the surrounding code.src/course-home/courseware-search/map-search-response.test.jsand themapSearchResponsemock usage insrc/course-home/courseware-search/data/apiHooks.test.tsx.