-
michael.minelli authoredmichael.minelli authored
DojoBackendManager.ts 19.56 KiB
import axios, { AxiosError } from 'axios';
import ora from 'ora';
import ApiRoute from '../sharedByClients/types/Dojo/ApiRoute.js';
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig.js';
import Assignment from '../sharedByClients/models/Assignment.js';
import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse.js';
import Exercise from '../sharedByClients/models/Exercise.js';
import GitlabToken from '../shared/types/Gitlab/GitlabToken.js';
import User from '../sharedByClients/models/User.js';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import * as Gitlab from '@gitbeaker/rest';
import DojoBackendHelper from '../sharedByClients/helpers/Dojo/DojoBackendHelper.js';
import GitlabPipelineStatus from '../shared/types/Gitlab/GitlabPipelineStatus.js';
import Tag from '../sharedByClients/models/Tag';
import TagProposal from '../sharedByClients/models/TagProposal';
import Result from '../sharedByClients/models/Result';
class DojoBackendManager {
private handleApiError(error: unknown, spinner: ora.Ora, verbose: boolean, defaultErrorMessage?: string, otherErrorHandler?: (error: AxiosError, spinner: ora.Ora, verbose: boolean) => void) {
const unknownErrorMessage: string = 'unknown error';
if ( verbose ) {
if ( error instanceof AxiosError ) {
switch ( error.response?.data?.code ) {
case DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED:
spinner.fail(`The exercise does not belong to the assignment.`);
break;
case DojoStatusCode.ASSIGNMENT_PUBLISH_NO_PIPELINE:
spinner.fail(`No pipeline found for this assignment.`);
break;
case DojoStatusCode.ASSIGNMENT_PUBLISH_PIPELINE_FAILED:
spinner.fail((error.response?.data?.message as string | undefined) ?? `Last pipeline status is not "${ GitlabPipelineStatus.SUCCESS }".`);
break;
case DojoStatusCode.EXERCISE_CORRECTION_ALREADY_EXIST:
spinner.fail(`This exercise is already labelled as a correction. If you want to update it, please use the update command.`);
break;
case DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST:
spinner.fail(`The exercise is not labelled as a correction so it's not possible to update it.`);
break;
case DojoStatusCode.ASSIGNMENT_NAME_CONFLICT:
spinner.fail(`Assignment creation error: The assignment name already exists. Please choose another name.`);
break;
case DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR:
spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment on GitLab (internal error message: ${ error.response?.data?.description ?? unknownErrorMessage }). Please try again later or contact an administrator.`);
break;
case DojoStatusCode.ASSIGNMENT_CREATION_INTERNAL_ERROR:
spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment (internal error message: ${ error.response?.data?.description ?? unknownErrorMessage }). Please try again later or contact an administrator.`);
break;
case DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED:
spinner.fail(`The following users have reached the maximum number of exercise of this assignment : ${ ((error.response.data as DojoBackendResponse<Array<Gitlab.UserSchema>>).data).map(user => user.name).join(', ') }.`);
break;
case DojoStatusCode.EXERCISE_CREATION_INTERNAL_ERROR:
spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise (internal error message: ${ error.response?.data?.description ?? unknownErrorMessage }). Please try again later or contact an administrator.`);
break;
case DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR:
spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise on GitLab (internal error message: ${ error.response?.data?.description ?? unknownErrorMessage }). Please try again later or contact an administrator.`);
break;
case DojoStatusCode.GITLAB_TEMPLATE_NOT_FOUND:
spinner.fail(`Template not found or access denied. Please check the template ID or url. Also, please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`);
break;
case DojoStatusCode.GITLAB_TEMPLATE_ACCESS_UNAUTHORIZED:
spinner.fail(`Please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`);
break;
case DojoStatusCode.TAG_ONLY_ADMIN_CREATION:
spinner.fail(`Only admins can create non UserDefined tags.`);
break;
case DojoStatusCode.TAG_WITH_ACTIVE_LINK_DELETION:
spinner.fail(`This tag is used in resources (e.g. assignments). Please remove this tag from these resources before deleting it.`);
break;
case DojoStatusCode.TAG_PROPOSAL_ANSWER_NOT_PENDING:
spinner.fail(`This tag proposal have already been answered.`);
break;
default:
if ( otherErrorHandler ) {
otherErrorHandler(error, spinner, verbose);
} else {
spinner.fail(defaultErrorMessage ?? 'Unknown error');
}
break;
}
} else {
spinner.fail(defaultErrorMessage ?? 'Unknown error');
}
}
}
public async login(gitlabTokens: GitlabToken): Promise<User | undefined> {
try {
return (await axios.post<DojoBackendResponse<User>>(DojoBackendHelper.getApiUrl(ApiRoute.LOGIN), {
accessToken : gitlabTokens.access_token,
refreshToken: gitlabTokens.refresh_token
})).data.data;
} catch ( error ) {
return undefined;
}
}
public async refreshTokens(refreshToken: string): Promise<GitlabToken> {
return (await axios.post<DojoBackendResponse<GitlabToken>>(DojoBackendHelper.getApiUrl(ApiRoute.REFRESH_TOKENS), {
refreshToken: refreshToken
})).data.data;
}
public async getAssignment(nameOrUrl: string, getMyExercises: boolean = false): Promise<Assignment | undefined> {
try {
return (await axios.get<DojoBackendResponse<Assignment>>(DojoBackendHelper.getApiUrl(ApiRoute.ASSIGNMENT_GET, { assignmentNameOrUrl: nameOrUrl }), getMyExercises ? { params: { getMyExercises } } : {})).data.data;
} catch ( error ) {
return undefined;
}
}
public async checkTemplateAccess(idOrNamespace: string, verbose: boolean = true): Promise<boolean> {
const spinner: ora.Ora = ora('Checking template access');
if ( verbose ) {
spinner.start();
}
try {
await axios.get(DojoBackendHelper.getApiUrl(ApiRoute.GITLAB_CHECK_TEMPLATE_ACCESS, { gitlabProjectId: idOrNamespace }));
if ( verbose ) {
spinner.succeed('Template access granted');
}
return true;
} catch ( error ) {
this.handleApiError(error, spinner, verbose, `Template error: ${ error }`);
return false;
}
}
public async createAssignment(name: string, members: Array<Gitlab.UserSchema>, templateIdOrNamespace: string | null, verbose: boolean = true): Promise<Assignment> {
const spinner: ora.Ora = ora('Creating assignment...');
if ( verbose ) {
spinner.start();
}
try {
const response = await axios.post<DojoBackendResponse<Assignment>>(DojoBackendHelper.getApiUrl(ApiRoute.ASSIGNMENT_CREATE), Object.assign({
name : name,
members: JSON.stringify(members)
}, templateIdOrNamespace ? { template: templateIdOrNamespace } : {}));
if ( verbose ) {
spinner.succeed(`Assignment successfully created`);
}
return response.data.data;
} catch ( error ) {
this.handleApiError(error, spinner, verbose, `Assignment creation error: unknown error`);
throw error;
}
}
public async createExercise(assignmentName: string, members: Array<Gitlab.UserSchema>, verbose: boolean = true): Promise<Exercise> {
const spinner: ora.Ora = ora('Creating exercise...');
if ( verbose ) {
spinner.start();
}
try {
const response = await axios.post<DojoBackendResponse<Exercise>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_CREATE, { assignmentNameOrUrl: assignmentName }), { members: JSON.stringify(members) });
if ( verbose ) {
spinner.succeed(`Exercise successfully created`);
}
return response.data.data;
} catch ( error ) {
this.handleApiError(error, spinner, verbose, `Exercise creation error: unknown error`);
throw error;
}
}
public async changeAssignmentPublishedStatus(assignment: Assignment, publish: boolean, verbose: boolean = true) {
const spinner: ora.Ora = ora('Changing published status...');
if ( verbose ) {
spinner.start();
}
try {
await axios.patch<DojoBackendResponse<null>>(DojoBackendHelper.getApiUrl(publish ? ApiRoute.ASSIGNMENT_PUBLISH : ApiRoute.ASSIGNMENT_UNPUBLISH, { assignmentNameOrUrl: assignment.name }), {});
if ( verbose ) {
spinner.succeed(`Assignment ${ assignment.name } successfully ${ publish ? 'published' : 'unpublished' }`);
}
return;
} catch ( error ) {
this.handleApiError(error, spinner, verbose, `Assignment visibility change error: ${ error }`);
throw error;
}
}
public async linkUpdateCorrection(exerciseIdOrUrl: string, assignment: Assignment, commit: string | undefined, description: string | undefined, isUpdate: boolean, verbose: boolean = true): Promise<boolean> {
const spinner: ora.Ora = ora(`${ isUpdate ? 'Updating' : 'Linking' } correction`);
if ( verbose ) {
spinner.start();
}
try {
const axiosFunction = isUpdate ? axios.patch.bind(axios) : axios.post.bind(axios);
const route = isUpdate ? ApiRoute.ASSIGNMENT_CORRECTION_UPDATE_DELETE : ApiRoute.ASSIGNMENT_CORRECTION_LINK;
await axiosFunction(DojoBackendHelper.getApiUrl(route, {
assignmentNameOrUrl: assignment.name,
exerciseIdOrUrl : exerciseIdOrUrl
}), {
exerciseIdOrUrl: exerciseIdOrUrl,
commit : commit,
description : description
});
if ( verbose ) {
spinner.succeed(`Correction ${ isUpdate ? 'updated' : 'linked' }`);
}
return true;
} catch ( error ) {
this.handleApiError(error, spinner, verbose, `Correction ${ isUpdate ? 'update' : 'link' } error: ${ error }`);
return false;
}
}
public async unlinkCorrection(exerciseIdOrUrl: string, assignment: Assignment, verbose: boolean = true): Promise<boolean> {
const spinner: ora.Ora = ora(`Unlinking correction`);
if ( verbose ) {
spinner.start();
}
try {
await axios.delete(DojoBackendHelper.getApiUrl(ApiRoute.ASSIGNMENT_CORRECTION_UPDATE_DELETE, {
assignmentNameOrUrl: assignment.name,
exerciseIdOrUrl : exerciseIdOrUrl
}));
if ( verbose ) {
spinner.succeed(`Correction unlinked`);
}
return true;
} catch ( error ) {
this.handleApiError(error, spinner, verbose, `Correction unlink error: ${ error }`);
return false;
}
}
public async createTag(name: string, type: string, verbose: boolean = true): Promise<Tag | undefined> {
const spinner: ora.Ora = ora('Creating tag...');
if ( verbose ) {
spinner.start();
}
try {
const response = await axios.post<DojoBackendResponse<Tag>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_CREATE), {
name: name,
type: type
});
if ( verbose ) {
spinner.succeed(`Tag successfully created`);
}
return response.data.data;
} catch ( error ) {
this.handleApiError(error, spinner, verbose, `Tag creation error: ${ error }`);
return undefined;
}
}
public async deleteTag(name: string, verbose: boolean = true): Promise<boolean> {
const spinner: ora.Ora = ora('Deleting tag...');
if ( verbose ) {
spinner.start();
}
try {
await axios.delete<DojoBackendResponse<Tag>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_DELETE, { tagName: name }));
if ( verbose ) {
spinner.succeed(`Tag successfully deleted`);
}
return true;
} catch ( error ) {
this.handleApiError(error, spinner, verbose, `Tag deletion error: ${ error }`);
return false;
}
}
public async getTagProposals(state: string | undefined): Promise<Array<TagProposal> | undefined> {
try {
return (await axios.get<DojoBackendResponse<Array<TagProposal>>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_PROPOSAL_GET_CREATE), { params: { stateFilter: state } })).data.data;
} catch ( error ) {
return undefined;
}
}
public async createTagProposal(name: string, type: string, verbose: boolean = true): Promise<TagProposal | undefined> {
const spinner: ora.Ora = ora('Creating tag...');
if ( verbose ) {
spinner.start();
}
try {
const response = await axios.post<DojoBackendResponse<TagProposal>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_PROPOSAL_GET_CREATE), {
name: name,
type: type
});
if ( verbose ) {
spinner.succeed(`Tag proposal successfully created`);
}
return response.data.data;
} catch ( error ) {
this.handleApiError(error, spinner, verbose, `Tag proposal creation error: ${ error }`);
return undefined;
}
}
public async answerTagProposal(tagProposalName: string, state: 'Approved' | 'Declined', details: string, verbose: boolean = true): Promise<boolean> {
const spinner: ora.Ora = ora('Answering tag proposal...');
if ( verbose ) {
spinner.start();
}
try {
await axios.patch<DojoBackendResponse<TagProposal>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_PROPOSAL_UPDATE, { tagName: tagProposalName }), {
state : state,
details: details
});
if ( verbose ) {
spinner.succeed(`Tag proposal ${ state.toLowerCase() } with success`);
}
return true;
} catch ( error ) {
this.handleApiError(error, spinner, verbose, `Tag proposal answer error: ${ error }`);
return false;
}
}
public async getUserExercises(): Promise<Array<Exercise> | undefined> {
try {
const response = await axios.get<DojoBackendResponse<Array<Exercise>>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_LIST));
return response.data.data;
} catch ( error ) {
console.error('Error fetching user exercises:', error);
return undefined;
}
}
public async getExerciseDetails(exerciseIdOrUrl: string): Promise<Exercise | undefined> {
try {
const response = await axios.get<Exercise>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_DETAILS_GET, {
exerciseIdOrUrl: exerciseIdOrUrl
}));
return response.data;
} catch ( error ) {
console.error('Error fetching exercise details:', error);
return undefined;
}
}
public async deleteExercise(exerciseIdOrUrl: string, verbose: boolean = true): Promise<void> {
const spinner: ora.Ora = ora('Deleting exercise...');
if ( verbose ) {
spinner.start();
}
try {
await axios.delete<DojoBackendResponse<Exercise>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_GET_DELETE, {
exerciseIdOrUrl: exerciseIdOrUrl
}));
if ( verbose ) {
spinner.succeed(`Exercise deleted with success`);
}
} catch ( error ) {
this.handleApiError(error, spinner, verbose, `Exercise deleting error: ${ error }`);
throw error;
}
}
public async getExerciseMembers(exerciseIdOrUrl: string): Promise<Array<User>> {
return (await axios.get<DojoBackendResponse<Array<User>>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_MEMBERS_GET, {
exerciseIdOrUrl: exerciseIdOrUrl
}))).data.data;
}
public async getExerciseResults(exerciseIdOrUrl: string): Promise<Array<Result>> {
try {
const response = await axios.get(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_RESULTS, {
exerciseIdOrUrl: exerciseIdOrUrl
}));
return response.data as Array<Result>;
} catch ( error ) {
console.error('Error fetching exercise results:', error);
return [];
}
}
public async getUsers(roleFilter?: string): Promise<Array<User> | undefined> {
try {
const response = await axios.get<DojoBackendResponse<Array<User>>>(DojoBackendHelper.getApiUrl(ApiRoute.USER_LIST), { params: roleFilter ? { roleFilter: roleFilter } : {} });
return response.data.data;
} catch ( error ) {
console.error('Error fetching professors:', error);
return undefined;
}
}
public async getTeachers(): Promise<Array<User> | undefined> {
return this.getUsers('teacher');
}
}
export default new DojoBackendManager();