diff --git a/nextjs/src/app/page.tsx b/nextjs/src/app/page.tsx index d8ca776..65fb74f 100644 --- a/nextjs/src/app/page.tsx +++ b/nextjs/src/app/page.tsx @@ -1,9 +1,14 @@ +import { canvasAssignmentService } from "@/services/canvas/canvasAssignmentService"; import Image from "next/image"; -export default function Home() { +export default async function Home() { + const assignments = await canvasAssignmentService.getAll(960410); + return (
- + {assignments.map((assignment) => ( +
{assignment.name}
+ ))}
); } diff --git a/nextjs/src/services/canvas/canvasAssignmentService.ts b/nextjs/src/services/canvas/canvasAssignmentService.ts new file mode 100644 index 0000000..73e6b23 --- /dev/null +++ b/nextjs/src/services/canvas/canvasAssignmentService.ts @@ -0,0 +1,18 @@ +import { CanvasAssignment } from "@/models/canvas/assignments/canvasAssignment"; +import { canvasServiceUtils } from "./canvasServiceUtils"; + +export const canvasAssignmentService = { + async getAll(courseId: number): Promise { + const url = `courses/${courseId}/assignments`; + const assignments = await canvasServiceUtils.paginatedRequest< + CanvasAssignment[] + >({ url }); + return assignments.flatMap((assignments) => + assignments.map((a) => ({ + ...a, + due_at: a.due_at ? new Date(a.due_at).toLocaleString() : undefined, // timezones? + lock_at: a.lock_at ? new Date(a.lock_at).toLocaleString() : undefined, // timezones? + })) + ); + }, +}; diff --git a/nextjs/src/services/canvas/canvasServiceUtils.ts b/nextjs/src/services/canvas/canvasServiceUtils.ts new file mode 100644 index 0000000..db2c537 --- /dev/null +++ b/nextjs/src/services/canvas/canvasServiceUtils.ts @@ -0,0 +1,59 @@ +// services/canvasServiceUtils.ts + +import { webRequestor } from "./webRequestor"; + +const BASE_URL = "https://snow.instructure.com/api/v1/"; + +const getNextUrl = (headers: Headers): string | undefined => { + const linkHeader = headers.get("Link"); + if (!linkHeader) return undefined; + + const links = linkHeader.split(",").map((link) => link.trim()); + const nextLink = links.find((link) => link.includes('rel="next"')); + + if (!nextLink) return undefined; + + const nextUrl = nextLink.split(";")[0].trim().slice(1, -1); + return nextUrl.replace(BASE_URL, "").trim(); +}; + +export const canvasServiceUtils = { + async paginatedRequest(request: { url: string }): Promise { + var requestCount = 1; + const url = new URL(request.url!, BASE_URL); + url.searchParams.set("per_page", "100"); + + const [firstData, firstResponse] = await webRequestor.get( + url.toString() + ); + + if (!firstResponse.ok) { + console.error( + "error in response", + firstResponse.statusText, + firstResponse.body + ); + throw new Error("error in response"); + } + + var returnData: T[] = firstData ? [firstData] : []; + var nextUrl = getNextUrl(firstResponse.headers); + + while (nextUrl) { + requestCount += 1; + const [data, response] = await webRequestor.get(nextUrl); + if (data) { + returnData = [...returnData, data]; + } + nextUrl = getNextUrl(response.headers); + } + + if (requestCount > 1) { + console.log( + `Requesting ${typeof returnData} took ${requestCount} requests` + ); + } + + return returnData; + }, +}; diff --git a/nextjs/src/services/canvas/requestUtils.ts b/nextjs/src/services/canvas/webRequestor.ts similarity index 88% rename from nextjs/src/services/canvas/requestUtils.ts rename to nextjs/src/services/canvas/webRequestor.ts index f1a631f..b9cf3d9 100644 --- a/nextjs/src/services/canvas/requestUtils.ts +++ b/nextjs/src/services/canvas/webRequestor.ts @@ -1,4 +1,3 @@ - type FetchOptions = Omit; const token = process.env.CANVAS_TOKEN; @@ -20,7 +19,7 @@ const isRateLimited = async (response: Response): Promise => { ); }; -const deserialize = async (response: Response): Promise => { +const deserialize = async (response: Response): Promise => { if (!response.ok) { console.error(`Error with response to ${response.url} ${response.status}`); throw new Error( @@ -112,11 +111,11 @@ const recursiveDeleteAsync = async ( throw e; } }; -export const WebRequestor = { - getManyAsync: async ( +export const webRequestor = { + getMany: async ( url: string, options: FetchOptions = {} - ): Promise<[T[] | null, Response]> => { + ): Promise<[T[] | undefined, Response]> => { const response = await fetch(url, { ...options, method: "GET", @@ -128,10 +127,10 @@ export const WebRequestor = { return [await deserialize(response), response]; }, - getAsync: async ( + get: async ( url: string, options: FetchOptions = {} - ): Promise<[T | null, Response]> => { + ): Promise<[T | undefined, Response]> => { const response = await fetch(url, { ...options, method: "GET", @@ -143,25 +142,19 @@ export const WebRequestor = { return [await deserialize(response), response]; }, - postAsync: async ( - url: string, - options: FetchOptions = {} - ): Promise => { + post: async (url: string, options: FetchOptions = {}): Promise => { return await rateLimitAwarePostAsync(url, options); }, - postAsyncWithDeserialize: async ( + postWithDeserialize: async ( url: string, options: FetchOptions = {} - ): Promise<[T | null, Response]> => { + ): Promise<[T | undefined, Response]> => { const response = await rateLimitAwarePostAsync(url, options); return [await deserialize(response), response]; }, - putAsync: async ( - url: string, - options: FetchOptions = {} - ): Promise => { + put: async (url: string, options: FetchOptions = {}): Promise => { const response = await fetch(url, { ...options, method: "PUT", @@ -174,10 +167,10 @@ export const WebRequestor = { return response; }, - putAsyncWithDeserialize: async ( + putWithDeserialize: async ( url: string, options: FetchOptions = {} - ): Promise<[T | null, Response]> => { + ): Promise<[T | undefined, Response]> => { const response = await fetch(url, { ...options, method: "PUT", @@ -190,7 +183,7 @@ export const WebRequestor = { return [await deserialize(response), response]; }, - deleteAsync: async ( + delete: async ( url: string, options: FetchOptions = {} ): Promise => {