1
0
Fork 0
mirror of https://github.com/classchartsapi/classcharts-api-js.git synced 2026-05-14 11:58:13 +00:00

feat: denoify (#33)

This commit is contained in:
James Cook 2023-08-30 12:28:49 +01:00 committed by GitHub
parent f2981dd167
commit f86ded2ae3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 331 additions and 4940 deletions

View file

@ -15,8 +15,8 @@ import type {
GetStudentInfoResponse,
HomeworksResponse,
LessonsResponse,
} from "../types.js";
import { PING_INTERVAL } from "../utils/consts.js";
} from "../types.ts";
import { PING_INTERVAL } from "../utils/consts.ts";
/**
* Shared client for both parent and student. This is not exported and should not be used directly
@ -46,7 +46,6 @@ export class BaseClient {
*/
protected API_BASE = "";
/**
*
* @param API_BASE Base API URL, this is different depending on if its called as a parent or student
*/
constructor(API_BASE: string) {
@ -113,7 +112,7 @@ export class BaseClient {
let responseJSON: ClassChartsResponse<unknown, unknown>;
try {
responseJSON = await request.json();
} catch (err) {
} catch {
throw new Error(
"Error parsing JSON. Returned response: " + (await request.text())
);
@ -121,8 +120,8 @@ export class BaseClient {
if (responseJSON.success == 0) {
throw new Error(responseJSON.error);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return responseJSON as any;
// deno-lint-ignore no-explicit-any
return responseJSON as unknown as any;
}
/**
* Gets general information about the current student
@ -150,7 +149,7 @@ export class BaseClient {
options?.from && params.append("from", options?.from);
options?.to && params.append("to", options?.to);
options?.last_id && params.append("last_id", options?.last_id);
return this.makeAuthedRequest(
return await this.makeAuthedRequest(
this.API_BASE + "/activity/" + this.studentId + "?" + params.toString(),
{
method: "GET",
@ -214,8 +213,9 @@ export class BaseClient {
*/
async getHomeworks(options?: GetHomeworkOptions): Promise<HomeworksResponse> {
const params = new URLSearchParams();
if (options?.displayDate)
if (options?.displayDate) {
params.append("display_date", String(options?.displayDate));
}
options?.from && params.append("from", String(options?.from));
options?.to && params.append("to", String(options?.to));

View file

@ -1,8 +1,8 @@
import type { GetPupilsResponse } from "../types.js";
import type { GetPupilsResponse } from "../types.ts";
import { BaseClient } from "./baseClient.js";
import { API_BASE_PARENT, BASE_URL } from "../utils/consts.js";
import { parseCookies } from "../utils/utils.js";
import { BaseClient } from "./baseClient.ts";
import { API_BASE_PARENT, BASE_URL } from "../utils/consts.ts";
import { parseCookies } from "../utils/utils.ts";
/**
* Parent Client
*/
@ -12,7 +12,6 @@ export class ParentClient extends BaseClient {
// @ts-expect-error Init in .login
public pupils: GetPupilsResponse;
/**
*
* @param email Parent's email address
* @param password Parent's password
*/
@ -26,7 +25,7 @@ export class ParentClient extends BaseClient {
* Authenticates with ClassCharts
*/
async login(): Promise<void> {
if (!this.email) throw new Error("Email not inputted");
if (!this.email) throw new Error("Email not provided");
const formData = new URLSearchParams();
formData.append("_method", "POST");
formData.append("email", this.email);
@ -42,13 +41,14 @@ export class ParentClient extends BaseClient {
headers: headers,
credentials: undefined,
});
if (response.status != 302 || !response.headers.get("set-cookie"))
if (response.status != 302 || !response.headers.get("set-cookie")) {
throw new Error(
"Unauthenticated: ClassCharts returned an error: " +
response.status +
" " +
response.statusText
);
}
const cookies = String(response.headers.get("set-cookie"));
// this.authCookies = cookies.split(";");
@ -66,7 +66,7 @@ export class ParentClient extends BaseClient {
* @returns an array of Pupils connected to this parent's account
*/
async getPupils(): Promise<GetPupilsResponse> {
return super.makeAuthedRequest(super.API_BASE + "/pupils", {
return await super.makeAuthedRequest(super.API_BASE + "/pupils", {
method: "GET",
});
}
@ -76,7 +76,7 @@ export class ParentClient extends BaseClient {
*
* @see getPupils
*/
async selectPupil(pupilId: number): Promise<void> {
selectPupil(pupilId: number) {
if (!pupilId) throw new Error("No pupil ID specified");
const pupils = this.pupils;
for (let i = 0; i < pupils.length; i++) {

View file

@ -1,6 +1,6 @@
import { API_BASE_STUDENT, BASE_URL } from "../utils/consts.js";
import { BaseClient } from "./baseClient.js";
import { parseCookies } from "../utils/utils.js";
import { API_BASE_STUDENT, BASE_URL } from "../utils/consts.ts";
import { BaseClient } from "./baseClient.ts";
import { parseCookies } from "../utils/utils.ts";
/**
* Student Client
@ -18,7 +18,6 @@ export class StudentClient extends BaseClient {
private dateOfBirth = "";
/**
*
* @param studentCode ClassCharts student code
* @param dateOfBirth Student's date of birth
*/
@ -32,7 +31,7 @@ export class StudentClient extends BaseClient {
* Authenticates with ClassCharts
*/
async login(): Promise<void> {
if (!this.studentCode) throw new Error("Student Code not inputted");
if (!this.studentCode) throw new Error("Student Code not provided");
const formData = new URLSearchParams();
formData.append("_method", "POST");
formData.append("code", this.studentCode.toUpperCase());
@ -43,14 +42,11 @@ export class StudentClient extends BaseClient {
method: "POST",
body: formData,
redirect: "manual",
credentials: undefined,
});
if (request.status != 302 || !request.headers.get("set-cookie")) {
await request.body?.cancel(); // Make deno tests happy by closing the body, unsure whether this is needed for the actual library
throw new Error(
"Unauthenticated: ClassCharts returned an error: " +
request.status +
" " +
request.statusText
"Unauthenticated: ClassCharts didn't return authentication cookies"
);
}
const cookies = String(request.headers.get("set-cookie"));

View file

@ -0,0 +1,25 @@
import { assertRejects } from "https://deno.land/std@0.200.0/assert/mod.ts";
import { StudentClient } from "./studentClient.ts";
Deno.test("Throws when no student code is provided", async () => {
const client = new StudentClient("");
await assertRejects(
async () => {
await client.login();
},
Error,
"Student Code not provided"
);
});
Deno.test("Throws with invalid student code", async () => {
const client = new StudentClient("invalid");
await assertRejects(
async () => {
await client.login();
},
Error,
"Unauthenticated: ClassCharts didn't return authentication cookies"
);
});

View file

@ -1,13 +0,0 @@
export * from "./core/studentClient.js";
export * from "./core/parentClient.js";
export type {
Student,
ActivityPoint,
BehaviourTimelinePoint,
Homework,
Lesson,
Badge,
Announcement,
Detention,
AttendanceDate,
} from "./types.js";

View file

@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Helper type to define response from ClassCharts
* @internal
@ -157,7 +155,6 @@ export interface GetHomeworkOptions {
*
* Used to sort homeworks by when they are due or when they were issued
* @default "issue_date"
*
*/
displayDate?: DisplayDate;
/**
@ -193,16 +190,16 @@ export interface Homework {
status: {
id: number;
state: "not_completed" | "late" | "completed" | null;
mark: any | null;
mark: unknown | null;
mark_relative: number;
ticked: "yes" | "no";
allow_attachments: "yes" | "no";
first_seen_date: string;
last_seen_date: string;
attachments: Array<any>;
attachments: Array<unknown>;
has_feedback: boolean;
};
validated_links: Array<any>;
validated_links: Array<unknown>;
validated_attachments: Array<ValidatedHomeworkAttachment>;
}
export type HomeworksResponseData = Array<Homework>;
@ -357,7 +354,7 @@ export interface Announcement {
filename: string;
url: string;
}>;
for_pupils: Array<any>;
for_pupils: Array<unknown>;
comment_visibility: string;
allow_comments: "yes" | "no";
allow_reactions: "yes" | "no";
@ -366,7 +363,7 @@ export interface Announcement {
requires_consent: "yes" | "no";
can_change_consent: boolean;
consent: string | null;
pupil_consents: Array<any>;
pupil_consents: Array<unknown>;
}
// TODO: Update typings to include meta response. Currently not possible since I don't have access

View file

@ -16,7 +16,6 @@ export function parseCookies(input: string) {
return output;
}
function leftTrim(str: string) {
if (!str) return str;
export function leftTrim(str: string) {
return str.replace(/^\s+/g, "");
}

41
src/utils/utils_test.ts Normal file
View file

@ -0,0 +1,41 @@
import {
assertEquals,
assertExists,
} from "https://deno.land/std@0.200.0/assert/mod.ts";
import { parseCookies } from "./utils.ts";
Deno.test("Parses simple cookie", () => {
const cookie =
"testCookie=Hello%20world!; expires=Tue, 28-Nov-2023 10:28:45 GMT; Max-Age=7776000; path=/";
const parsed = parseCookies(cookie);
assertEquals(parsed.testCookie, "Hello world!");
});
Deno.test("Parses multiple cookies", () => {
const cookies =
"firstCookie=I'm%20the%20first%20cookie; expires=Tue, 28-Nov-2023 10:28:45 GMT; Max-Age=7776000; path=/, secondCookie=I'm%20the%20second%20cookie; expires=Tue, 28-Nov-2023 10:28:45 GMT; Max-Age=7776000; path=/";
const parsed = parseCookies(cookies);
assertEquals(parsed.firstCookie, "I'm the first cookie");
assertEquals(parsed.secondCookie, "I'm the second cookie");
});
Deno.test("Parses cookie with no value", () => {
const cookie =
"cookieWithNoValue=; expires=Tue, 28-Nov-2023 10:28:45 GMT; Max-Age=7776000; path=/";
const parsed = parseCookies(cookie);
assertExists(parsed.cookieWithNoValue);
assertEquals(parsed.cookieWithNoValue, "");
});
import { leftTrim } from "./utils.ts";
Deno.test("Trims left with spaces", () => {
const input = " Hello world!";
const output = leftTrim(input);
assertEquals(output, "Hello world!");
});
Deno.test("Trims left with no spaces", () => {
const input = "Hello world!";
const output = leftTrim(input);
assertEquals(output, "Hello world!");
});