From 1db5656d67e475b5a12e309732f91b4a15496f51 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 8 Aug 2021 23:25:57 +0300 Subject: [PATCH] feat: added course service --- .dockerignore | 6 + .../Http/Api/v1/CoursesController.ts | 155 ++++----- app/Helpers/RoleHelper.ts | 24 ++ app/Repositories/CategoryRepository.ts | 23 ++ app/Repositories/CourseRepository.ts | 129 ++++++++ app/Repositories/UserRepository.ts | 4 + app/Services/CourseService.ts | 299 ++++++++++++++++++ .../Course/CreateCourseValidator.ts | 2 +- .../Course/UpdateCourseValidator.ts | 2 +- server.ts | 8 +- 10 files changed, 561 insertions(+), 91 deletions(-) create mode 100644 .dockerignore create mode 100644 app/Helpers/RoleHelper.ts create mode 100644 app/Repositories/CategoryRepository.ts create mode 100644 app/Repositories/CourseRepository.ts create mode 100644 app/Services/CourseService.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cdd1e64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +build +node_modules +.git +*.log +.env +Dockerfile \ No newline at end of file diff --git a/app/Controllers/Http/Api/v1/CoursesController.ts b/app/Controllers/Http/Api/v1/CoursesController.ts index bea9c1e..1aeb352 100644 --- a/app/Controllers/Http/Api/v1/CoursesController.ts +++ b/app/Controllers/Http/Api/v1/CoursesController.ts @@ -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); diff --git a/app/Helpers/RoleHelper.ts b/app/Helpers/RoleHelper.ts new file mode 100644 index 0000000..5a0c56f --- /dev/null +++ b/app/Helpers/RoleHelper.ts @@ -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; + } +} diff --git a/app/Repositories/CategoryRepository.ts b/app/Repositories/CategoryRepository.ts new file mode 100644 index 0000000..c8283e6 --- /dev/null +++ b/app/Repositories/CategoryRepository.ts @@ -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 { + const category = await this.Category.query().where('id', id).first(); + return category; + } +} diff --git a/app/Repositories/CourseRepository.ts b/app/Repositories/CourseRepository.ts new file mode 100644 index 0000000..4c82240 --- /dev/null +++ b/app/Repositories/CourseRepository.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/app/Repositories/UserRepository.ts b/app/Repositories/UserRepository.ts index 4d502e3..60d39bb 100644 --- a/app/Repositories/UserRepository.ts +++ b/app/Repositories/UserRepository.ts @@ -99,6 +99,10 @@ export default class UserRepository { */ public async create(data: CreateUserValidator['schema']['props']): Promise { const createdUser = await this.User.create(data); + + await createdUser.load('contacts'); + await createdUser.load('roles'); + return createdUser; } diff --git a/app/Services/CourseService.ts b/app/Services/CourseService.ts new file mode 100644 index 0000000..773934e --- /dev/null +++ b/app/Services/CourseService.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + /** + * 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 { + 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); diff --git a/app/Validators/Course/CreateCourseValidator.ts b/app/Validators/Course/CreateCourseValidator.ts index d1bb20d..8da3021 100644 --- a/app/Validators/Course/CreateCourseValidator.ts +++ b/app/Validators/Course/CreateCourseValidator.ts @@ -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' })]), diff --git a/app/Validators/Course/UpdateCourseValidator.ts b/app/Validators/Course/UpdateCourseValidator.ts index e77775d..61cbe97 100644 --- a/app/Validators/Course/UpdateCourseValidator.ts +++ b/app/Validators/Course/UpdateCourseValidator.ts @@ -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' })]), diff --git a/server.ts b/server.ts index f334636..1df210a 100644 --- a/server.ts +++ b/server.ts @@ -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));