feat: added course service

This commit is contained in:
Sergey Yarkov 2021-08-08 23:25:57 +03:00
parent e6880e3e0e
commit 1db5656d67
10 changed files with 561 additions and 91 deletions

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
build
node_modules
.git
*.log
.env
Dockerfile

View File

@ -1,4 +1,4 @@
import { Exception } from '@adonisjs/core/build/standalone';
import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
/**
@ -11,6 +11,7 @@ import Role from 'App/Datatypes/Enums/RoleEnum';
*/
import Course from 'App/Models/Course';
import User from 'App/Models/User';
import CourseService from 'App/Services/CourseService';
/**
* Validators
@ -20,12 +21,19 @@ import CreateCourseValidator from 'App/Validators/Course/CreateCourseValidator';
import DelCourseFromUserValidator from 'App/Validators/Course/DelCourseFromUserValidator';
import UpdateCourseValidator from 'App/Validators/Course/UpdateCourseValidator';
export default class CoursesController {
import BaseController from '../../BaseController';
@inject()
export default class CoursesController extends BaseController {
private courseService: CourseService;
private readonly Course: typeof Course;
private readonly User: typeof User;
constructor() {
constructor(courseService: CourseService) {
super();
this.courseService = courseService;
this.Course = Course;
this.User = User;
}
@ -34,144 +42,113 @@ export default class CoursesController {
* List of all Courses
* GET /courses
*/
public async list({ response }: HttpContextContract) {
const courses = await this.Course.query().preload('teacher').preload('category').preload('lessons');
public async list(ctx: HttpContextContract) {
const result = await this.courseService.fetchCourses();
return response.ok({
message: 'Fetched all courses.',
data: courses,
});
if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code);
}
return this.sendResponse(ctx, result.data, result.message, result.status);
}
/**
* Show Course by "id"
* GET /courses/:id
*/
public async show({ response, params }: HttpContextContract) {
const course = await this.Course.query()
.preload('teacher')
.preload('category')
.preload('lessons')
.where('id', params.id)
.firstOrFail();
public async show(ctx: HttpContextContract) {
const result = await this.courseService.fetchCourse(ctx.params.id);
return response.ok({
message: `Fetched course with id: "${course.id}"`,
data: course,
});
if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code);
}
return this.sendResponse(ctx, result.data, result.message, result.status);
}
/**
* Show Teacher of Course with "id"
* GET /courses/:id/teacher
*/
public async showTeacher({ response, params }: HttpContextContract) {
const course = await this.Course.query()
.select('teacher_id')
.preload('teacher', user => user.preload('contacts').preload('roles'))
.where('id', params.id)
.firstOrFail();
public async showTeacher(ctx: HttpContextContract) {
const result = await this.courseService.fetchCourseTeacher(ctx.params.id);
return response.ok({
message: `Fetched teacher by courseId: "${params.id}"`,
data: course.teacher,
});
if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code);
}
return this.sendResponse(ctx, result.data, result.message, result.status);
}
/**
* Show Category of Course with "id"
* GET /courses/:id/category
*/
public async showCategory({ response, params }: HttpContextContract) {
const course = await this.Course.query()
.select('category_id')
.preload('category')
.where('id', params.id)
.firstOrFail();
public async showCategory(ctx: HttpContextContract) {
const result = await this.courseService.fetchCourseCategory(ctx.params.id);
return response.ok({
message: `Fetched category by courseId: "${params.id}"`,
data: course.category,
});
if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code);
}
return this.sendResponse(ctx, result.data, result.message, result.status);
}
/**
* Show Lessons of ourse with "id"
* GET /courses/:id/lessons
*/
public async showLessons({ response, params }: HttpContextContract) {
const course = await this.Course.query().select('id').preload('lessons').where('id', params.id).firstOrFail();
public async showLessons(ctx: HttpContextContract) {
const result = await this.courseService.fetchCourseLessons(ctx.params.id);
return response.ok({
message: `Fetched lessons by courseId: "${params.id}"`,
data: course.lessons,
});
if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code);
}
return this.sendResponse(ctx, result.data, result.message, result.status);
}
/**
* Show Students of Course with "id"
* GET /courses/:id/students
*/
public async showStudents({ response, params }: HttpContextContract) {
const course = await this.Course.query().preload('students').where('id', params.id).firstOrFail();
public async showStudents(ctx: HttpContextContract) {
const result = await this.courseService.fetchCourseStudents(ctx.params.id);
return response.ok({
message: `Fetched students from course with id: "${course.id}"`,
data: course.students,
});
if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code);
}
return this.sendResponse(ctx, result.data, result.message, result.status);
}
/**
* Create new Course in a system
* POST /courses
*/
public async create({ response, request }: HttpContextContract) {
const payload = await request.validate(CreateCourseValidator);
public async create(ctx: HttpContextContract) {
const payload = await ctx.request.validate(CreateCourseValidator);
const result = await this.courseService.createCourse(payload);
const teacher = await this.User.findOrFail(payload.teacher_id);
await teacher.load('roles');
/**
* Check user on role Teacher
*/
const roles = teacher.roles.map(r => r.slug);
if (!roles.includes(Role.TEACHER)) {
throw new Exception(`User with id "${teacher.id}" not teacher.`, 400, 'ERR_USER_NOT_TEACHER');
if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code);
}
const course = await this.Course.create({
title: payload.title,
description: payload.description,
teacher_id: teacher.id,
category_id: payload.category_id,
});
await course.save();
return response.created({
message: 'Course has been created',
data: course,
});
return this.sendResponse(ctx, result.data, result.message, result.status);
}
/**
* Delete Course by "id"
* DELETE /courses/:id
*/
public async delete({ response, params }: HttpContextContract) {
const course = await this.Course.query()
.preload('teacher')
.preload('category')
.preload('lessons')
.where('id', params.id)
.firstOrFail();
public async delete(ctx: HttpContextContract) {
const result = await this.courseService.deleteCourse(ctx.params.id);
await course.delete();
if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code);
}
return response.ok({
message: `Course with id "${params.id}" has been deleted.`,
data: course,
});
return this.sendResponse(ctx, result.data, result.message, result.status);
}
/**
@ -256,3 +233,5 @@ export default class CoursesController {
});
}
}
new Ioc().make(CoursesController);

24
app/Helpers/RoleHelper.ts Normal file
View File

@ -0,0 +1,24 @@
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
import type Role from 'App/Models/Role';
export default class RoleHelper {
/**
* If the user roles is not found in the array of roles
* then return false
*
* @param userRoles Roles of existing user
* @param roles Roles to check
* @returns boolean
*/
public static userHasRoles(userRoles: Role[], roles: RoleEnum[]): boolean {
for (let i = 0; i < roles.length; i += 1) {
if (!userRoles.map(r => r.slug).includes(roles[i])) {
/**
* Unable to find current role in user roles
*/
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,23 @@
/**
* Models
*/
import Category from 'App/Models/Category';
export default class CategoryRepository {
private Category: typeof Category;
constructor() {
this.Category = Category;
}
/**
* Get category by id
*
* @param id Category id
* @returns Caregory or null
*/
public async getById(id: string | number): Promise<Category | null> {
const category = await this.Category.query().where('id', id).first();
return category;
}
}

View File

@ -0,0 +1,129 @@
/**
* Models
*/
import Category from 'App/Models/Category';
import Course from 'App/Models/Course';
import Lesson from 'App/Models/Lesson';
import User from 'App/Models/User';
/**
* Validators
*/
import CreateCourseValidator from 'App/Validators/Course/CreateCourseValidator';
export default class CourseRepository {
private Course: typeof Course;
constructor() {
this.Course = Course;
}
/**
* Get all courses
*
* @returns List of courses
*/
public async getAll(): Promise<Course[]> {
const courses = await this.Course.query().preload('teacher').preload('category').preload('lessons');
return courses;
}
/**
* Get course by id
*
* @param id Course id
* @returns Course or null
*/
public async getById(id: string | number): Promise<Course | null> {
const course = await this.Course.query()
.preload('teacher')
.preload('category')
.preload('lessons')
.where('id', id)
.first();
return course;
}
/**
* Get teacher of course
*
* @param id Course id
* @returns User or null
*/
public async getTeacher(id: string | number): Promise<User | null> {
const course = await this.Course.query()
.select('teacher_id')
.preload('teacher', user => user.preload('contacts').preload('roles'))
.where('id', id)
.first();
return course && course.teacher;
}
/**
* Get category of course
*
* @param id Course id
* @returns Category or null
*/
public async getCategory(id: string | number): Promise<Category | null> {
const course = await this.Course.query().select('category_id').preload('category').where('id', id).first();
return course && course.category;
}
/**
* Get lessons of course
*
* @param id Course id
* @returns List of lessons or null
*/
public async getLessons(id: string | number): Promise<Lesson[] | null> {
const course = await this.Course.query().select('id').preload('lessons').where('id', id).first();
return course && course.lessons;
}
/**
* Get students of cours
*
* @param id Course id
* @returns List of users or null
*/
public async getStudents(id: string | number): Promise<User[] | null> {
const course = await this.Course.query().preload('students').where('id', id).first();
return course && course.students;
}
/**
* Create course
*
* @param data Data input
* @returns Created course
*/
public async create(data: CreateCourseValidator['schema']['props']): Promise<Course> {
const createdCourse = await this.Course.create(data);
return createdCourse;
}
/**
* Delete course
*
* @param id Course id
* @returns Deleted course
*/
public async delete(id: string | number): Promise<Course | null> {
const course = await this.Course.query()
.preload('teacher')
.preload('category')
.preload('lessons')
.where('id', id)
.first();
if (course) {
await course.delete();
return course;
}
return null;
}
}

View File

@ -99,6 +99,10 @@ export default class UserRepository {
*/
public async create(data: CreateUserValidator['schema']['props']): Promise<User> {
const createdUser = await this.User.create(data);
await createdUser.load('contacts');
await createdUser.load('roles');
return createdUser;
}

View File

@ -0,0 +1,299 @@
import { inject, Ioc } from '@adonisjs/core/build/standalone';
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
/**
* Datatypes
*/
import StatusCodeEnum from 'App/Datatypes/Enums/StatusCodeEnum';
import IResponse from 'App/Datatypes/Interfaces/IResponse';
import RoleHelper from 'App/Helpers/RoleHelper';
import CategoryRepository from 'App/Repositories/CategoryRepository';
/**
* Repositories
*/
import CourseRepository from 'App/Repositories/CourseRepository';
import UserRepository from 'App/Repositories/UserRepository';
import CreateCourseValidator from 'App/Validators/Course/CreateCourseValidator';
@inject()
export default class CourseService {
private courseRepository: CourseRepository;
private userRepository: UserRepository;
private categoryRepository: CategoryRepository;
constructor(
courseRepository: CourseRepository,
userRepository: UserRepository,
categoryRepository: CategoryRepository
) {
this.courseRepository = courseRepository;
this.userRepository = userRepository;
this.categoryRepository = categoryRepository;
}
/**
* Fetch list of courses
*
* @returns Response
*/
public async fetchCourses(): Promise<IResponse> {
const data = await this.courseRepository.getAll();
return {
success: true,
status: StatusCodeEnum.OK,
message: 'Fetched all courses.',
data,
};
}
/**
* Fetch one course by id
*
* @param id Course id
* @returns Response
*/
public async fetchCourse(id: string | number): Promise<IResponse> {
const data = await this.courseRepository.getById(id);
if (!data) {
return {
success: false,
status: StatusCodeEnum.NOT_FOUND,
message: 'Course not found.',
data: {},
error: {
code: 'E_NOT_FOUND',
},
};
}
return {
success: true,
status: StatusCodeEnum.OK,
message: 'Fetched course.',
data,
};
}
/**
* Fetch teacher by course id
*
* @param id Course id
* @returns Response
*/
public async fetchCourseTeacher(id: string | number): Promise<IResponse> {
const data = await this.courseRepository.getTeacher(id);
if (!data) {
return {
success: false,
status: StatusCodeEnum.NOT_FOUND,
message: 'Course not found.',
data: {},
error: {
code: 'E_NOT_FOUND',
},
};
}
return {
success: true,
status: StatusCodeEnum.OK,
message: 'Fetched course teacher.',
data,
};
}
/**
* Fetch category by course id
*
* @param id Course id
* @returns Response
*/
public async fetchCourseCategory(id: string | number): Promise<IResponse> {
const data = await this.courseRepository.getCategory(id);
if (!data) {
return {
success: false,
status: StatusCodeEnum.NOT_FOUND,
message: 'Course not found.',
data: {},
error: {
code: 'E_NOT_FOUND',
},
};
}
return {
success: true,
status: StatusCodeEnum.OK,
message: 'Fetched course category.',
data,
};
}
/**
* Fetch lessons by course id
*
* @param id Course id
* @returns Response
*/
public async fetchCourseLessons(id: string | number): Promise<IResponse> {
const data = await this.courseRepository.getLessons(id);
if (!data) {
return {
success: false,
status: StatusCodeEnum.NOT_FOUND,
message: 'Course not found.',
data: {},
error: {
code: 'E_NOT_FOUND',
},
};
}
return {
success: true,
status: StatusCodeEnum.OK,
message: 'Fetched course lessons.',
data,
};
}
/**
* Fetch students by course id
*
* @param id Course id
* @returns Response
*/
public async fetchCourseStudents(id: string | number): Promise<IResponse> {
const data = await this.courseRepository.getStudents(id);
if (!data) {
return {
success: false,
status: StatusCodeEnum.NOT_FOUND,
message: 'Course not found.',
data: {},
error: {
code: 'E_NOT_FOUND',
},
};
}
return {
success: true,
status: StatusCodeEnum.OK,
message: 'Fetched course students.',
data,
};
}
/**
* Create course
*
* @param data Data input
* @returns Response
*/
public async createCourse(data: CreateCourseValidator['schema']['props']): Promise<IResponse> {
/**
* Find course teacher
*/
const teacher = await this.userRepository.getById(data.teacher_id);
if (!teacher) {
return {
success: false,
status: StatusCodeEnum.NOT_FOUND,
message: 'Teacher not found.',
data: {},
error: {
code: 'E_NOT_FOUND',
},
};
}
/**
* Check teacher on role
*/
const isTeacher = RoleHelper.userHasRoles(teacher.roles, [RoleEnum.TEACHER]);
if (!isTeacher) {
return {
success: false,
status: StatusCodeEnum.BAD_REQUEST,
message: `User with id "${teacher.id}" not teacher.`,
data: {},
error: {
code: 'E_BAD_REQUEST',
},
};
}
/**
* Find course category
*/
const category = await this.categoryRepository.getById(data.category_id);
if (!category) {
return {
success: false,
status: StatusCodeEnum.NOT_FOUND,
message: 'Category not found.',
data: {},
error: {
code: 'E_NOT_FOUND',
},
};
}
/**
* Creating new course
*/
const course = await this.courseRepository.create(data);
return {
success: true,
status: StatusCodeEnum.CREATED,
message: 'Course created.',
data: course,
};
}
/**
* Delete course
*
* @param id Course id
* @returns Response
*/
public async deleteCourse(id: string | number): Promise<IResponse> {
const course = await this.courseRepository.delete(id);
if (!course) {
return {
success: false,
status: StatusCodeEnum.NOT_FOUND,
message: 'Course not found.',
data: {},
error: {
code: 'E_NOT_FOUND',
},
};
}
return {
success: true,
status: StatusCodeEnum.OK,
message: 'Course deleted.',
data: course,
};
}
}
new Ioc().make(CourseService);

View File

@ -5,7 +5,7 @@ export default class CreateCourseValidator {
constructor(protected ctx: HttpContextContract) {}
public schema = schema.create({
title: schema.string({}, [rules.unique({ table: 'courses', column: 'title' })]),
title: schema.string(),
description: schema.string(),
teacher_id: schema.string({}, [rules.exists({ table: 'users', column: 'id' })]),
category_id: schema.string({}, [rules.exists({ table: 'categories', column: 'id' })]),

View File

@ -5,7 +5,7 @@ export default class UpdateCourseValidator {
constructor(protected ctx: HttpContextContract) {}
public schema = schema.create({
title: schema.string.optional({}, [rules.unique({ table: 'courses', column: 'title' })]),
title: schema.string.optional(),
description: schema.string.optional(),
teacher_id: schema.string.optional({}, [rules.exists({ table: 'users', column: 'id' })]),
category_id: schema.string.optional({}, [rules.exists({ table: 'categories', column: 'id' })]),

View File

@ -16,4 +16,10 @@ import { Ignitor } from '@adonisjs/core/build/standalone';
sourceMapSupport.install({ handleUncaughtExceptions: false });
new Ignitor(__dirname).httpServer().start();
const server = new Ignitor(__dirname).httpServer();
server.start().catch(error => {
throw new Error(error);
});
process.on('SIGINT', () => server.kill(10));