diff --git a/app/Models/Course.ts b/app/Models/Course.ts index 7ca6ca5..7007864 100644 --- a/app/Models/Course.ts +++ b/app/Models/Course.ts @@ -17,13 +17,14 @@ import Category from 'App/Models/Category'; import Lesson from 'App/Models/Lesson'; import User from 'App/Models/User'; import CourseStatusEnum from 'App/Datatypes/Enums/CourseStatusEnum'; +import Image from './Image'; export default class Course extends BaseModel { @column({ isPrimary: true }) public id: string; - @column() - public bg_image: string; + @column({ serializeAs: null }) + public bg_image_id: number; @column() public title: string; @@ -45,6 +46,11 @@ export default class Course extends BaseModel { }) public category: BelongsTo; + @belongsTo(() => Image, { + foreignKey: 'bg_image_id', + }) + public image: BelongsTo; + @belongsTo(() => User, { foreignKey: 'teacher_id', }) diff --git a/app/Models/Image.ts b/app/Models/Image.ts new file mode 100644 index 0000000..680deb1 --- /dev/null +++ b/app/Models/Image.ts @@ -0,0 +1,22 @@ +import { DateTime } from 'luxon'; +import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'; + +export default class Image extends BaseModel { + @column({ isPrimary: true }) + public id: number; + + @column() + public path: string; + + @column() + public name: string; + + @column() + public ext: string; + + @column.dateTime({ autoCreate: true }) + public createdAt: DateTime; + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + public updatedAt: DateTime; +} diff --git a/app/Repositories/CourseRepository.ts b/app/Repositories/CourseRepository.ts index ec53a0b..e2b4d7a 100644 --- a/app/Repositories/CourseRepository.ts +++ b/app/Repositories/CourseRepository.ts @@ -1,13 +1,3 @@ -/** - * Services - */ -import CloudinaryService from '@ioc:Adonis/Addons/Cloudinary'; - -/** - * Helpers - */ -import { cuid } from '@ioc:Adonis/Core/Helpers'; - /** * Datatypes */ @@ -160,16 +150,10 @@ export default class CourseRepository { * @returns Created course */ public async create(data: CreateCourseValidator['schema']['props']): Promise { - /** - * Load background image - */ - const imageUploadResponse = await CloudinaryService.upload(data.background_image, cuid()); - /** * Create new course */ const course = await this.Course.create({ - bg_image: imageUploadResponse.secure_url, 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 new file mode 100644 index 0000000..d416ec2 --- /dev/null +++ b/app/Repositories/ImageRepository.ts @@ -0,0 +1,42 @@ +import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser'; +import CloudinaryService from '@ioc:Adonis/Addons/Cloudinary'; +import Image from 'App/Models/Image'; + +export default class ImageRepository { + private Image: typeof Image; + + constructor() { + this.Image = Image; + } + + /** + * Create image in cloudinary cloud + * + * @param file File + * @param publicId Image public id + * @returns Created image + */ + public async createInCloudinaryCloud(file: MultipartFileContract, publicId: string | undefined): Promise { + const response = await CloudinaryService.upload(file, publicId); + const image = await this.Image.create({ name: publicId, path: response.secure_url, ext: response.format }); + return image; + } + + /** + * Delete image from cloudinary cloud + * + * @param id Image id + * @returns Result or null + */ + 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 image.delete(); + return result; + } + + return null; + } +} diff --git a/app/Services/CloudinaryService.ts b/app/Services/CloudinaryService.ts index 291b5df..f65bbfe 100644 --- a/app/Services/CloudinaryService.ts +++ b/app/Services/CloudinaryService.ts @@ -1,5 +1,5 @@ import * as cloudinary from 'cloudinary'; -import { UploadApiResponse, UploadApiOptions, ResponseCallback } from 'cloudinary'; +import { UploadApiResponse, UploadApiOptions, ResponseCallback, ResourceType, DeliveryType } from 'cloudinary'; import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser'; import { CloudinaryConfig, CloudinaryInterface } from '@ioc:Adonis/Addons/Cloudinary'; @@ -39,6 +39,18 @@ export default class CloudinaryService implements CloudinaryInterface { return response; } + public async destroy( + publicId: string, + options?: { + resource_type?: ResourceType; + type?: DeliveryType; + invalidate?: boolean; + } + ): Promise { + const response = await this.cloudinary.v2.uploader.destroy(publicId, options); + return response; + } + public getConfig(): CloudinaryConfig { return this.config; } diff --git a/app/Services/CourseService.ts b/app/Services/CourseService.ts index 7de6300..07af72d 100644 --- a/app/Services/CourseService.ts +++ b/app/Services/CourseService.ts @@ -1,3 +1,4 @@ +import { cuid } from '@ioc:Adonis/Core/Helpers'; import { inject, Ioc } from '@adonisjs/core/build/standalone'; import { AuthContract } from '@ioc:Adonis/Addons/Auth'; @@ -16,6 +17,7 @@ import RoleHelper from 'App/Helpers/RoleHelper'; import CourseRepository from 'App/Repositories/CourseRepository'; import UserRepository from 'App/Repositories/UserRepository'; import CategoryRepository from 'App/Repositories/CategoryRepository'; +import ImageRepository from 'App/Repositories/ImageRepository'; /** * Validators @@ -31,14 +33,18 @@ export default class CourseService { private categoryRepository: CategoryRepository; + private imageRepository: ImageRepository; + constructor( courseRepository: CourseRepository, userRepository: UserRepository, - categoryRepository: CategoryRepository + categoryRepository: CategoryRepository, + imageRepository: ImageRepository ) { this.courseRepository = courseRepository; this.userRepository = userRepository; this.categoryRepository = categoryRepository; + this.imageRepository = imageRepository; } /** @@ -274,11 +280,15 @@ export default class CourseService { }; } - /** - * Creating new course - */ + /* 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, @@ -308,6 +318,11 @@ export default class CourseService { }; } + /** + * Delete background image from course + */ + await this.imageRepository.deleteFormCloudinaryCloud(course.bg_image_id); + return { success: true, status: StatusCodeEnum.OK, diff --git a/app/Validators/Course/CreateCourseValidator.ts b/app/Validators/Course/CreateCourseValidator.ts index 417dd06..0ad11f0 100644 --- a/app/Validators/Course/CreateCourseValidator.ts +++ b/app/Validators/Course/CreateCourseValidator.ts @@ -7,7 +7,7 @@ export default class CreateCourseValidator { public schema = schema.create({ title: schema.string(), description: schema.string(), - background_image: schema.file({ + background_image: schema.file.optional({ size: '2mb', extnames: ['jpg', 'jpeg', 'png'], }), diff --git a/contracts/cloudinary.ts b/contracts/cloudinary.ts index 2935d9b..26ae909 100644 --- a/contracts/cloudinary.ts +++ b/contracts/cloudinary.ts @@ -1,5 +1,12 @@ declare module '@ioc:Adonis/Addons/Cloudinary' { - import { ResponseCallback, TransformationOptions, UploadApiOptions, UploadApiResponse } from 'cloudinary'; + import { + ResponseCallback, + TransformationOptions, + UploadApiOptions, + UploadApiResponse, + ResourceType, + DeliveryType, + } from 'cloudinary'; import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser'; export interface CloudinaryConfig { @@ -18,6 +25,15 @@ declare module '@ioc:Adonis/Addons/Cloudinary' { callback?: ResponseCallback ): Promise; + destroy( + publicId: string, + options?: { + resource_type?: ResourceType; + type?: DeliveryType; + invalidate?: boolean; + } + ): Promise; + getConfig(): CloudinaryConfig; } diff --git a/database/migrations/1627300211260_courses.ts b/database/migrations/1627300211260_courses.ts index d3bc121..f063de4 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'); + table.string('bg_image_id'); table.string('title').notNullable(); table.string('description').notNullable(); table.string('teacher_id', 21).unsigned().references('users.id').onDelete('CASCADE'); diff --git a/database/migrations/1629037559962_images.ts b/database/migrations/1629037559962_images.ts new file mode 100644 index 0000000..4799319 --- /dev/null +++ b/database/migrations/1629037559962_images.ts @@ -0,0 +1,20 @@ +import BaseSchema from '@ioc:Adonis/Lucid/Schema'; + +export default class Images extends BaseSchema { + protected tableName = 'images'; + + public async up() { + this.schema.createTable(this.tableName, table => { + table.increments('id'); + table.string('path').notNullable(); + table.string('name').notNullable(); + table.string('ext').notNullable(); + table.timestamp('created_at', { useTz: true }); + table.timestamp('updated_at', { useTz: true }); + }); + } + + public async down() { + this.schema.dropTable(this.tableName); + } +}