mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-26 07:38:33 -06:00
workign on canvas api requests
This commit is contained in:
28
nextjs/src/app/api/canvas/[...rest]/route.ts
Normal file
28
nextjs/src/app/api/canvas/[...rest]/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,38 +2,30 @@ import { isServer } from "@tanstack/react-query";
|
||||
import axios, { AxiosInstance, AxiosError, AxiosHeaders } from "axios";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const token = process.env.NEXT_PUBLIC_CANVAS_TOKEN;
|
||||
if (!token) {
|
||||
throw new Error("NEXT_PUBLIC_CANVAS_TOKEN not in environment");
|
||||
}
|
||||
const canvasBaseUrl = "https://snow.instructure.com/api/v1/";
|
||||
|
||||
export const axiosClient: AxiosInstance = axios.create();
|
||||
|
||||
if (!isServer) {
|
||||
axiosClient.interceptors.request.use((config) => {
|
||||
if (
|
||||
config.url &&
|
||||
config.url.startsWith("https://snow.instructure.com/api/v1/")
|
||||
) {
|
||||
const newUrl = config.url.replace(
|
||||
"https://snow.instructure.com/api/v1/",
|
||||
"/api/canvas/"
|
||||
);
|
||||
if (config.url && config.url.startsWith(canvasBaseUrl)) {
|
||||
const newUrl = config.url.replace(canvasBaseUrl, "/api/canvas/");
|
||||
config.url = newUrl;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
axiosClient.interceptors.request.use((config) => {
|
||||
if (
|
||||
config.url &&
|
||||
config.url.startsWith("https://snow.instructure.com/api/v1/")
|
||||
) {
|
||||
config.headers.set("Authorization", `Bearer ${token}`);
|
||||
} else {
|
||||
const token = process.env.CANVAS_TOKEN;
|
||||
if (!token) {
|
||||
throw new Error("CANVAS_TOKEN not in environment");
|
||||
}
|
||||
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(
|
||||
(response) => response,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
|
||||
import { LocalCoursePage } from "@/models/local/page/localCoursePage";
|
||||
import { canvasServiceUtils } from "./canvasServiceUtils";
|
||||
import { webRequestor } from "./webRequestor";
|
||||
|
||||
const baseCanvasUrl = "https://snow.instructure.com/api/v1";
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { CanvasEnrollmentTermModel } from "@/models/canvas/enrollmentTerms/canvasEnrollmentTermModel";
|
||||
import { canvasServiceUtils } from "./canvasServiceUtils";
|
||||
import { webRequestor } from "./webRequestor";
|
||||
import { CanvasCourseModel } from "@/models/canvas/courses/canvasCourseModel";
|
||||
import { CanvasModuleItem } from "@/models/canvas/modules/canvasModuleItems";
|
||||
import { CanvasPage } from "@/models/canvas/pages/canvasPageModel";
|
||||
import { CanvasEnrollmentModel } from "@/models/canvas/enrollments/canvasEnrollmentModel";
|
||||
import { axiosClient } from "../axiosUtils";
|
||||
|
||||
const getTerms = async () => {
|
||||
const url = `accounts/10/terms`;
|
||||
@@ -28,7 +28,7 @@ export const canvasService = {
|
||||
|
||||
async getCourse(courseId: number): Promise<CanvasCourseModel> {
|
||||
const url = `courses/${courseId}`;
|
||||
const { data, response } = await webRequestor.get<CanvasCourseModel>(url);
|
||||
const { data } = await axiosClient.get<CanvasCourseModel>(url);
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -62,8 +62,7 @@ export const canvasService = {
|
||||
const body = {
|
||||
module_item: { title: item.title, position: item.position },
|
||||
};
|
||||
const { data, response } =
|
||||
await webRequestor.putWithDeserialize<CanvasModuleItem>(url, body);
|
||||
const { data } = await axiosClient.put<CanvasModuleItem>(url, body);
|
||||
|
||||
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}`);
|
||||
const url = `courses/${canvasCourseId}/modules/${canvasModuleId}/items`;
|
||||
const body = { module_item: { title, type, content_id: contentId } };
|
||||
const response = await webRequestor.post(url, body);
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error("Something went wrong creating module item");
|
||||
const response = await axiosClient.post(url, body);
|
||||
},
|
||||
|
||||
async createPageModuleItem(
|
||||
@@ -95,18 +91,13 @@ export const canvasService = {
|
||||
const body = {
|
||||
module_item: { title, type: "Page", page_url: canvasPage.url },
|
||||
};
|
||||
const { data, response } =
|
||||
await webRequestor.postWithDeserialize<CanvasModuleItem>(url, body);
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error("Something went wrong creating page module item");
|
||||
await axiosClient.post<CanvasModuleItem>(url, body);
|
||||
},
|
||||
|
||||
async getEnrolledStudents(canvasCourseId: number) {
|
||||
console.log(`Getting students for course ${canvasCourseId}`);
|
||||
const url = `courses/${canvasCourseId}/enrollments?enrollment_type=student`;
|
||||
const { data, response } =
|
||||
await webRequestor.getMany<CanvasEnrollmentModel>(url);
|
||||
const { data } = await axiosClient.get<CanvasEnrollmentModel[]>(url);
|
||||
|
||||
if (!data)
|
||||
throw new Error(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// services/canvasServiceUtils.ts
|
||||
|
||||
import { AxiosResponseHeaders, RawAxiosResponseHeaders } from "axios";
|
||||
import { webRequestor } from "./webRequestor";
|
||||
import { axiosClient } from "../axiosUtils";
|
||||
|
||||
const getNextUrl = (
|
||||
headers: AxiosResponseHeaders | RawAxiosResponseHeaders
|
||||
@@ -28,19 +28,20 @@ export const canvasServiceUtils = {
|
||||
const url = new URL(request.url);
|
||||
url.searchParams.set("per_page", "100");
|
||||
|
||||
const { data: firstData, response: firstResponse } =
|
||||
await webRequestor.get<T>(url.toString());
|
||||
const { data: firstData, headers: firstHeaders } = await axiosClient.get<T>(
|
||||
url.toString()
|
||||
);
|
||||
|
||||
var returnData: T[] = firstData ? [firstData] : [];
|
||||
var nextUrl = getNextUrl(firstResponse.headers);
|
||||
var nextUrl = getNextUrl(firstHeaders);
|
||||
|
||||
while (nextUrl) {
|
||||
requestCount += 1;
|
||||
const { data, response } = await webRequestor.get<T>(nextUrl);
|
||||
const { data, headers } = await axiosClient.get<T>(nextUrl);
|
||||
if (data) {
|
||||
returnData = [...returnData, data];
|
||||
}
|
||||
nextUrl = getNextUrl(response.headers);
|
||||
nextUrl = getNextUrl(headers);
|
||||
}
|
||||
|
||||
if (requestCount > 1) {
|
||||
|
||||
66
nextjs/src/services/canvas/canvasWebRequestor.ts
Normal file
66
nextjs/src/services/canvas/canvasWebRequestor.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user