workign on canvas api requests

This commit is contained in:
2024-09-10 08:35:07 -06:00
parent 029e3ff7eb
commit f94dcca904
8 changed files with 121 additions and 210 deletions

View File

@@ -1,19 +1,7 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const token = process.env.NEXT_PUBLIC_CANVAS_TOKEN;
if (!token) {
throw new Error("CANVAS_TOKEN not in environment");
}
const nextConfig = { const nextConfig = {
async rewrites() {
return [
{
source: "/api/canvas/:rest*",
destination: "https://snow.instructure.com/api/v1/:rest*",
},
];
},
}; };
export default nextConfig; export default nextConfig;

View File

@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from "next/server";
import { axiosClient } from "@/services/axiosUtils";
export async function GET(
req: NextRequest,
{ params }: { params: { rest: string[] } }
) {
const { rest } = params;
const path = rest.join("/");
try {
const newUrl = `https://snow.instructure.com/api/v1/${path}`;
const response = await axiosClient.get(newUrl, {
headers: {
// Include other headers from the incoming request if needed:
// 'Content-Type': req.headers.get('content-type') || 'application/json',
"Content-Type": "application/json",
},
});
return NextResponse.json(response.data);
} catch (error: any) {
return new NextResponse(
JSON.stringify({ error: error.message || "Request failed" }),
{ status: error.response?.status || 500 }
);
}
}

View File

