From a96ad1e7b58974beaaf118144c4af8b17bc1a996 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Thu, 29 Jul 2021 23:58:34 +0300 Subject: [PATCH] basic Course crud & fix seeder --- .eslintrc.json | 7 +- app/Controllers/Http/CoursesController.ts | 181 ++++++++++++++++++++++ app/Models/Category.ts | 4 +- app/Models/Lesson.ts | 4 +- app/Validators/CreateCourseValidator.ts | 15 ++ app/Validators/UpdateCourseValidator.ts | 15 ++ database/factories/index.ts | 2 +- database/seeders/Course.ts | 10 +- start/routes.ts | 25 +++ 9 files changed, 248 insertions(+), 15 deletions(-) create mode 100644 app/Controllers/Http/CoursesController.ts create mode 100644 app/Validators/CreateCourseValidator.ts create mode 100644 app/Validators/UpdateCourseValidator.ts diff --git a/.eslintrc.json b/.eslintrc.json index 9980d45..c99dccd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,11 +3,7 @@ "es6": true, "node": true }, - "extends": [ - "airbnb-typescript/base", - "prettier", - "plugin:prettier/recommended" - ], + "extends": ["airbnb-typescript/base", "prettier", "plugin:prettier/recommended"], "globals": { "Atomics": "readonly", "SharedArrayBuffer": "readonly" @@ -17,7 +13,6 @@ "ecmaVersion": 2018, "sourceType": "module", "project": "./tsconfig.json", - "tsconfigRootDir": "server", "include": ["src/**/*.ts", "src/**/*.js", "test/**/*.ts"] }, "plugins": ["@typescript-eslint"], diff --git a/app/Controllers/Http/CoursesController.ts b/app/Controllers/Http/CoursesController.ts new file mode 100644 index 0000000..ef3a4fd --- /dev/null +++ b/app/Controllers/Http/CoursesController.ts @@ -0,0 +1,181 @@ +import { Exception } from '@adonisjs/core/build/standalone'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; +import Course from 'App/Models/Course'; +import User from 'App/Models/User'; +import CreateCourseValidator from 'App/Validators/CreateCourseValidator'; +import UpdateCourseValidator from 'App/Validators/UpdateCourseValidator'; + +export default class CoursesController { + private readonly Course: typeof Course; + + private readonly User: typeof User; + + constructor() { + this.Course = Course; + this.User = User; + } + + /** + * List of all Courses + * GET /courses + */ + public async index({ response }: HttpContextContract) { + const courses = await this.Course.query().preload('teacher').preload('category').preload('lessons'); + + return response.ok({ + message: 'Fetched all courses.', + data: courses, + }); + } + + /** + * 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(); + + return response.ok({ + message: `Fetched course with id: "${course.id}"`, + data: course, + }); + } + + /** + * 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(); + + return response.ok({ + message: `Fetched teacher by courseId: "${params.id}"`, + data: course.teacher, + }); + } + + /** + * 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(); + + return response.ok({ + message: `Fetched category by courseId: "${params.id}"`, + data: course.category, + }); + } + + /** + * 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(); + + return response.ok({ + message: `Fetched lessons by courseId: "${params.id}"`, + data: course.lessons, + }); + } + + /** + * Show Students of Course with "id" + */ + public async showStudents({ response, params }: HttpContextContract) {} + + /** + * Create new Course in a system + * POST /courses + */ + public async create({ response, request }: HttpContextContract) { + const payload = await request.validate(CreateCourseValidator); + + 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('teacher')) { + throw new Exception(`User with id "${teacher.id}" not teacher.`, 400, 'ERR_USER_NOT_TEACHER'); + } + + 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, + }); + } + + /** + * Delete Course by "id" + * DELETE /courses/:id + */ + async destroy({ response, params }: HttpContextContract) { + const course = await this.Course.query() + .preload('teacher') + .preload('category') + .preload('lessons') + .where('id', params.id) + .firstOrFail(); + + await course.delete(); + + return response.ok({ + message: `Course with id "${params.id}" has been deleted.`, + data: course, + }); + } + + /** + * Update Course by "id" + * PATCH /courses/:id + */ + async update({ response, request, params }: HttpContextContract) { + const payload = await request.validate(UpdateCourseValidator); + const course = await this.Course.findOrFail(params.id); + + /** + * Update course + */ + Object.keys(payload).forEach(k => { + if (payload[k] !== undefined) { + course[k] = payload[k]; + } + }); + + await course.save(); + await course.load('category'); + await course.load('lessons'); + await course.load('teacher'); + + return response.ok({ + message: `Course with id: "${course.id}" was successfully updated.`, + data: course, + }); + } +} diff --git a/app/Models/Category.ts b/app/Models/Category.ts index 181ee29..9254650 100644 --- a/app/Models/Category.ts +++ b/app/Models/Category.ts @@ -14,10 +14,10 @@ export default class Category extends BaseModel { @column() public description: string; - @column.dateTime({ autoCreate: true }) + @column.dateTime({ autoCreate: true, serializeAs: null }) public createdAt: DateTime; - @column.dateTime({ autoCreate: true, autoUpdate: true }) + @column.dateTime({ autoCreate: true, autoUpdate: true, serializeAs: null }) public updatedAt: DateTime; /** diff --git a/app/Models/Lesson.ts b/app/Models/Lesson.ts index 65b27cb..97c08ed 100644 --- a/app/Models/Lesson.ts +++ b/app/Models/Lesson.ts @@ -16,10 +16,10 @@ export default class Lesson extends BaseModel { @column() public description: string; - @column.dateTime({ autoCreate: true }) + @column.dateTime({ autoCreate: true, serializeAs: null }) public createdAt: DateTime; - @column.dateTime({ autoCreate: true, autoUpdate: true }) + @column.dateTime({ autoCreate: true, autoUpdate: true, serializeAs: null }) public updatedAt: DateTime; /** diff --git a/app/Validators/CreateCourseValidator.ts b/app/Validators/CreateCourseValidator.ts new file mode 100644 index 0000000..d1bb20d --- /dev/null +++ b/app/Validators/CreateCourseValidator.ts @@ -0,0 +1,15 @@ +import { schema, rules } from '@ioc:Adonis/Core/Validator'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +export default class CreateCourseValidator { + constructor(protected ctx: HttpContextContract) {} + + public schema = schema.create({ + title: schema.string({}, [rules.unique({ table: 'courses', column: 'title' })]), + description: schema.string(), + teacher_id: schema.string({}, [rules.exists({ table: 'users', column: 'id' })]), + category_id: schema.string({}, [rules.exists({ table: 'categories', column: 'id' })]), + }); + + public messages = {}; +} diff --git a/app/Validators/UpdateCourseValidator.ts b/app/Validators/UpdateCourseValidator.ts new file mode 100644 index 0000000..e77775d --- /dev/null +++ b/app/Validators/UpdateCourseValidator.ts @@ -0,0 +1,15 @@ +import { schema, rules } from '@ioc:Adonis/Core/Validator'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +export default class UpdateCourseValidator { + constructor(protected ctx: HttpContextContract) {} + + public schema = schema.create({ + title: schema.string.optional({}, [rules.unique({ table: 'courses', column: 'title' })]), + 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' })]), + }); + + public messages = {}; +} diff --git a/database/factories/index.ts b/database/factories/index.ts index 5dc64ed..8d13370 100644 --- a/database/factories/index.ts +++ b/database/factories/index.ts @@ -16,7 +16,7 @@ export const UserFactory = Factory.define(User, ({ faker }) => { first_name: faker.name.firstName(), last_name: faker.name.lastName(), login: `${faker.lorem.word(6)}${faker.datatype.number(10)}`, - password: faker.internet.password(6), + password: '12345', }; }) .relation('contacts', () => ContactFactory) diff --git a/database/seeders/Course.ts b/database/seeders/Course.ts index 79bc1b9..4912289 100644 --- a/database/seeders/Course.ts +++ b/database/seeders/Course.ts @@ -1,4 +1,5 @@ import BaseSeeder from '@ioc:Adonis/Lucid/Seeder'; +import Course from 'App/Models/Course'; import User from 'App/Models/User'; import { CourseFactory } from 'Database/factories'; @@ -9,11 +10,12 @@ export default class CourseSeeder extends BaseSeeder { this.User = User; const teacher = await this.User.query() - .preload('roles', q => q.where('slug', 'teacher')) + .preload('roles') + .whereHas('roles', q => q.where('slug', 'teacher')) .firstOrFail(); - await (await CourseFactory.with('category').with('lessons', 3).create()).related('teacher').associate(teacher); - await (await CourseFactory.with('category').with('lessons', 2).create()).related('teacher').associate(teacher); - await (await CourseFactory.with('category').with('lessons', 1).create()).related('teacher').associate(teacher); + await CourseFactory.with('category') + .with('lessons', 2) + .createMany(3, (course: Course) => course.related('teacher').associate(teacher)); } } diff --git a/start/routes.ts b/start/routes.ts index 9c7430f..ea944d5 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -47,9 +47,34 @@ Route.group(() => { Route.patch('me/password', 'MeController.changePassword') .middleware('role:admin,teacher,student') .as('changePassword'); + Route.put('me/contacts', 'MeController.updateContacts') .middleware('role:admin,teacher,student') .as('updateContacts'); + + /** + * Courses controller + */ + Route.get('courses', 'CoursesController.index').middleware('role:admin,teacher,student').as('showAllCourses'); + Route.post('courses', 'CoursesController.create').middleware('role:admin,teacher').as('createCourse'); + Route.get('courses/:id', 'CoursesController.show').middleware('role:admin,teacher,student').as('showCourseById'); + Route.delete('courses/:id', 'CoursesController.destroy').middleware('role:admin,teacher').as('deleteCourseById'); + Route.patch('courses/:id', 'CoursesController.update').middleware('role:admin,teacher').as('updateCourse'); + Route.get('courses/:id/teacher', 'CoursesController.showTeacher') + .middleware('role:admin,teacher,student') + .as('showCourseTeacher'); + + Route.get('courses/:id/category', 'CoursesController.showCategory') + .middleware('role:admin,teacher,student') + .as('showCourseCategory'); + + Route.get('courses/:id/lessons', 'CoursesController.showLessons') + .middleware('role:admin,teacher,student') + .as('showCourseLessons'); + + Route.get('courses/:id/students', 'CourseController.showStudents') + .middleware('role:admin,teacher,student') + .as('showCourseStudents'); }).middleware('auth'); }) .prefix('/api/v1')