Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion backend/src/application/resources/joinRequestResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as JoinRequestSchemas from "../schemas/joinRequestSchemas";
* - DELETE /requests/:id - Delete invite
* - POST /requests - Create new invite
* - GET /users/:idParent/requests - Get all pending invites for a user
* - GET /classes/:idParent/requests - Get all pending invites for a class
*/

/* ************* Extractors ************* */
Expand All @@ -23,6 +24,7 @@ const extractors = {
deleteJoinRequest: deps.createZodParamsExtractor(JoinRequestSchemas.deleteJoinRequestSchema),
createJoinRequest: deps.createZodParamsExtractor(JoinRequestSchemas.createJoinRequestSchema),
getUserJoinRequests: deps.createZodParamsExtractor(JoinRequestSchemas.getUserJoinRequestsSchema),
getClassJoinRequests: deps.createZodParamsExtractor(JoinRequestSchemas.getClassJoinRequestsSchema),
};

/* ************* Controller ************* */
Expand All @@ -34,8 +36,9 @@ export class JoinRequestController extends deps.Controller {
remove: JoinRequestServices.DeleteJoinRequest,
create: JoinRequestServices.CreateJoinRequest,
getUserJoinRequests: JoinRequestServices.GetUserJoinRequests,
getClassJoinRequests: JoinRequestServices.GetClassJoinRequests,
) {
super({ get, update, remove, create, getUserJoinRequests });
super({ get, update, remove, create, getUserJoinRequests, getClassJoinRequests });
}
}

Expand Down Expand Up @@ -93,6 +96,15 @@ export function joinRequestRoutes(
handler: (req, data) => controller.getChildren(req, data, controller.services.getUserJoinRequests),
middleware,
},
{
app,
method: deps.HttpMethod.GET,
urlPattern: "/classes/:idParent/requests",
controller,
extractor: extractors.getClassJoinRequests,
handler: (req, data) => controller.getChildren(req, data, controller.services.getClassJoinRequests),
middleware,
},
],
deps.DEFAULT_METHOD_MAP,
);
Expand Down
4 changes: 4 additions & 0 deletions backend/src/application/schemas/joinRequestSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export const getUserJoinRequestsSchema = z.object({
idParent: z.string(),
});

export const getClassJoinRequestsSchema = z.object({
idParent: z.string(),
});

export const getJoinRequestSchema = z.object({
id: z.string(),
});
1 change: 1 addition & 0 deletions backend/src/config/controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const controllers = {
services.joinRequest.remove,
services.joinRequest.create,
services.joinRequest.getUserJoinRequests,
services.joinRequest.getClassJoinRequests,
),
message: new Resources.MessageController(
services.message.get,
Expand Down
12 changes: 12 additions & 0 deletions backend/src/config/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,15 @@ export class DatabaseError extends Error {
this.message = message;
}
}

/**
* An error that is thrown when trying to use an expired join code.
*/
export class ExpiredError extends Error {
message: string;

constructor(message: string) {
super(message);
this.message = message;
}
}
1 change: 1 addition & 0 deletions backend/src/config/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const services = {
remove: new JoinRequestServices.DeleteJoinRequest(repos.joinRequest),
create: new JoinRequestServices.CreateJoinRequest(repos.joinRequest, repos.class),
getUserJoinRequests: new JoinRequestServices.GetUserJoinRequests(repos.joinRequest),
getClassJoinRequests: new JoinRequestServices.GetClassJoinRequests(repos.joinRequest),
},
message: {
get: new MessageServices.GetMessage(repos.message),
Expand Down
23 changes: 23 additions & 0 deletions backend/src/core/repositories/joinCodeRepositoryInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { AbstractRepository } from "./abstractRepository";

/**
* Interface for join code repositories.
* Allows CRUD operations on join code entities.
*/
export abstract class IJoinCodeRepository extends AbstractRepository {
/**
* Get an active join code for a class.
* If no active join code exists yet,
* One is created and returned
* @param classId The id of the class.
* @returns A promise that resolves to the alphanumeric 6 letter join code.
*/
public abstract getByClassId(classId: string): Promise<string>;

/**
* Marks a join code as expired.
* No students will be able to use this join code after the code is expired.
* @param code The actual alphanumerical code as a string.
*/
public abstract setExpired(code: string): Promise<void>;
}
13 changes: 12 additions & 1 deletion backend/src/core/repositories/joinRequestRepositoryInterface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AbstractRepository } from "./abstractRepository";
import { JoinRequest } from "../entities/joinRequest";
import { JoinRequest, JoinRequestType } from "../entities/joinRequest";

/**
* Interface for join request repositories.
Expand All @@ -16,6 +16,17 @@ export abstract class IJoinRequestRepository extends AbstractRepository {
*/
public abstract create(joinRequest: JoinRequest): Promise<JoinRequest>;

