feat(canvas): add Canvas LMS - #487
Conversation
|
@Dhirenderchoudhary is attempting to deploy a commit to the corsair Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryCanvas LMS integration now includes:
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
Caller["Canvas endpoint caller"] --> Input["Validate operation input"]
Input --> BaseURL["Resolve plugin or account base URL"]
BaseURL --> HTTP["Canvas REST / GraphQL request"]
HTTP --> Output["Validate operation response"]
Output --> Result["Return typed result"]
Canvas["Canvas Live Event"] --> Match["Match event and tenant"]
Match --> Verify["Verify HMAC signature"]
Verify --> Trigger["Dispatch webhook trigger"]
Reviews (13): Last reviewed commit: "feat(canvas): add Canvas API response sc..." | Re-trigger Greptile |
Plugin PR scorecard —
|
| Check | Status | Notes |
|---|---|---|
| R1 — Scope: plugin files only | ✅ | |
| R2 — Tests with assertions | ✅ | |
| R3 — Description complete | ✅ | |
| R3 — Linked issue / claim | ✅ | |
| R4 — Demo video / recording | ✅ |
Rules: PLUGIN_PR_RULES.md · re-runs on every push
|
Hey @Dhirenderchoudhary, thanks for the contribution! 🏴☠️ Before a maintainer reviews, please fix the items below — the review re-runs automatically on your next push. Must fix
Rule Used: Flag boilerplate residue from the plugin generator... (source)
Rule Used: Verify the implementation matches the PR descripti... (source)
Rule Used: Every endpoint must validate inputs and outputs wi... (source)
Rule Used: Every endpoint must validate inputs and outputs wi... (source)
Rule Used: Every endpoint must validate inputs and outputs wi... (source)
Rule Used: Flag boilerplate residue from the plugin generator... (source)
Rule Used: Flag Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Rule Used: Flag boilerplate residue from the plugin generator... (source) Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Rule Used: Flag boilerplate residue from the plugin generator... (source) Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! PR requirements (rules)
If anything remains after your next push, a bot commit will clean it up; a maintainer always does the final review and merge. |
|
Remaining findings are being fixed by a bot commit — it will be re-reviewed automatically. |
Maintainer review neededAutomated rounds are exhausted. Remaining findings:
Knowledge Base Used: The provider-plugin package pattern |
📝 WalkthroughWalkthroughAdds a Canvas LMS provider with typed REST and GraphQL operations, authenticated requests, endpoint schemas, API-key and OAuth support, webhook verification and triggers, tenant resolution, packaging, tests, and provider registration. ChangesCanvas operation contracts
Canvas endpoint execution
Canvas plugin wiring and packaging
Canvas webhook support
Validation and provider registration
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Biome (2.5.6)packages/canvas/endpoints/types.tsComment |
5624ff9 to
b0f22ad
Compare
|
@greptile review |
|
@greptile review |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (9)
packages/corsair/core/constants.ts (1)
220-316: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd
'canvas'toAllProvidersfor autocomplete consistency.No exhaustive
AllProvidershandling exists, and(string & {})keeps the type open. The omission does not affect assignability or exhaustive switches.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/corsair/core/constants.ts` around lines 220 - 316, Update the AllProviders type to include the literal provider value 'canvas' alongside the existing provider names, preserving the open (string & {}) fallback and current type behavior.packages/canvas/index.ts (1)
578-579: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove the
canvasOperationsimport to the top of the file.The import sits between declarations at Line 579. ES module imports hoist, so the behavior is correct. The placement still breaks the convention used by the other imports in this file and can confuse readers and tooling.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/index.ts` around lines 578 - 579, Move the canvasOperations import from its current position between declarations to the file’s top import section, alongside the other module imports. Do not change how canvasOperations is used.packages/canvas/endpoints/operations.ts (2)
619-628: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the duplicate
deleteAMessageoperation.
deleteAMessagedeclares the same method and the same path asdeleteConversationMessages. Two operation names for one Canvas endpoint increase the public surface without adding capability.♻️ Proposed removal
deleteConversationMessages: { method: 'POST', path: '/api/v1/conversations/{conversation_id}/remove_messages', description: 'Delete specific messages from a Canvas conversation', }, - deleteAMessage: { - method: 'POST', - path: '/api/v1/conversations/{conversation_id}/remove_messages', - description: 'Delete messages from a Canvas conversation', - },Also remove
deleteAMessagefrom theConversationsgroup inpackages/canvas/endpoints/index.tsand fromcanvasEndpointsNested.conversationsinpackages/canvas/index.ts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/endpoints/operations.ts` around lines 619 - 628, Remove the duplicate deleteAMessage operation from the endpoint definitions, and remove its references from the Conversations group and canvasEndpointsNested.conversations. Retain deleteConversationMessages as the sole operation for this method and path.
63-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument that GraphQL operations require a caller-supplied
querydocument.Many entries map to
POST /api/graphqlwith the same method and path. The factory sends onlyinput.bodyto that path. ThereforegetAccountGraphQl,getAssignment2,createAssignmentGraphQl, andgetLegacyNodeare behaviorally identical passthroughs. The descriptions state that each operation retrieves or creates a specific resource. That is only true when the caller supplies the correct GraphQLqueryandvariablesin the body.Two options improve this:
- Add the GraphQL document to the operation metadata and merge it in the factory.
- Update the descriptions to state that the caller must supply
queryandvariables.Also applies to: 223-228, 244-248, 1210-1215
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/endpoints/operations.ts` around lines 63 - 68, Update the descriptions for getAccountGraphQl, getAssignment2, createAssignmentGraphQl, and getLegacyNode to state that callers must supply the appropriate GraphQL query document and variables in input.body. Preserve their existing method and path metadata; do not claim resource-specific behavior is built in unless the factory also supplies the GraphQL document.packages/canvas/endpoints/factory.ts (1)
12-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueType the
base_urlkey getter instead of castingctx.keys.
canvasAuthConfigdeclaresaccount: ['base_url']for both auth types inpackages/canvas/index.ts(Lines 105-112). The generated key context should therefore expose thebase_urlgetter. The cast to{ get_base_url?: () => Promise<string | null | undefined> }hides that contract. If the generated getter name changes, the cast keeps compiling and the fallback silently returnsundefined.Use the typed
KeyBuilderContextshape for Canvas here, or export a small typed helper frompackages/canvas/index.ts.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/endpoints/factory.ts` around lines 12 - 28, Update resolveCanvasBaseUrl to use the generated KeyBuilderContext type for ctx.keys, preserving the base_url getter contract declared by canvasAuthConfig instead of casting to an ad hoc getter shape. Reuse the existing Canvas typing from canvas/index.ts or export a focused typed helper there, and keep the current option/account fallback behavior unchanged.packages/canvas/jest.config.cjs (1)
5-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the copied patterns that do not apply to this package.
collectCoverageFromexcludesjest.config.ts, but this file isjest.config.cjs, so the exclusion never applies. The**/plugins/**and**/setup/**entries intestMatchtarget directories that this package does not contain. The**/*.test.tspattern already coversschema.test.ts.♻️ Proposed cleanup
testMatch: [ '**/*.test.ts', '**/tests/**/*.test.ts', - '**/plugins/**/*.test.ts', - '**/setup/**/*.test.ts', ], collectCoverageFrom: [ '**/*.ts', '!**/*.d.ts', '!**/node_modules/**', '!**/dist/**', - '!jest.config.ts', + '!tsup.config.ts', '!tests/**', ],🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/jest.config.cjs` around lines 5 - 18, Clean up the Jest configuration by removing the redundant `**/plugins/**/*.test.ts` and `**/setup/**/*.test.ts` entries from `testMatch`, and remove the ineffective `!jest.config.ts` exclusion from `collectCoverageFrom` because the config is CJS. Keep the broad `**/*.test.ts` matching and all applicable coverage exclusions unchanged.packages/canvas/endpoints/types.ts (1)
35-56: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winUnresolved path placeholders reach the Canvas API. No layer enforces that a value exists for each
{placeholder}in an operation path: the generated input schema keepspathParamsfully optional, andresolvePathsubstitutes silently. A call such asgetSingleCourse({})therefore sends a request whose URL still contains the literal placeholder, and Canvas answers with a confusing error instead of a local validation failure.
packages/canvas/endpoints/types.ts#L35-L56: extract the placeholder names fromoperation.pathand require each one as a non-empty string in the generated input schema.packages/canvas/client.ts#L37-L47: replace all placeholders with a global regex and throw when a value is missing, so an unresolved placeholder cannot reach the request URL.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/endpoints/types.ts` around lines 35 - 56, The generated schema in createRequestInputSchema must extract every placeholder from operation.path and require each corresponding pathParams value to be a non-empty string, while keeping unrelated parameters optional. In packages/canvas/endpoints/types.ts lines 35-56, add this per-operation validation. In packages/canvas/client.ts lines 37-47, update resolvePath to replace all occurrences using a global placeholder match and throw when any required value is missing, preventing unresolved placeholders from reaching the request URL.packages/canvas/tsconfig.json (1)
17-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExclude non-library files from the declaration project.
include: ["./**/*"]includesschema.test.tsandtsup.config.ts; exclude these files to prevent declaration output for them. Keepreferences: []; workspace package resolution handles thecorsairimports.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/tsconfig.json` around lines 17 - 19, Update the declaration project configuration in packages/canvas/tsconfig.json to exclude schema.test.ts and tsup.config.ts alongside dist and node_modules, while preserving the existing include pattern and references: [] setting.packages/canvas/endpoints/index.ts (1)
4-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd an exhaustiveness check for endpoint groups.
defineGrouppermits partial registration. Add a type-level or test-level assertion that everyCanvasOperationNameappears in exactly one endpoint group.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/endpoints/index.ts` around lines 4 - 14, Add an exhaustiveness assertion alongside defineGroup and the endpoint-group declarations so every CanvasOperationName is registered exactly once: reject missing names and duplicate registrations at the type or test level. Keep defineGroup’s generated endpoint mapping behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/canvas/client.ts`:
- Around line 12-22: Update normalizeCanvasBaseUrl to parse the trimmed URL with
new URL(), require the protocol to be exactly https:, and reject malformed or
non-HTTPS values before returning the normalized base URL. Preserve
trailing-slash removal and the existing required-value validation, using the
parsed URL result to ensure the base URL includes a valid host.
- Around line 65-86: Update the request preparation in the Canvas client around
the config and requestOptions objects to transform array-valued query parameters
into Canvas bracketed keys such as include[] before passing them to the shared
executor. Preserve the existing bearer-token configuration and ensure non-array
query parameters remain unchanged.
In `@packages/canvas/endpoints/factory.ts`:
- Around line 54-58: Update the completion logging around logEventFromContext in
the endpoint handler to avoid passing the full input object, which may contain
personal or access-token data. Construct and pass only non-sensitive metadata,
or redact input.body, pathParams, and query fields known to contain sensitive
values, while preserving the parsed response and completed-event behavior.
In `@packages/canvas/endpoints/types.ts`:
- Around line 65-75: Widen canvasResponseSchema in CanvasEndpointOutputSchemas
to accept successful responses with undefined, empty-string, and other string
bodies in addition to JSON objects and arrays. Preserve the existing object and
array validation so factory.ts parsing continues to validate structured
responses without turning 204 DELETE results or getRubricsUploadTemplate into
errors.
In `@packages/canvas/error-handlers.ts`:
- Around line 5-18: Restrict the fallback matching in RATE_LIMIT_ERROR.match so
“429” is recognized only when the error message clearly indicates rate limiting,
while preserving the existing ApiError status check and rate_limited matching.
Remove the broad substring behavior that treats unrelated identifiers containing
429 as retryable.
In `@packages/canvas/index.ts`:
- Around line 630-644: Update the OAuth configuration created by canvas() so
authUrl and tokenUrl use each tenant’s account base_url, matching
resolveCanvasBaseUrl in the endpoint factory, instead of resolving
options.baseUrl only once. If OAuth URLs cannot be resolved per tenant by the
framework, require options.baseUrl for oauth_2 and reject configuration without
it rather than falling back to the public Canvas host.
- Around line 581-601: Update CanvasOperation in operations.ts to support an
optional riskLevel, then modify buildEndpointMeta to prefer an operation’s
explicit riskLevel over HTTP-method inference. Mark deleteDiscussionEntry,
deleteDiscussionTopicGraphQl, deleteSubmissionDraft, and deleteOutcomeLinks as
destructive so PluginPermissionsConfig enforces the correct permission.
In `@packages/canvas/webhooks/types.ts`:
- Around line 91-99: Update verifyCanvasWebhookSignature to normalize
request.headers['x-canvas-signature'] by selecting the first element when it is
an array, while preserving the existing string and missing-header behavior. Use
the normalized string for subsequent signature verification instead of directly
casting the header value.
---
Nitpick comments:
In `@packages/canvas/endpoints/factory.ts`:
- Around line 12-28: Update resolveCanvasBaseUrl to use the generated
KeyBuilderContext type for ctx.keys, preserving the base_url getter contract
declared by canvasAuthConfig instead of casting to an ad hoc getter shape. Reuse
the existing Canvas typing from canvas/index.ts or export a focused typed helper
there, and keep the current option/account fallback behavior unchanged.
In `@packages/canvas/endpoints/index.ts`:
- Around line 4-14: Add an exhaustiveness assertion alongside defineGroup and
the endpoint-group declarations so every CanvasOperationName is registered
exactly once: reject missing names and duplicate registrations at the type or
test level. Keep defineGroup’s generated endpoint mapping behavior unchanged.
In `@packages/canvas/endpoints/operations.ts`:
- Around line 619-628: Remove the duplicate deleteAMessage operation from the
endpoint definitions, and remove its references from the Conversations group and
canvasEndpointsNested.conversations. Retain deleteConversationMessages as the
sole operation for this method and path.
- Around line 63-68: Update the descriptions for getAccountGraphQl,
getAssignment2, createAssignmentGraphQl, and getLegacyNode to state that callers
must supply the appropriate GraphQL query document and variables in input.body.
Preserve their existing method and path metadata; do not claim resource-specific
behavior is built in unless the factory also supplies the GraphQL document.
In `@packages/canvas/endpoints/types.ts`:
- Around line 35-56: The generated schema in createRequestInputSchema must
extract every placeholder from operation.path and require each corresponding
pathParams value to be a non-empty string, while keeping unrelated parameters
optional. In packages/canvas/endpoints/types.ts lines 35-56, add this
per-operation validation. In packages/canvas/client.ts lines 37-47, update
resolvePath to replace all occurrences using a global placeholder match and
throw when any required value is missing, preventing unresolved placeholders
from reaching the request URL.
In `@packages/canvas/index.ts`:
- Around line 578-579: Move the canvasOperations import from its current
position between declarations to the file’s top import section, alongside the
other module imports. Do not change how canvasOperations is used.
In `@packages/canvas/jest.config.cjs`:
- Around line 5-18: Clean up the Jest configuration by removing the redundant
`**/plugins/**/*.test.ts` and `**/setup/**/*.test.ts` entries from `testMatch`,
and remove the ineffective `!jest.config.ts` exclusion from
`collectCoverageFrom` because the config is CJS. Keep the broad `**/*.test.ts`
matching and all applicable coverage exclusions unchanged.
In `@packages/canvas/tsconfig.json`:
- Around line 17-19: Update the declaration project configuration in
packages/canvas/tsconfig.json to exclude schema.test.ts and tsup.config.ts
alongside dist and node_modules, while preserving the existing include pattern
and references: [] setting.
In `@packages/corsair/core/constants.ts`:
- Around line 220-316: Update the AllProviders type to include the literal
provider value 'canvas' alongside the existing provider names, preserving the
open (string & {}) fallback and current type behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a6d72a4b-7ce9-4b84-ad90-8ffbe5279db5
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (19)
packages/canvas/client.tspackages/canvas/endpoints/factory.tspackages/canvas/endpoints/index.tspackages/canvas/endpoints/operations.tspackages/canvas/endpoints/types.tspackages/canvas/error-handlers.tspackages/canvas/index.tspackages/canvas/jest.config.cjspackages/canvas/package.jsonpackages/canvas/schema.test.tspackages/canvas/schema/index.tspackages/canvas/tsconfig.jsonpackages/canvas/tsup.config.tspackages/canvas/webhooks/index.tspackages/canvas/webhooks/oauth-tenant-link.tspackages/canvas/webhooks/tenant-matcher.tspackages/canvas/webhooks/triggers.tspackages/canvas/webhooks/types.tspackages/corsair/core/constants.ts
|
@greptile review |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/canvas/endpoints/operations.ts (2)
207-210: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winUse
user_idin the last-attended route.Canvas defines this endpoint as
/api/v1/courses/{course_id}/users/{user_id}/last_attended, not an enrollment-scoped route.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/endpoints/operations.ts` around lines 207 - 210, Update the addLastAttendedDate operation’s path to use the Canvas user-scoped route with the {user_id} placeholder instead of the enrollment-scoped {enrollment_id} route, while preserving the existing method and operation metadata.
492-495: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winMark both no-body POST operations as
bodyless.
duplicateGroupDiscussionTopicneeds no body.assignUnassignedMembersToGroupCategoryonly has optionalsyncquery input. Withoutbodyless: true, valid path-only calls fail local validation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/endpoints/operations.ts` around lines 492 - 495, Mark both duplicateGroupDiscussionTopic and assignUnassignedMembersToGroupCategory operation definitions as bodyless: true, while preserving the existing method, paths, and optional sync query input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/canvas/webhooks/oauth-tenant-link.ts`:
- Around line 59-64: Replace the accounts[0] fallback in the OAuth tenant-link
resolution with an explicit account identifier from the OAuth contract; when no
identifier exists or multiple accounts remain ambiguous, return null instead of
selecting an arbitrary account. Update the surrounding account-resolution flow
and add coverage for reordered multi-account responses and paginated results.
---
Outside diff comments:
In `@packages/canvas/endpoints/operations.ts`:
- Around line 207-210: Update the addLastAttendedDate operation’s path to use
the Canvas user-scoped route with the {user_id} placeholder instead of the
enrollment-scoped {enrollment_id} route, while preserving the existing method
and operation metadata.
- Around line 492-495: Mark both duplicateGroupDiscussionTopic and
assignUnassignedMembersToGroupCategory operation definitions as bodyless: true,
while preserving the existing method, paths, and optional sync query input.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 5be6ea9b-b88e-4427-89c4-2c986f7af8a6
📒 Files selected for processing (6)
packages/canvas/api.test.tspackages/canvas/client.tspackages/canvas/endpoints/operations.tspackages/canvas/endpoints/types.tspackages/canvas/schema.test.tspackages/canvas/webhooks/oauth-tenant-link.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/canvas/client.ts
- packages/canvas/schema.test.ts
|
@greptile review |
|
@greptile review |
|
@greptile review |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
packages/canvas/endpoints/routes.ts (2)
53-59: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueUse a Map for route lookup.
canvasRoutesholds over 200 entries.getCanvasRouteperforms a linear scan on every call. Build aMap<CanvasOperationName, CanvasRoute>once at module load and read from it.♻️ Proposed refactor
+const canvasRouteByKey = new Map<CanvasOperationName, CanvasRoute>( + canvasRoutes.map((entry) => [entry.key, entry]), +); + export function getCanvasRoute(key: CanvasOperationName): CanvasRoute { - const route = canvasRoutes.find((entry) => entry.key === key); + const route = canvasRouteByKey.get(key); if (!route) { throw new Error(`[canvas] Unknown operation: ${key}`); } return route; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/endpoints/routes.ts` around lines 53 - 59, Replace the linear canvasRoutes.find lookup in getCanvasRoute with a module-level Map<CanvasOperationName, CanvasRoute> initialized once from canvasRoutes. Retrieve routes by key from the Map, preserve the existing unknown-operation error and return behavior, and avoid rebuilding the Map per call.
24-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
pathParamsOfduplicatespathParamNamesinpackages/canvas/endpoints/types.ts.Both functions extract
{param}placeholders with the same regular expression. Export one helper and import it in the other module. This keeps the path-parameter contract in a single place.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/endpoints/routes.ts` around lines 24 - 26, Remove the duplicate placeholder extraction from pathParamsOf and reuse the exported pathParamNames helper from the endpoints types module instead. Update the relevant import/export declarations so both call sites share the single implementation and preserve the existing readonly string-array behavior.packages/canvas/endpoints/response-schemas.ts (2)
449-468: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDerive
expectsListResponsefrom the schema instead of repeating the branches.
expectsListResponseduplicates every branch ofcreateResponseSchema. If one function changes, the two can disagree, andpackages/canvas/api.test.tsbuilds mock responses fromexpectsListResponse. A test would then pass while the runtime schema rejects the real response.Compute the answer from the schema that
createResponseSchemareturns.♻️ Proposed refactor
export function expectsListResponse( name: CanvasOperationName, operation: CanvasOperation = canvasOperations[name], ): boolean { - if (operation.path === '/api/graphql') return false; - if (operation.method === 'DELETE') return false; - if (operation.path.includes('/upload')) return false; - const resource = resourceSchemaFor(name, operation); - if ( - resource === CanvasPermissionsSchema || - resource === CanvasUnreadCountSchema || - resource === CanvasQuotaSchema || - resource === CanvasSubmissionSummarySchema || - resource === CanvasJsonObjectSchema || - resource === CanvasJsonArraySchema - ) { - return resource === CanvasJsonArraySchema; - } - return isListOperation(name, operation); + return createResponseSchema(name, operation) instanceof z.ZodArray; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/endpoints/response-schemas.ts` around lines 449 - 468, Refactor expectsListResponse to derive its result from the response schema produced by createResponseSchema, rather than duplicating path, method, and resource-specific branches. Reuse the existing schema-generation symbols and determine whether the returned schema represents a list, keeping the boolean consistent with runtime validation.
13-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReplace
.passthrough()withz.looseObject()across this file. Zod 4 deprecates the method, andz.looseObject()preserves unknown keys.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/endpoints/response-schemas.ts` around lines 13 - 17, Replace the deprecated .passthrough() usage on CanvasEntitySchema and any other schemas in this file with z.looseObject(), preserving each schema’s existing fields and unknown-key behavior.packages/canvas/endpoints/operations.ts (1)
68-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider marking read-only GraphQL operations as
read.
riskForinpackages/canvas/endpoints/routes.tsmaps any POST towrite. All GraphQL operations use POST, so query-only operations such asgetAccountGraphQl,getLegacyNode,getAuditLogs,getInternalSettings,getModuleItem, andgetAssignmentGroupare reported aswriterisk. The newriskLevelfield can correct this metadata for query-only GraphQL operations.Note that the GraphQL document is supplied by the caller in
input.body, so areadlabel is only accurate if callers are constrained to queries. Choose the label that matches the intended guarantee.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/canvas/endpoints/operations.ts` around lines 68 - 73, Update the GraphQL endpoint metadata for getAccountGraphQl and the other query-only operations (getLegacyNode, getAuditLogs, getInternalSettings, getModuleItem, and getAssignmentGroup) to use the riskLevel field with a read value only if callers are constrained to query documents; otherwise retain write to reflect the caller-supplied GraphQL operation. Ensure each label matches the intended guarantee and riskFor consumes this metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/canvas/endpoints/response-schemas.ts`:
- Around line 415-446: Update createResponseSchema to return operation-specific
wrapper schemas for getQuizStatistics, getOutcomeResults,
createBatchOverridesInACourse, and polling create operations returning polls,
poll_choices, poll_sessions, or poll_submissions. Define or reuse strict Zod
schemas matching each documented response shape, including the
assignment-overrides array, and select them by operation name before generic
resource/list handling; do not use a permissive fallback.
In `@packages/canvas/endpoints/types.ts`:
- Around line 62-67: Update CanvasEndpointInputs to derive body optionality from
each operation’s mutation and bodyless status, matching
createRequestInputSchema: require a non-empty body for mutations that are not
bodyless, while preserving optional body for bodyless or non-mutation
operations. Ensure the existing pathParams mapping remains unchanged.
In `@packages/canvas/webhooks/oauth-tenant-link.ts`:
- Around line 44-46: Update the singleton-row branch in the tenant-link
resolution function so it does not use the child row’s id as the tenant identity
when parent_account_id is set. Prefer a valid root_account_id from the row or
related account data, and return null when no valid root identity is available;
preserve the existing behavior for root accounts.
In `@packages/canvas/webhooks/tenant-matcher.ts`:
- Around line 5-8: The numericAccountId function in
packages/canvas/webhooks/tenant-matcher.ts:5-8 must iterate through all values
and return the first normalized string matching /^\d+$/, rather than validating
only firstString(values). In
packages/canvas/webhooks/oauth-tenant-link.ts:74-76, scan canvas_account_id,
account_id, and root_account_id for the first normalized numeric value before
checking UUID fields.
---
Nitpick comments:
In `@packages/canvas/endpoints/operations.ts`:
- Around line 68-73: Update the GraphQL endpoint metadata for getAccountGraphQl
and the other query-only operations (getLegacyNode, getAuditLogs,
getInternalSettings, getModuleItem, and getAssignmentGroup) to use the riskLevel
field with a read value only if callers are constrained to query documents;
otherwise retain write to reflect the caller-supplied GraphQL operation. Ensure
each label matches the intended guarantee and riskFor consumes this metadata.
In `@packages/canvas/endpoints/response-schemas.ts`:
- Around line 449-468: Refactor expectsListResponse to derive its result from
the response schema produced by createResponseSchema, rather than duplicating
path, method, and resource-specific branches. Reuse the existing
schema-generation symbols and determine whether the returned schema represents a
list, keeping the boolean consistent with runtime validation.
- Around line 13-17: Replace the deprecated .passthrough() usage on
CanvasEntitySchema and any other schemas in this file with z.looseObject(),
preserving each schema’s existing fields and unknown-key behavior.
In `@packages/canvas/endpoints/routes.ts`:
- Around line 53-59: Replace the linear canvasRoutes.find lookup in
getCanvasRoute with a module-level Map<CanvasOperationName, CanvasRoute>
initialized once from canvasRoutes. Retrieve routes by key from the Map,
preserve the existing unknown-operation error and return behavior, and avoid
rebuilding the Map per call.
- Around line 24-26: Remove the duplicate placeholder extraction from
pathParamsOf and reuse the exported pathParamNames helper from the endpoints
types module instead. Update the relevant import/export declarations so both
call sites share the single implementation and preserve the existing readonly
string-array behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ef056dd4-115d-4e3a-adf9-d4531366220e
📒 Files selected for processing (17)
packages/canvas/api.test.tspackages/canvas/client.tspackages/canvas/endpoints/factory.tspackages/canvas/endpoints/index.tspackages/canvas/endpoints/operations.tspackages/canvas/endpoints/response-schemas.tspackages/canvas/endpoints/routes.tspackages/canvas/endpoints/types.tspackages/canvas/error-handlers.tspackages/canvas/index.tspackages/canvas/jest.config.cjspackages/canvas/schema.test.tspackages/canvas/tsconfig.jsonpackages/canvas/webhooks/oauth-tenant-link.tspackages/canvas/webhooks/tenant-matcher.tspackages/canvas/webhooks/types.tspackages/corsair/core/constants.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- packages/canvas/tsconfig.json
- packages/canvas/endpoints/factory.ts
- packages/canvas/error-handlers.ts
- packages/canvas/webhooks/types.ts
- packages/canvas/client.ts
- packages/canvas/schema.test.ts
| export function createResponseSchema( | ||
| name: CanvasOperationName, | ||
| operation: CanvasOperation, | ||
| ): z.ZodTypeAny { | ||
| if (operation.path === '/api/graphql') { | ||
| return CanvasGraphqlResponseSchema; | ||
| } | ||
| if (operation.method === 'DELETE') { | ||
| return CanvasDeleteResponseSchema; | ||
| } | ||
| if (operation.path.includes('/upload')) { | ||
| return CanvasTextResponseSchema; | ||
| } | ||
|
|
||
| const resource = resourceSchemaFor(name, operation); | ||
| if ( | ||
| resource === CanvasPermissionsSchema || | ||
| resource === CanvasUnreadCountSchema || | ||
| resource === CanvasQuotaSchema || | ||
| resource === CanvasSubmissionSummarySchema || | ||
| resource === CanvasJsonObjectSchema || | ||
| resource === CanvasJsonArraySchema | ||
| ) { | ||
| return resource; | ||
| } | ||
|
|
||
| if (isListOperation(name, operation)) { | ||
| return z.array(resource); | ||
| } | ||
|
|
||
| return resource; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🌐 Web query:
Canvas LMS API GET quizzes statistics response quiz_statistics wrapper object
💡 Result:
In the Canvas LMS REST API, the GET /api/v1/courses/:course_id/quizzes/:quiz_id/statistics endpoint returns a response that uses the quiz_statistics key as the root wrapper object [1][2][3]. The API returns this object as an array containing the quiz statistics [1][2][3]. The structure of the response is as follows: { "quiz_statistics": [ { "id": 1, "quiz_id": 2, "multiple_attempts_exist": true, "includes_all_versions": true, "generated_at": "2013-01-23T23:59:00-07:00", "url": "http://canvas.example.edu/api/v1/courses/1/quizzes/2/statistics", "html_url": "http://canvas.example.edu/courses/1/quizzes/2/statistics", "question_statistics": null, "submission_statistics": null, "links": null } ] } According to the official Canvas LMS API documentation, the QuizStatistics object itself contains various fields, such as generated_at, multiple_attempts_exist, and URLs for both API and HTML access [1][3]. Note that certain fields, such as quiz_id, are specifically noted as being available only in non-JSON-API requests [1][2][3].
Citations:
- 1: https://developerdocs.instructure.com/services/canvas/resources/quiz_statistics
- 2: https://canvas.ou.edu/doc/api/quiz_statistics.html
- 3: https://canvas.instructure.com/doc/api/quiz_statistics.html
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target file outline ---'
ast-grep outline packages/canvas/endpoints/response-schemas.ts --match createResponseSchema --view expanded || true
printf '%s\n' '--- target implementation ---'
sed -n '1,180p' packages/canvas/endpoints/response-schemas.ts
sed -n '380,470p' packages/canvas/endpoints/response-schemas.ts
printf '%s\n' '--- factory ---'
sed -n '1,100p' packages/canvas/endpoints/factory.ts
printf '%s\n' '--- Canvas operation/schema references ---'
rg -n "getQuizStatistics|getOutcomeResults|createBatchOverridesInACourse|Poll|poll|QuizSubmission|quiz_statistics|outcome_results|quiz_submissions|poll_choices" packages/canvasRepository: corsairdev/corsair
Length of output: 16166
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- response schema selection and definitions ---'
sed -n '180,430p' packages/canvas/endpoints/response-schemas.ts
sed -n '430,510p' packages/canvas/endpoints/response-schemas.ts
printf '%s\n' '--- cited operation definitions ---'
sed -n '280,315p' packages/canvas/endpoints/operations.ts
sed -n '525,565p' packages/canvas/endpoints/operations.ts
sed -n '995,1025p' packages/canvas/endpoints/operations.ts
sed -n '1075,1140p' packages/canvas/endpoints/operations.ts
printf '%s\n' '--- operation name and type declarations ---'
rg -n "type CanvasOperationName|export type CanvasOperation|function isListOperation|const isListOperation|resourceSchemaFor|CanvasEndpointOutputSchemas" packages/canvas/endpointsRepository: corsairdev/corsair
Length of output: 16607
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
ops = Path("packages/canvas/endpoints/operations.ts").read_text()
src = Path("packages/canvas/endpoints/response-schemas.ts").read_text()
names = [
"getQuizStatistics",
"getOutcomeResults",
"createBatchOverridesInACourse",
"createSinglePoll",
"getSinglePoll",
"createSinglePollChoice",
"getSinglePollChoice",
"createSinglePollSession",
"createSinglePollSubmission",
]
for name in names:
m = re.search(
rf"^\s*{re.escape(name)}:\s*\{{\s*"
rf"method:\s*'([^']+)'\s*,\s*"
rf"path:\s*'([^']+)'",
ops,
re.MULTILINE,
)
if not m:
print(f"{name}: operation not found")
continue
method, path = m.groups()
key = name.lower()
if "quizstatistics" in key:
resource = "CanvasJsonArraySchema"
elif "course" in key or "/courses" in path.lower():
resource = "CanvasCourseSchema"
elif "quiz" in key or "/quizzes" in path.lower():
resource = "CanvasQuizSchema"
elif "poll" in key:
resource = "CanvasEntitySchema"
else:
resource = "CanvasEntitySchema"
last = path.rstrip("/").split("/")[-1]
is_list = (
method == "GET"
and (
re.match(r"^(getAll|find|getAccountsThat|getAccountNotifications"
r"|getEnrollmentInvitations|getCourses12|getAUsersMostRecently"
r"|getAllPeer|getAllOutcome|getAllEPortfolios|getAligned"
r"|getCoursesBlueprint)", name, re.I)
or (not last.endswith("}") and (last.endswith("s") or last == "recipients"
or last == "overrides"))
)
)
schema = resource if resource in {
"CanvasJsonArraySchema", "CanvasJsonObjectSchema"
} else f"z.array({resource})" if is_list else resource
print(f"{name}: {method} {path} -> {schema}")
print("\nMinimal Zod-shape checks:")
responses = {
"getQuizStatistics": {"quiz_statistics": [{"id": 1}]},
"getOutcomeResults": {"outcome_results": [{"id": 1}]},
"createBatchOverridesInACourse": [{"id": 1}],
}
expected = {
"getQuizStatistics": "array",
"getOutcomeResults": "array",
"createBatchOverridesInACourse": "object",
}
for name, value in responses.items():
actual = "array" if isinstance(value, list) else "object"
print(f"{name}: expected {expected[name]}, documented-shape sample {actual}, mismatch={actual != expected[name]}")
PYRepository: corsairdev/corsair
Length of output: 1280
🌐 Web query:
Canvas LMS API polls poll choices poll sessions poll submissions response wrapper object create poll get poll official documentation
💡 Result:
The Canvas LMS API provides several endpoints for managing polls, poll sessions, choices, and submissions. These are structured under the Polling namespace [1][2][3][4]. Polls The Poll object includes a unique ID, question, description, creation timestamp, user ID of the creator, and an aggregate of results from all associated poll sessions (where keys are poll choice IDs and values are submission counts) [1][5]. You can list, retrieve, create, update, and delete polls using the /api/v1/polls endpoints [1][6]. Poll Sessions A PollSession object tracks a session associated with a specific poll, course, and course section [3]. It includes information on whether the session is published (is_published) and if results are public (has_public_results) [3]. It also contains an aggregate results object [3]. New poll sessions can be created via POST /api/v1/polls/:poll_id/poll_sessions [3]. Poll Choices PollChoice objects define the options available for a poll [4][7]. Each object contains an ID, the associated poll_id, the text of the choice, whether it is considered a correct answer (is_correct), and its position order [4][8]. You can manage these via /api/v1/polls/:poll_id/poll_choices [4][7]. Poll Submissions PollSubmission objects manage individual responses to poll sessions [2][9]. A submission is created by specifying the poll_choice_id for a given poll session via POST /api/v1/polls/:poll_id/poll_sessions/:poll_session_id/poll_submissions [2][10][9]. Response Wrapper Objects API responses for these endpoints typically wrap the requested object or array of objects in a root-level key matching the resource name, such as {"poll_submissions": [PollSubmission]} or {"poll_choices": [PollChoice]} [2][3][11].
Citations:
- 1: https://canvas.instructure.com/doc/api/polls.html
- 2: https://canvas.instructure.com/doc/api/poll_submissions.html
- 3: https://developerdocs.instructure.com/services/canvas/resources/poll_sessions
- 4: https://developerdocs.instructure.com/services/canvas/resources/poll_choices
- 5: https://developerdocs.instructure.com/services/canvas/resources/polls
- 6: https://documentation.instructure.com/doc/api/polls.html
- 7: https://documentation.instructure.com/doc/api/poll_choices.html
- 8: https://canvas.instructure.com/doc/api/poll%5Fchoices.html
- 9: https://github.com/instructure/canvas-lms/blob/master/app/controllers/polling/poll_submissions_controller.rb
- 10: https://developerdocs.instructure.com/services/canvas/resources/poll_submissions
- 11: https://canvas.ou.edu/doc/api/poll_choices.html
🌐 Web query:
Canvas LMS API batch create assignment overrides response list official documentation
💡 Result:
The official Canvas LMS REST API endpoint for batch creating assignment overrides is POST /api/v1/courses/:course_id/assignments/overrides [1]. According to the official documentation and source code, this endpoint accepts an array of override objects and returns a list of the successfully created AssignmentOverride objects [2][1]. Key details regarding the response and error handling: 1. Response Format: Upon success, the API returns a JSON array containing the created AssignmentOverride objects [1]. 2. Error Handling: The API processes creation within a database transaction, meaning all records are created or none are [2][1]. If errors occur, the response includes an errors attribute, which is an array of error messages corresponding to the inputs [2][1]. Global errors are reported as a single-element array [2][1]. When making the request, the assignment_overrides[] parameter is required and must contain the attributes for each override (such as assignment_id and either student_ids, group_id, or course_section_id) [3][2][1]. Note that if multiple target types (student_ids, group_id, or course_section_id) are provided for a single override, only the most specific one is used (student_ids, then group_id, then course_section_id) and others are ignored [2][1].
Citations:
- 1: https://documentation.instructure.com/doc/api/assignments.html
- 2: https://github.com/instructure/canvas-lms/blob/master/app/controllers/assignment_overrides_controller.rb
- 3: https://developerdocs.instructure.com/services/canvas/resources/assignments
🌐 Web query:
Canvas LMS API outcome results response outcome_results official documentation
💡 Result:
The Canvas LMS API provides the endpoint GET /api/v1/courses/:course_id/outcome_results to retrieve outcome results for users and outcomes within a specific course context [1][2][3]. ### Endpoint Overview - Endpoint: GET /api/v1/courses/:course_id/outcome_results [1][2] - Purpose: Gets the outcome results for users and outcomes in the specified context (frequently used in the Learning Mastery Gradebook) [1][2][4]. ### Request Parameters The endpoint supports several parameters to filter and include additional data: * user_ids[] (integer): Filters results to only include the specified users (SIS IDs can be used if prefixed with sis_user_id:). It is an error to specify a user who is not a student in the context [1][2][3]. * outcome_ids[] (integer): Filters results to only include the specified outcomes. It is an error to specify an outcome not linked to the context [1][2][3]. * include[] (string): Allows for side-loading additional collections. Accepted values include alignments, outcomes, outcomes.alignments, outcome_groups, outcome_links, outcome_paths, and users [2][3][4]. * include_hidden (boolean): If set to true, includes results that are typically hidden from the Learning Mastery Gradebook and student rollup scores [1][3][4]. ### Example Response Structure The standard response returns a JSON object containing an array of outcome_results [1][2][3]: json { "outcome_results": [OutcomeResult] } When specific include[] parameters are used, the response may also contain a linked object containing additional related data, such as outcomes, users, or alignments, depending on what was requested [5]. For more detailed information, you can consult the official Canvas LMS REST API documentation [2][3].
Citations:
- 1: https://developerdocs.instructure.com/services/canvas/resources/outcome_results
- 2: https://canvas.instructure.com/doc/api/outcome_results.html
- 3: https://documentation.instructure.com/doc/api/outcome_results.html
- 4: https://github.com/instructure/canvas-lms/blob/master/app/controllers/outcome_results_controller.rb
- 5: https://canvas.uts.edu.au/doc/api/outcome_results.html
🌐 Web query:
site:canvas.instructure.com/doc/api/polls.html "POST /api/v1/polls" response
💡 Result:
The POST /api/v1/polls endpoint is defined in the Canvas LMS REST API [1]. It is used to create a new poll for the current user [1]. The request requires parameters such as the poll title (polls[][question]) and an optional description (polls[][description]) [1]. Upon a successful request, the endpoint returns a response object containing the created poll details [1]. The structure of the response is a JSON object containing an array of polls, formatted as follows [1]: { "polls": [Poll] } This endpoint belongs to the Polling::PollsController in the Canvas LMS architecture [1].
Citations:
🌐 Web query:
site:canvas.instructure.com/doc/api/poll_choices.html "POST /api/v1/polls/:poll_id/poll_choices" response
💡 Result:
The POST /api/v1/polls/:poll_id/poll_choices endpoint is part of the Canvas LMS REST API and is used to create a new poll choice for a specific poll [1]. According to the official documentation, the response to a successful request returns an object containing a list of the poll's choices [1]. The structure of the response is as follows: { "poll_choices": [PollChoice] } In this response, the [PollChoice] element represents the newly created poll choice object [1]. The endpoint requires the poll_choices[][text] parameter for the descriptive text of the new choice, while poll_choices[][is_correct] (boolean) and poll_choices[][position] (integer) are optional parameters [1].
Citations:
🌐 Web query:
site:canvas.instructure.com/doc/api/poll_sessions.html "POST /api/v1/polls/:poll_id/poll_sessions" response
💡 Result:
The response is:
{
"poll_sessions": [PollSession]
}The PollSession object includes fields such as id, poll_id, course_id, course_section_id, is_published, has_public_results, created_at, results, and poll_submissions. (canvas.instructure.com)
Citations:
🌐 Web query:
site:canvas.instructure.com/doc/api/poll_submissions.html "POST /api/v1/polls/:poll_id/poll_sessions/:poll_session_id/poll_submissions" response
💡 Result:
The POST /api/v1/polls/:poll_id/poll_sessions/:poll_session_id/poll_submissions endpoint in the Canvas LMS REST API is used to create a new poll submission for a specific poll session [1]. The request requires the following parameter: | Parameter | Type | Description | | --- | --- | --- | | poll_submissions[][poll_choice_id] | integer | The chosen poll choice ID for this submission | The API returns a response containing the created poll submission object, wrapped in a poll_submissions array [1]. The structure of a PollSubmission object is as follows [1]: { "id": 1023, "poll_choice_id": 155, "user_id": 4555, "created_at": "2013-11-07T13:16:18Z" } The full response structure is [1]: { "poll_submissions": [ { "id": 1023, "poll_choice_id": 155, "user_id": 4555, "created_at": "2013-11-07T13:16:18Z" } ] }
Citations:
🌐 Web query:
site:canvas.instructure.com/doc/api/polls.html "GET /api/v1/polls/:poll_id" "Poll"
💡 Result:
The GET /api/v1/polls/:id endpoint is part of the Canvas LMS REST API [1]. It is used to retrieve the details of a specific poll by its unique identifier [1]. In this endpoint,:id represents the ID of the poll you wish to fetch [1]. The request is handled by the Polling::PollsController#show method within the Canvas backend [1].
Citations:
🌐 Web query:
site:canvas.instructure.com/doc/api/quizzes.html "POST /api/v1/courses/:course_id/quizzes/:quiz_id/submissions" response quiz_submissions
💡 Result:
The Canvas LMS REST API does not provide a standard endpoint with the path POST /api/v1/courses/:course_id/quizzes/:quiz_id/submissions [1]. Based on the official Canvas LMS REST API documentation, quiz-related operations are typically handled through the Quizzes API (e.g., creating or updating quizzes) [1]. Operations involving quiz submissions are generally managed through the Quiz Submissions API, which uses different endpoint structures, such as POST /api/v1/courses/:course_id/quizzes/:quiz_id/submissions (which is not documented as a valid path for creating submissions) or other endpoints specifically designed for interacting with submission data. Because the requested path is not present in the official documentation, there is no defined response format for it [1]. You should verify the intended API documentation for Quiz Submissions or Quiz Reports if you are looking to interact with student attempts or results.
Citations:
Add operation-specific schemas for Canvas response wrappers
CanvasEndpointOutputSchemas[name].parse(response) rejects documented responses for getQuizStatistics, getOutcomeResults, and createBatchOverridesInACourse. Canvas returns wrapper objects for the first two and an array of assignment overrides for the third.
The polling create operations also return wrappers such as { "polls": [...] }, { "poll_choices": [...] }, { "poll_sessions": [...] }, and { "poll_submissions": [...] }. Add explicit schemas for these operations and preserve strict validation instead of using a permissive fallback.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/canvas/endpoints/response-schemas.ts` around lines 415 - 446, Update
createResponseSchema to return operation-specific wrapper schemas for
getQuizStatistics, getOutcomeResults, createBatchOverridesInACourse, and polling
create operations returning polls, poll_choices, poll_sessions, or
poll_submissions. Define or reuse strict Zod schemas matching each documented
response shape, including the assignment-overrides array, and select them by
operation name before generic resource/list handling; do not use a permissive
fallback.
| export type CanvasEndpointInputs = { | ||
| [K in CanvasOperationName]: CanvasRequestFields & | ||
| (HasPathParams<(typeof canvasOperations)[K]['path']> extends true | ||
| ? { pathParams: PathParamsFor<(typeof canvasOperations)[K]['path']> } | ||
| : { pathParams?: Record<string, string> }); | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Type-level body optionality disagrees with the runtime schema.
CanvasEndpointInputs[K] spreads CanvasRequestFields, where body is always optional. createRequestInputSchema requires a non-empty body for every mutation that is not bodyless. A caller can therefore omit body, compile successfully, and fail at runtime inside CanvasEndpointInputSchemas[name].parse.
Mirror the runtime rule in the mapped type. Make body required for mutations that are not bodyless.
🐛 Sketch of the type change
+type RequiresBody<K extends CanvasOperationName> =
+ (typeof canvasOperations)[K]['method'] extends 'POST' | 'PUT' | 'PATCH'
+ ? (typeof canvasOperations)[K] extends { bodyless: true }
+ ? false
+ : true
+ : false;
+
export type CanvasEndpointInputs = {
- [K in CanvasOperationName]: CanvasRequestFields &
+ [K in CanvasOperationName]: Omit<CanvasRequestFields, 'body'> &
+ (RequiresBody<K> extends true
+ ? { body: Record<string, unknown> }
+ : { body?: Record<string, unknown> }) &
(HasPathParams<(typeof canvasOperations)[K]['path']> extends true
? { pathParams: PathParamsFor<(typeof canvasOperations)[K]['path']> }
: { pathParams?: Record<string, string> });
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export type CanvasEndpointInputs = { | |
| [K in CanvasOperationName]: CanvasRequestFields & | |
| (HasPathParams<(typeof canvasOperations)[K]['path']> extends true | |
| ? { pathParams: PathParamsFor<(typeof canvasOperations)[K]['path']> } | |
| : { pathParams?: Record<string, string> }); | |
| }; | |
| type RequiresBody<K extends CanvasOperationName> = | |
| (typeof canvasOperations)[K]['method'] extends 'POST' | 'PUT' | 'PATCH' | |
| ? (typeof canvasOperations)[K] extends { bodyless: true } | |
| ? false | |
| : true | |
| : false; | |
| export type CanvasEndpointInputs = { | |
| [K in CanvasOperationName]: Omit<CanvasRequestFields, 'body'> & | |
| (RequiresBody<K> extends true | |
| ? { body: Record<string, unknown> } | |
| : { body?: Record<string, unknown> }) & | |
| (HasPathParams<(typeof canvasOperations)[K]['path']> extends true | |
| ? { pathParams: PathParamsFor<(typeof canvasOperations)[K]['path']> } | |
| : { pathParams?: Record<string, string> }); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/canvas/endpoints/types.ts` around lines 62 - 67, Update
CanvasEndpointInputs to derive body optionality from each operation’s mutation
and bodyless status, matching createRequestInputSchema: require a non-empty body
for mutations that are not bodyless, while preserving optional body for bodyless
or non-mutation operations. Ensure the existing pathParams mapping remains
unchanged.
| if (rows.length === 1) { | ||
| return toExternalId(firstString(rows[0]?.id)) ?? null; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Do not use a singleton child account as the tenant identity.
Line 45 returns a child id when the API page has one row. The resulting canvas_account_id cannot match webhook events that carry the root account ID. If parent_account_id is set, use a valid root_account_id or return null.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/canvas/webhooks/oauth-tenant-link.ts` around lines 44 - 46, Update
the singleton-row branch in the tenant-link resolution function so it does not
use the child row’s id as the tenant identity when parent_account_id is set.
Prefer a valid root_account_id from the row or related account data, and return
null when no valid root identity is available; preserve the existing behavior
for root accounts.
| function numericAccountId(values: unknown[]): string | null { | ||
| const value = firstString(values); | ||
| if (!value || !/^\d+$/.test(value)) return null; | ||
| return value; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Select the first numeric account ID, not the first non-empty value.
Both paths stop when an earlier identifier is non-empty but non-numeric. A valid later account ID is then ignored.
packages/canvas/webhooks/tenant-matcher.ts#L5-L8: iterate throughvaluesand return the first normalized value that matches^\d+$.packages/canvas/webhooks/oauth-tenant-link.ts#L74-L76: scancanvas_account_id,account_id, androot_account_idfor the first normalized numeric value before checking UUID fields.
📍 Affects 2 files
packages/canvas/webhooks/tenant-matcher.ts#L5-L8(this comment)packages/canvas/webhooks/oauth-tenant-link.ts#L74-L76
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/canvas/webhooks/tenant-matcher.ts` around lines 5 - 8, The
numericAccountId function in packages/canvas/webhooks/tenant-matcher.ts:5-8 must
iterate through all values and return the first normalized string matching
/^\d+$/, rather than validating only firstString(values). In
packages/canvas/webhooks/oauth-tenant-link.ts:74-76, scan canvas_account_id,
account_id, and root_account_id for the first normalized numeric value before
checking UUID fields.
Description
This PR introduces a comprehensive Canvas LMS integration to Corsair.
Key Features:
canvasplugin with over 200+ REST and GraphQL endpoints.baseUrlresolution to accommodate both cloud and self-hosted Canvas instances.Closes #486
Checklist
Before submitting your PR, please verify the following:
pnpm lintand all checks pass (Note: passed for canvas package)pnpm typecheckand there are no TypeScript errorspnpm buildand all packages build successfullypnpm testand all tests pass (Canvas tests passing 5/5)Screenshots / Demos (if applicable)
Additional Notes
x-canvas-signature).Summary by CodeRabbit