diff --git a/app/Controllers/Http/Api/v1/ImagesController.ts b/app/Controllers/Http/Api/v1/ImagesController.ts new file mode 100644 index 0000000..db6b85d --- /dev/null +++ b/app/Controllers/Http/Api/v1/ImagesController.ts @@ -0,0 +1,58 @@ +import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; +import ImageService from 'App/Services/ImageService'; +import CreateImageValidator from 'App/Validators/Image/CreateImageValidator'; +import BaseController from '../../BaseController'; + +@inject() +export default class ImagesController extends BaseController { + private imageService: ImageService; + + constructor(imageService: ImageService) { + super(); + this.imageService = imageService; + } + + public async list(ctx: HttpContextContract) { + const result = await this.imageService.fetchImages(); + + 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); + } + + public async show(ctx: HttpContextContract) { + const result = await this.imageService.fetchImage(ctx.params.id); + + 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); + } + + public async create(ctx: HttpContextContract) { + const payload = await ctx.request.validate(CreateImageValidator); + const result = await this.imageService.createImage(payload); + + 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); + } + + public async delete(ctx: HttpContextContract) { + const result = await this.imageService.deleteImage(ctx.params.id); + + 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); + } +} + +new Ioc().make(ImagesController); diff --git a/app/Repositories/CourseRepository.ts b/app/Repositories/CourseRepository.ts index e2b4d7a..685be29 100644 --- a/app/Repositories/CourseRepository.ts +++ b/app/Repositories/CourseRepository.ts @@ -49,7 +49,8 @@ export default class CourseRepository { .preload('teacher') .preload('category') .preload('lessons') - .where('status', CourseStatusEnum.PUBLISHED); + .preload('image'); + // .where('status', CourseStatusEnum.PUBLISHED); return courses; } @@ -65,8 +66,9 @@ export default class CourseRepository { .preload('category') .preload('lessons') .preload('students') + .preload('image') .where('id', id) - .where('status', CourseStatusEnum.PUBLISHED) + // .where('status', CourseStatusEnum.PUBLISHED) .first(); return course; @@ -154,6 +156,7 @@ export default class CourseRepository { * Create new course */ const course = await this.Course.create({ + bg_image_id: data.bg_image_id, title: data.title, description: data.description, status: CourseStatusEnum.DRAFT, // make course DRAFT before adding lessons diff --git a/app/Repositories/ImageRepository.ts b/app/Repositories/ImageRepository.ts index d416ec2..6896a40 100644 --- a/app/Repositories/ImageRepository.ts +++ b/app/Repositories/ImageRepository.ts @@ -9,6 +9,16 @@ export default class ImageRepository { this.Image = Image; } + public async getById(id: number): Promise { + const image = await this.Image.query().where('id', id).first(); + return image; + } + + public async getAll(): Promise { + const images = await this.Image.query(); + return images; + } + /** * Create image in cloudinary cloud * @@ -28,13 +38,13 @@ export default class ImageRepository { * @param id Image id * @returns Result or null */ - public async deleteFormCloudinaryCloud(id: number): Promise { + public async deleteFormCloudinaryCloud(id: number): Promise { const image = await this.Image.query().where('id', id).first(); if (image) { - const result = await CloudinaryService.destroy(image.name); + await CloudinaryService.destroy(image.name); await image.delete(); - return result; + return image; } return null; diff --git a/app/Services/CourseService.ts b/app/Services/CourseService.ts index 07af72d..df0e0af 100644 --- a/app/Services/CourseService.ts +++ b/app/Services/CourseService.ts @@ -1,4 +1,3 @@ -import { cuid } from '@ioc:Adonis/Core/Helpers'; import { inject, Ioc } from '@adonisjs/core/build/standalone'; import { AuthContract } from '@ioc:Adonis/Addons/Auth'; @@ -283,12 +282,6 @@ export default class CourseService { /* Create course */ const course = await this.courseRepository.create(data); - /* Create image if provided */ - if (data.background_image) { - const bgImage = await this.imageRepository.createInCloudinaryCloud(data.background_image, cuid()); - await course.related('image').associate(bgImage); - } - return { success: true, status: StatusCodeEnum.CREATED, diff --git a/app/Services/ImageService.ts b/app/Services/ImageService.ts new file mode 100644 index 0000000..f18deb0 --- /dev/null +++ b/app/Services/ImageService.ts @@ -0,0 +1,85 @@ +import { inject, Ioc } from '@adonisjs/core/build/standalone'; +import { cuid } from '@ioc:Adonis/Core/Helpers'; +import StatusCodeEnum from 'App/Datatypes/Enums/StatusCodeEnum'; +import IResponse from 'App/Datatypes/Interfaces/IResponse'; +import ImageRepository from 'App/Repositories/ImageRepository'; +import CreateImageValidator from 'App/Validators/Image/CreateImageValidator'; + +@inject() +export default class ImageService { + private imageRepository: ImageRepository; + + constructor(imageRepository: ImageRepository) { + this.imageRepository = imageRepository; + } + + public async fetchImages(): Promise { + const images = await this.imageRepository.getAll(); + + return { + success: true, + message: 'Fetched all images.', + status: StatusCodeEnum.OK, + data: images, + }; + } + + public async fetchImage(id: number): Promise { + const image = await this.imageRepository.getById(id); + + if (!image) { + return { + success: false, + status: StatusCodeEnum.NOT_FOUND, + message: 'Image not found.', + data: {}, + error: { + code: 'E_NOT_FOUND', + }, + }; + } + + return { + success: true, + message: 'Fetched image.', + status: StatusCodeEnum.OK, + data: image, + }; + } + + public async createImage(data: CreateImageValidator['schema']['props']): Promise { + const image = await this.imageRepository.createInCloudinaryCloud(data.image, cuid()); + + return { + success: true, + message: 'Image created.', + status: StatusCodeEnum.OK, + data: image, + }; + } + + public async deleteImage(id: number): Promise { + const image = await this.imageRepository.deleteFormCloudinaryCloud(id); + + if (!image) { + return { + success: false, + status: StatusCodeEnum.NOT_FOUND, + message: 'Image not found.', + data: {}, + error: { + code: 'E_NOT_FOUND', + }, + }; + } + + return { + success: true, + message: 'Image deleted.', + status: StatusCodeEnum.OK, + data: image, + }; + } +} + +new Ioc().make(ImageService); diff --git a/app/Validators/Course/CreateCourseValidator.ts b/app/Validators/Course/CreateCourseValidator.ts index 0ad11f0..6c7c14c 100644 --- a/app/Validators/Course/CreateCourseValidator.ts +++ b/app/Validators/Course/CreateCourseValidator.ts @@ -7,10 +7,7 @@ export default class CreateCourseValidator { public schema = schema.create({ title: schema.string(), description: schema.string(), - background_image: schema.file.optional({ - size: '2mb', - extnames: ['jpg', 'jpeg', 'png'], - }), + bg_image_id: schema.number.optional([rules.exists({ table: 'images', column: 'id' })]), 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/Image/CreateImageValidator.ts b/app/Validators/Image/CreateImageValidator.ts new file mode 100644 index 0000000..8a2b01c --- /dev/null +++ b/app/Validators/Image/CreateImageValidator.ts @@ -0,0 +1,15 @@ +import { schema } from '@ioc:Adonis/Core/Validator'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +export default class CreateImageValidator { + constructor(protected ctx: HttpContextContract) {} + + public schema = schema.create({ + image: schema.file({ + size: '10mb', + extnames: ['jpg', 'jpeg', 'png'], + }), + }); + + public messages = {}; +} diff --git a/database/migrations/1629037559962_images.ts b/database/migrations/1626545059988_images.ts similarity index 100% rename from database/migrations/1629037559962_images.ts rename to database/migrations/1626545059988_images.ts diff --git a/database/migrations/1627300211260_courses.ts b/database/migrations/1627300211260_courses.ts index f063de4..5682713 100644 --- a/database/migrations/1627300211260_courses.ts +++ b/database/migrations/1627300211260_courses.ts @@ -11,7 +11,7 @@ export default class Courses extends BaseSchema { public async up() { this.schema.createTable(this.tableName, table => { table.string('id', 21).primary(); - table.string('bg_image_id'); + table.integer('bg_image_id').unsigned().references('images.id').onDelete('SET NULL'); table.string('title').notNullable(); table.string('description').notNullable(); table.string('teacher_id', 21).unsigned().references('users.id').onDelete('CASCADE'); diff --git a/database/seeders/User.ts b/database/seeders/User.ts index 0ca0c4f..89c7cf2 100644 --- a/database/seeders/User.ts +++ b/database/seeders/User.ts @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this */ import BaseSeeder from '@ioc:Adonis/Lucid/Seeder'; import Role from 'App/Models/Role'; +import User from 'App/Models/User'; import { UserFactory } from 'Database/factories'; export default class UserSeeder extends BaseSeeder { @@ -17,7 +18,13 @@ export default class UserSeeder extends BaseSeeder { /** * Administrator */ - const userAdmin = await UserFactory.with('contacts').create(); + const userAdmin = await User.create({ + first_name: 'John', + last_name: 'Doe', + login: 'admin', + password: '12345', + email: 'example.email@mail.com', + }); await userAdmin.related('roles').attach([roles.admin.id]); /** diff --git a/routes/apis/v1/images.ts b/routes/apis/v1/images.ts new file mode 100644 index 0000000..bae3929 --- /dev/null +++ b/routes/apis/v1/images.ts @@ -0,0 +1,8 @@ +import Route from '@ioc:Adonis/Core/Route'; + +Route.group(() => { + Route.post('/', 'Api/v1/ImagesController.create').as('images.create'); + Route.delete('/:id', 'Api/v1/ImagesController.delete').as('images.delete'); + Route.get('/', 'Api/v1/ImagesController.list').as('images.list'); + Route.get('/:id', 'Api/v1/ImagesController.show').as('images.show'); +}).prefix('images'); diff --git a/routes/apis/v1/index.ts b/routes/apis/v1/index.ts index 272a8f9..a5657ee 100644 --- a/routes/apis/v1/index.ts +++ b/routes/apis/v1/index.ts @@ -48,6 +48,13 @@ Route.group(() => { * */ require('./lessons'); + + /** + * + * Images module + * + */ + require('./images'); }).middleware('auth'); }) .prefix('v1')