feat: image repository & images table

This commit is contained in:
Sergey Yarkov 2021-08-15 18:10:02 +03:00
parent fb92e0e4e4
commit 18819024b2
10 changed files with 143 additions and 26 deletions

View File

@ -17,13 +17,14 @@ import Category from 'App/Models/Category';
import Lesson from 'App/Models/Lesson'; import Lesson from 'App/Models/Lesson';
import User from 'App/Models/User'; import User from 'App/Models/User';
import CourseStatusEnum from 'App/Datatypes/Enums/CourseStatusEnum'; import CourseStatusEnum from 'App/Datatypes/Enums/CourseStatusEnum';
import Image from './Image';
export default class Course extends BaseModel { export default class Course extends BaseModel {
@column({ isPrimary: true }) @column({ isPrimary: true })
public id: string; public id: string;
@column() @column({ serializeAs: null })
public bg_image: string; public bg_image_id: number;
@column() @column()
public title: string; public title: string;
@ -45,6 +46,11 @@ export default class Course extends BaseModel {
}) })
public category: BelongsTo<typeof Category>; public category: BelongsTo<typeof Category>;
@belongsTo(() => Image, {
foreignKey: 'bg_image_id',
})
public image: BelongsTo<typeof Image>;
@belongsTo(() => User, { @belongsTo(() => User, {
foreignKey: 'teacher_id', foreignKey: 'teacher_id',
}) })

22
app/Models/Image.ts Normal file
View File

@ -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;
}

View File

@ -1,13 +1,3 @@
/**
* Services
*/
import CloudinaryService from '@ioc:Adonis/Addons/Cloudinary';
/**
* Helpers
*/
import { cuid } from '@ioc:Adonis/Core/Helpers';
/** /**
* Datatypes * Datatypes
*/ */
@ -160,16 +150,10 @@ export default class CourseRepository {
* @returns Created course * @returns Created course
*/ */
public async create(data: CreateCourseValidator['schema']['props']): Promise<Course> { public async create(data: CreateCourseValidator['schema']['props']): Promise<Course> {
/**
* Load background image
*/
const imageUploadResponse = await CloudinaryService.upload(data.background_image, cuid());
/** /**
* Create new course * Create new course
*/ */
const course = await this.Course.create({ const course = await this.Course.create({
bg_image: imageUploadResponse.secure_url,
title: data.title, title: data.title,
description: data.description, description: data.description,
status: CourseStatusEnum.DRAFT, // make course DRAFT before adding lessons status: CourseStatusEnum.DRAFT, // make course DRAFT before adding lessons

View File

@ -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<Image> {
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<any> {
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;
}
}

View File

@ -1,5 +1,5 @@
import * as cloudinary from 'cloudinary'; 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 { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser';
import { CloudinaryConfig, CloudinaryInterface } from '@ioc:Adonis/Addons/Cloudinary'; import { CloudinaryConfig, CloudinaryInterface } from '@ioc:Adonis/Addons/Cloudinary';
@ -39,6 +39,18 @@ export default class CloudinaryService implements CloudinaryInterface {
return response; return response;
} }
public async destroy(
publicId: string,
options?: {
resource_type?: ResourceType;
type?: DeliveryType;
invalidate?: boolean;
}
): Promise<any> {
const response = await this.cloudinary.v2.uploader.destroy(publicId, options);
return response;
}
public getConfig(): CloudinaryConfig { public getConfig(): CloudinaryConfig {
return this.config; return this.config;
} }

View File

@ -1,3 +1,4 @@
import { cuid } from '@ioc:Adonis/Core/Helpers';
import { inject, Ioc } from '@adonisjs/core/build/standalone'; import { inject, Ioc } from '@adonisjs/core/build/standalone';
import { AuthContract } from '@ioc:Adonis/Addons/Auth'; import { AuthContract } from '@ioc:Adonis/Addons/Auth';
@ -16,6 +17,7 @@ import RoleHelper from 'App/Helpers/RoleHelper';
import CourseRepository from 'App/Repositories/CourseRepository'; import CourseRepository from 'App/Repositories/CourseRepository';
import UserRepository from 'App/Repositories/UserRepository'; import UserRepository from 'App/Repositories/UserRepository';
import CategoryRepository from 'App/Repositories/CategoryRepository'; import CategoryRepository from 'App/Repositories/CategoryRepository';
import ImageRepository from 'App/Repositories/ImageRepository';
/** /**
* Validators * Validators
@ -31,14 +33,18 @@ export default class CourseService {
private categoryRepository: CategoryRepository; private categoryRepository: CategoryRepository;
private imageRepository: ImageRepository;
constructor( constructor(
courseRepository: CourseRepository, courseRepository: CourseRepository,
userRepository: UserRepository, userRepository: UserRepository,
categoryRepository: CategoryRepository categoryRepository: CategoryRepository,
imageRepository: ImageRepository
) { ) {
this.courseRepository = courseRepository; this.courseRepository = courseRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.categoryRepository = categoryRepository; this.categoryRepository = categoryRepository;
this.imageRepository = imageRepository;
} }
/** /**
@ -274,11 +280,15 @@ export default class CourseService {
}; };
} }
/** /* Create course */
* Creating new course
*/
const course = await this.courseRepository.create(data); 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 { return {
success: true, success: true,
status: StatusCodeEnum.CREATED, 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 { return {
success: true, success: true,
status: StatusCodeEnum.OK, status: StatusCodeEnum.OK,

View File

@ -7,7 +7,7 @@ export default class CreateCourseValidator {
public schema = schema.create({ public schema = schema.create({
title: schema.string(), title: schema.string(),
description: schema.string(), description: schema.string(),
background_image: schema.file({ background_image: schema.file.optional({
size: '2mb', size: '2mb',
extnames: ['jpg', 'jpeg', 'png'], extnames: ['jpg', 'jpeg', 'png'],
}), }),

View File

@ -1,5 +1,12 @@
declare module '@ioc:Adonis/Addons/Cloudinary' { 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'; import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser';
export interface CloudinaryConfig { export interface CloudinaryConfig {
@ -18,6 +25,15 @@ declare module '@ioc:Adonis/Addons/Cloudinary' {
callback?: ResponseCallback callback?: ResponseCallback
): Promise<UploadApiResponse>; ): Promise<UploadApiResponse>;
destroy(
publicId: string,
options?: {
resource_type?: ResourceType;
type?: DeliveryType;
invalidate?: boolean;
}
): Promise<any>;
getConfig(): CloudinaryConfig; getConfig(): CloudinaryConfig;
} }

View File

@ -11,7 +11,7 @@ export default class Courses extends BaseSchema {
public async up() { public async up() {
this.schema.createTable(this.tableName, table => { this.schema.createTable(this.tableName, table => {
table.string('id', 21).primary(); table.string('id', 21).primary();
table.string('bg_image'); table.string('bg_image_id');
table.string('title').notNullable(); table.string('title').notNullable();
table.string('description').notNullable(); table.string('description').notNullable();
table.string('teacher_id', 21).unsigned().references('users.id').onDelete('CASCADE'); table.string('teacher_id', 21).unsigned().references('users.id').onDelete('CASCADE');

View File

@ -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);
}
}