@@ -2,38 +2,30 @@ import { isServer } from "@tanstack/react-query";
import axios, { AxiosInstance, AxiosError, AxiosHeaders } from "axios"; import axios, { AxiosInstance, AxiosError, AxiosHeaders } from "axios";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
const token = process.env.NEXT_PUBLIC_CANVAS_TOKEN; const canvasBaseUrl = "https://snow.instructure.com/api/v1/";
if (!token) {
throw new Error("NEXT_PUBLIC_CANVAS_TOKEN not in environment");
}
export const axiosClient: AxiosInstance = axios.create(); export const axiosClient: AxiosInstance = axios.create();
if (!isServer) { if (!isServer) {
axiosClient.interceptors.request.use((config) => { axiosClient.interceptors.request.use((config) => {
if ( if (config.url && config.url.startsWith(canvasBaseUrl)) {
config.url && const newUrl = config.url.replace(canvasBaseUrl, "/api/canvas/");
config.url.startsWith("https://snow.instructure.com/api/v1/")
) {
const newUrl = config.url.replace(
"https://snow.instructure.com/api/v1/",
"/api/canvas/"
);
config.url = newUrl; config.url = newUrl;
} }
return config; return config;
}); });
} } else {
const token = process.env.CANVAS_TOKEN;
axiosClient.interceptors.request.use((config) => { if (!token) {
if ( throw new Error("CANVAS_TOKEN not in environment");
config.url &&
config.url.startsWith("https://snow.instructure.com/api/v1/")
) {
config.headers.set("Authorization", `Bearer ${token}`);
} }
return config; axiosClient.interceptors.request.use((config) => {
}); if (config.url && config.url.startsWith(canvasBaseUrl)) {
config.headers.set("Authorization", `Bearer ${token}`);
}
return config;
});
}
axiosClient.interceptors.response.use( axiosClient.interceptors.response.use(
(response) => response, (response) => response,

View File

@@ -1,7 +1,6 @@
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel"; import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
import { LocalCoursePage } from "@/models/local/page/localCoursePage"; import { LocalCoursePage } from "@/models/local/page/localCoursePage";
import { canvasServiceUtils } from "./canvasServiceUtils"; import { canvasServiceUtils } from "./canvasServiceUtils";
import { webRequestor } from "./webRequestor";
const baseCanvasUrl = "https://snow.instructure.com/api/v1"; const baseCanvasUrl = "https://snow.instructure.com/api/v1";

View File

@@ -1,10 +1,10 @@
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel"; import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
import { canvasServiceUtils } from "./canvasServiceUtils"; import { canvasServiceUtils } from "./canvasServiceUtils";
import { webRequestor } from "./webRequestor";
import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel"; import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
import { CanvasModuleItem } from "@/models/canvas/modules/canvasModuleItems"; import { CanvasModuleItem } from "@/models/canvas/modules/canvasModuleItems";
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel"; import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
import { CanvasEnrollmentModel } from "@/models/canvas/enrollments/canvasEnrollmentModel"; import { CanvasEnrollmentModel } from "@/models/canvas/enrollments/canvasEnrollmentModel";
import { axiosClient } from "../axiosUtils";
const getTerms = async () => { const getTerms = async () => {
const url = `accounts/10/terms`; const url = `accounts/10/terms`;
@@ -28,7 +28,7 @@ export const canvasService = {
async getCourse(courseId: number): Promise<CanvasCourseModel> { async getCourse(courseId: number): Promise<CanvasCourseModel> {
const url = `courses/${courseId}`; const url = `courses/${courseId}`;
const { data, response } = await webRequestor.get<CanvasCourseModel>(url); const { data } = await axiosClient.get<CanvasCourseModel>(url);
return data; return data;
}, },
@@ -62,8 +62,7 @@ export const canvasService = {
const body = { const body = {
module_item: { title: item.title, position: item.position }, module_item: { title: item.title, position: item.position },
}; };
const { data, response } = const { data } = await axiosClient.put<CanvasModuleItem>(url, body);
await webRequestor.putWithDeserialize<CanvasModuleItem>(url, body);
if (!data) throw new Error("Something went wrong updating module item"); if (!data) throw new Error("Something went wrong updating module item");
}, },
@@ -78,10 +77,7 @@ export const canvasService = {
console.log(`Creating new module item ${title}`); console.log(`Creating new module item ${title}`);
const url = `courses/${canvasCourseId}/modules/${canvasModuleId}/items`; const url = `courses/${canvasCourseId}/modules/${canvasModuleId}/items`;
const body = { module_item: { title, type, content_id: contentId } }; const body = { module_item: { title, type, content_id: contentId } };
const response = await webRequestor.post(url, body); const response = await axiosClient.post(url, body);
if (!response.ok)
throw new Error("Something went wrong creating module item");
}, },
async createPageModuleItem( async createPageModuleItem(
@@ -95,18 +91,13 @@ export const canvasService = {
const body = { const body = {
module_item: { title, type: "Page", page_url: canvasPage.url }, module_item: { title, type: "Page", page_url: canvasPage.url },
}; };
const { data, response } = await axiosClient.post<CanvasModuleItem>(url, body);
await webRequestor.postWithDeserialize<CanvasModuleItem>(url, body);
if (!response.ok)
throw new Error("Something went wrong creating page module item");
}, },
async getEnrolledStudents(canvasCourseId: number) { async getEnrolledStudents(canvasCourseId: number) {
console.log(`Getting students for course ${canvasCourseId}`); console.log(`Getting students for course ${canvasCourseId}`);
const url = `courses/${canvasCourseId}/enrollments?enrollment_type=student`; const url = `courses/${canvasCourseId}/enrollments?enrollment_type=student`;
const { data, response } = const { data } = await axiosClient.get<CanvasEnrollmentModel[]>(url);
await webRequestor.getMany<CanvasEnrollmentModel>(url);
if (!data) if (!data)
throw new Error( throw new Error(

View File

@@ -1,7 +1,7 @@
// services/canvasServiceUtils.ts // services/canvasServiceUtils.ts
import { AxiosResponseHeaders, RawAxiosResponseHeaders } from "axios"; import { AxiosResponseHeaders, RawAxiosResponseHeaders } from "axios";
import { webRequestor } from "./webRequestor"; import { axiosClient } from "../axiosUtils";
const getNextUrl = ( const getNextUrl = (
headers: AxiosResponseHeaders | RawAxiosResponseHeaders headers: AxiosResponseHeaders | RawAxiosResponseHeaders
@@ -28,19 +28,20 @@ export const canvasServiceUtils = {
const url = new URL(request.url); const url = new URL(request.url);
url.searchParams.set("per_page", "100"); url.searchParams.set("per_page", "100");
const { data: firstData, response: firstResponse } = const { data: firstData, headers: firstHeaders } = await axiosClient.get<T>(
await webRequestor.get<T>(url.toString()); url.toString()
);
var returnData: T[] = firstData ? [firstData] : []; var returnData: T[] = firstData ? [firstData] : [];
var nextUrl = getNextUrl(firstResponse.headers); var nextUrl = getNextUrl(firstHeaders);
while (nextUrl) { while (nextUrl) {
requestCount += 1; requestCount += 1;
const { data, response } = await webRequestor.get<T>(nextUrl); const { data, headers } = await axiosClient.get<T>(nextUrl);
if (data) { if (data) {
returnData = [...returnData, data]; returnData = [...returnData, data];
} }
nextUrl = getNextUrl(response.headers); nextUrl = getNextUrl(headers);
} }
if (requestCount > 1) { if (requestCount > 1) {

View File

@@ -0,0 +1,66 @@
import { AxiosResponse } from "axios";
import { axiosClient } from "../axiosUtils";
type FetchOptions = Omit<RequestInit, "method">;
const rateLimitRetryCount = 6;
const rateLimitSleepInterval = 1000;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const isRateLimited = async (
response: AxiosResponse
): Promise<boolean> => {
const content = await response.data;
return (
response.status === 403 &&
content.includes("403 Forbidden (Rate Limit Exceeded)")
);
};
const rateLimitAwarePost = async (url: string, body: any, retryCount = 0) => {
const response = await axiosClient.post(url, body);
if (await isRateLimited(response)) {
if (retryCount < rateLimitRetryCount) {
console.info(
`Hit rate limit on post, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
);
await sleep(rateLimitSleepInterval);
return await rateLimitAwarePost(url, body, retryCount + 1);
}
}
return response;
};
export const rateLimitAwareDelete = async (
url: string,
retryCount = 0
): Promise<void> => {
try {
const response = await axiosClient.delete(url);
if (await isRateLimited(response)) {
console.info("After delete response in rate limited");
await sleep(rateLimitSleepInterval);
return await rateLimitAwareDelete(url, retryCount + 1);
}
} catch (e) {
const error = e as Error & { response?: Response };
if (error.response?.status === 403) {
if (retryCount < rateLimitRetryCount) {
console.info(
`Hit rate limit in delete, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
);
await sleep(rateLimitSleepInterval);
return await rateLimitAwareDelete(url, retryCount + 1);
} else {
console.info(
`Hit rate limit in delete, ${rateLimitRetryCount} retries did not fix it`
);
}
}
throw e;
}
};

View File

@@ -1,154 +0,0 @@
import { axiosClient } from "../axiosUtils";
type FetchOptions = Omit<RequestInit, "method">;
const rateLimitRetryCount = 6;
const rateLimitSleepInterval = 1000;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const isRateLimited = async (response: Response): Promise<boolean> => {
const content = await response.text();
return (
response.status === 403 &&
content.includes("403 Forbidden (Rate Limit Exceeded)")
);
};
const deserialize = async <T>(response: Response): Promise<T | undefined> => {
if (!response.ok) {
console.error(`Error with response to ${response.url} ${response.status}`);
throw new Error(
`Error with response to ${response.url} ${response.status}`
);
}
try {
return (await response.json()) as T;
} catch (e) {
console.error(`An error occurred during deserialization: ${e}`);
throw e;
}
};
const rateLimitAwarePost = async (
url: string,
body: any,
retryCount = 0
): Promise<Response> => {
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
});
if (await isRateLimited(response)) {
if (retryCount < rateLimitRetryCount) {
console.info(
`Hit rate limit on post, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
);
await sleep(rateLimitSleepInterval);
return await rateLimitAwarePost(url, body, retryCount + 1);
}
}
if (!response.ok) {
const content = await response.text();
console.error(`Error with response, response content: ${content}`);
throw new Error(
`Error post response, retrycount: ${retryCount}, ratelimited: ${await isRateLimited(
response
)}, code: ${response.status}, response content: ${content}`
);
}
return response;
};
const recursiveDelete = async (
url: string,
options: FetchOptions,
retryCount = 0
): Promise<Response> => {
try {
const response = await fetch(url, {
...options,
method: "DELETE",
headers: {
},
});
if (await isRateLimited(response)) {
console.info("After delete response in rate limited");
await sleep(rateLimitSleepInterval);
return await recursiveDelete(url, options, retryCount + 1);
}
return response;
} catch (e) {
const error = e as Error & { response?: Response };
if (error.response?.status === 403) {
if (retryCount < rateLimitRetryCount) {
console.info(
`Hit rate limit in delete, retry count is ${retryCount} / ${rateLimitRetryCount}, retrying`
);
await sleep(rateLimitSleepInterval);
return await recursiveDelete(url, options, retryCount + 1);
} else {
console.info(
`Hit rate limit in delete, ${rateLimitRetryCount} retries did not fix it`
);
}
}
throw e;
}
};
export const webRequestor = {
getMany: async <T>(url: string, options: FetchOptions = {}) => {
const response = await axiosClient.get<T[]>(url);
return { data: response.data, response };
},
get: async <T>(url: string) => {
const response = await axiosClient.get<T>(url);
return { data: response.data, response };
},
post: async (url: string, body: any) => {
return await rateLimitAwarePost(url, body);
},
postWithDeserialize: async <T>(url: string, body: any) => {
const response = await rateLimitAwarePost(url, body);
return { data: await deserialize<T[]>(response), response };
},
put: async (url: string, body: any = {}) => {
const response = await fetch(url, {
method: "PUT",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
});
return response;
},
putWithDeserialize: async <T>(url: string, body: any = {}) => {
const response = await fetch(url, {
body: JSON.stringify(body),
method: "PUT",
headers: {
"Content-Type": "application/json",
},
});
return { data: await deserialize<T[]>(response), response };
},
delete: async (
url: string,
options: FetchOptions = {}
): Promise<Response> => {
return await recursiveDelete(url, options);
},
};