/**
* Creates a join request for a user using a class code.
* @param code The actual alphanumerical code as a string.
* @param userId The id of the user that wants to join.
* @param type The type of the user. The role that the user will get in the class (student or teacher)
* @throws EntityNotFoundError when the code is not found.
* @throws ExpiredError when the code is expired.
* @returns A promise that resolves to a new join request for the class the code is for.
*/
public abstract createUsingCode(code: string, userId: string, type: JoinRequestType): Promise<JoinRequest>;

/**
* Get a join request by its id.
* @param id The id of the join request
Expand Down
36 changes: 35 additions & 1 deletion backend/src/core/services/joinRequest/getJoinRequest.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { z } from "zod";
import { JoinRequestService } from "./joinRequestService";
import { getJoinRequestSchema, getUserJoinRequestsSchema } from "../../../application/schemas";
import {
getJoinRequestSchema,
getUserJoinRequestsSchema,
getClassJoinRequestsSchema,
} from "../../../application/schemas";
import { JoinRequest } from "../../entities/joinRequest";
import { tryRepoEntityOperation } from "../../helpers";

/**
* @description paramaters to get all joinRequests of a class
*
* @param _classId The id of the class.
*/
export type GetClassJoinRequestsInput = z.infer<typeof getClassJoinRequestsSchema>;

/**
* @description paramaters to get all joinRequests of a user
*
Expand All @@ -17,6 +28,29 @@ export type GetUserJoinRequestsInput = z.infer<typeof getUserJoinRequestsSchema>
*/
export type GetJoinRequestInput = z.infer<typeof getJoinRequestSchema>;

/**
* @description class representing service to get all joinRequests of a class
*
*/
export class GetClassJoinRequests extends JoinRequestService<GetClassJoinRequestsInput> {
/**
* Executes the class join-request get process.
* @param input - The input data for getting class join-request, validated by getClassJoinCodesSchema.
* @returns A promise resolving to an object with a list of join-request.
* @throws {ApiError} If the class with the given id is not found.
*/
async execute(input: GetClassJoinRequestsInput): Promise<object> {
// Get all requests for user
const requests: JoinRequest[] = await tryRepoEntityOperation(
this.joinRequestRepository.getByClassId(input.idParent),
"Class",
input.idParent,
true,
);
return { requests: requests.map(request => request.id) };
}
}

/**
* @description class representing service to get all joinRequests of a user
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Entity, PrimaryColumn, Column, ManyToOne, CreateDateColumn } from "typeorm";
import { BeforeInsert } from "typeorm/decorator/listeners/BeforeInsert"; // Important to specify the exact path here
import { ClassTypeORM } from "./classTypeorm";

@Entity()
export class JoinCodeTypeORM {
@BeforeInsert()
generateShortId() {
// Convert a random number to its base 36 representation. (uses 0-9 and a-z)
const base36String = Math.random().toString(36);
// Skip the "0.", use the decimals to make the id
this.code = base36String.substring(2, 8);
}

@PrimaryColumn({ unique: true })
code!: string; // a short id of length 6 with alphanumeric characters

@ManyToOne(() => ClassTypeORM, { cascade: true, onDelete: "CASCADE" })
class!: ClassTypeORM;

@CreateDateColumn() // Auto-set on creation
createdAt!: Date;

@Column({ type: "boolean" })
isExpired!: boolean;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { DatasourceTypeORM } from "./datasourceTypeORM";
import { EntityNotFoundError } from "../../../../../config/error";
import { EntityNotFoundError, ExpiredError } from "../../../../../config/error";
import { Class } from "../../../../../core/entities/class";
import { JoinRequestType } from "../../../../../core/entities/joinRequest";
import { ClassTypeORM } from "../../data_models/classTypeorm";
import { JoinCodeTypeORM } from "../../data_models/joinCodeTypeorm";
import { StudentOfClassTypeORM } from "../../data_models/studentOfClassTypeorm";
import { StudentTypeORM } from "../../data_models/studentTypeorm";
import { TeacherOfClassTypeORM } from "../../data_models/teacherOfClassTypeorm";
Expand Down Expand Up @@ -86,6 +87,36 @@ export class DatasourceClassTypeORM extends DatasourceTypeORM {
return null; // No result
}

/**
* Get the class that an active join code belongs to.
* @param code The actual alphanumerical code.
* @throws EntityNotFoundError when the code is not found.
* @throws ExpiredError when the code is expired.
* @returns A promise that resolves to the class for the given code.
*/
public async getClassByActiveCode(code: string): Promise<Class> {
const datasource = await DatasourceTypeORM.datasourcePromise;

const joinCodeModel: JoinCodeTypeORM | null = await datasource
.getRepository(JoinCodeTypeORM)
.findOne({ where: { code: code } });

if (!joinCodeModel) {
throw new EntityNotFoundError(`Join code ${code} not found.`);
}

if (joinCodeModel.isExpired) {
throw new ExpiredError(`The join code ${code} is expired.`);
}

const classModel: ClassTypeORM = joinCodeModel.class;

const _class: Class | null = await this.getClassById(classModel.id);

// The class is definitely in the database, because the code for that class was found
return _class!;
}

public async getAllClasses(): Promise<Class[]> {
const datasource = await DatasourceTypeORM.datasourcePromise;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { DatasourceTypeORM } from "./datasourceTypeORM";
import { EntityNotFoundError } from "../../../../../config/error";
import { ClassTypeORM } from "../../data_models/classTypeorm";
import { JoinCodeTypeORM } from "../../data_models/joinCodeTypeorm";

export class DatasourceJoinCodeTypeORM extends DatasourceTypeORM {
public async getActiveCodeByClassId(classId: string): Promise<string | null> {
const datasource = await DatasourceTypeORM.datasourcePromise;

// Find an active join code for the class
const joinCodeModel: JoinCodeTypeORM | null = await datasource
.getRepository(JoinCodeTypeORM)
.findOne({ where: { class: { id: classId }, isExpired: false } });

if (joinCodeModel !== null) {
return joinCodeModel.code;
}
// There are no active codes
return null; // No result
}

/*
Creates a new join code for a class and returns the code that was created as an alphanumerical string of length 6
*/
public async createForClass(classId: string): Promise<string> {
const datasource = await DatasourceTypeORM.datasourcePromise;

// Find the relevant class
const classModel: ClassTypeORM | null = await datasource
.getRepository(ClassTypeORM)
.findOne({ where: { id: classId } });

if (!classModel) {
throw new EntityNotFoundError(`Class with id ${classId} not found`);
}

// Create a new join code
const joinCodeModel: JoinCodeTypeORM = datasource.getRepository(JoinCodeTypeORM).create({
class: classModel,
isExpired: false,
});

// Return the code, which is the id of the join code model
return joinCodeModel.code;
}

/*
Marks the join code as being expired. No one can use expired codes to join a class
*/
public async setExpired(code: string) {
const datasource = await DatasourceTypeORM.datasourcePromise;
const joinCodeRepository = datasource.getRepository(JoinCodeTypeORM);

// Find the code
const joinCodeModel: JoinCodeTypeORM | null = await joinCodeRepository.findOne({ where: { code: code } });

if (!joinCodeModel) {
throw new EntityNotFoundError(`Join code ${code} not found`);
}

// Set expired to true
joinCodeModel.isExpired = true;

// Update the database
await joinCodeRepository.update(code, joinCodeModel);
}

public async delete(code: string) {
const datasource = await DatasourceTypeORM.datasourcePromise;
await datasource.getRepository(JoinCodeTypeORM).delete(code);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DatasourceTypeORMConnectionSettings } from "./datasourceTypeORMConnecti
import { AssignmentTypeORM } from "../../data_models/assignmentTypeorm";
import { ClassTypeORM } from "../../data_models/classTypeorm";
import { GroupTypeORM } from "../../data_models/groupTypeorm";
import { JoinCodeTypeORM } from "../../data_models/joinCodeTypeorm";
import { JoinRequestTypeORM } from "../../data_models/joinRequestTypeorm";
import { MessageTypeORM } from "../../data_models/messageTypeorm";
import { QuestionThreadTypeORM } from "../../data_models/questionThreadTypeorm";
Expand Down Expand Up @@ -50,6 +51,7 @@ export class DatasourceTypeORMConnectionSettingsFactory {
UserTypeORM,
StudentTypeORM,
TeacherTypeORM,
JoinCodeTypeORM,
JoinRequestTypeORM,
ClassTypeORM,
TeacherOfClassTypeORM,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IJoinCodeRepository } from "../../core/repositories/joinCodeRepositoryInterface";
import { DatasourceJoinCodeTypeORM } from "../database/data/data_sources/typeorm/datasourceJoinCodeTypeORM";

export class JoinCodeRepositoryTypeORM extends IJoinCodeRepository {
private datasourceJoinCode: DatasourceJoinCodeTypeORM;

public constructor() {
super();
this.datasourceJoinCode = new DatasourceJoinCodeTypeORM();
}

public async getByClassId(classId: string): Promise<string> {
const code: string | null = await this.datasourceJoinCode.getActiveCodeByClassId(classId);
if (code) {
return code!;
}
return await this.datasourceJoinCode.createForClass(classId);
}

public async setExpired(code: string): Promise<void> {
return this.datasourceJoinCode.setExpired(code);
}
}
Loading