From fb92e0e4e4feacb282fb579d6d62d58c2c301505 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 15 Aug 2021 16:49:31 +0300 Subject: [PATCH] feat: cloudinary service && bg image for course --- .adonisrc.json | 9 ++- app/Models/Course.ts | 3 + app/Repositories/CourseRepository.ts | 26 ++++++++- app/Repositories/UserRepository.ts | 10 ++-- app/Services/CloudinaryService.ts | 55 +++++++++++++++++++ app/Services/MeService.ts | 1 + .../Course/CreateCourseValidator.ts | 4 ++ app/Validators/User/UpdateUserValidator.ts | 4 +- config/cloudinary.ts | 11 ++++ contracts/cloudinary.ts | 26 +++++++++ database/migrations/1627300211260_courses.ts | 1 + env.ts | 4 ++ package-lock.json | 26 +++++++++ package.json | 1 + providers/CloudinaryProvider.ts | 14 +++++ routes/apis/v1/index.ts | 4 +- 16 files changed, 187 insertions(+), 12 deletions(-) create mode 100644 app/Services/CloudinaryService.ts create mode 100644 config/cloudinary.ts create mode 100644 contracts/cloudinary.ts create mode 100644 providers/CloudinaryProvider.ts diff --git a/.adonisrc.json b/.adonisrc.json index fa1c0f9..809f844 100644 --- a/.adonisrc.json +++ b/.adonisrc.json @@ -23,6 +23,13 @@ "Contracts": "contracts" }, "preloads": ["./routes/index", "./start/kernel"], - "providers": ["./providers/AppProvider", "@adonisjs/core", "@adonisjs/lucid", "@adonisjs/auth", "@adonisjs/redis"], + "providers": [ + "./providers/AppProvider", + "./providers/CloudinaryProvider", + "@adonisjs/core", + "@adonisjs/lucid", + "@adonisjs/auth", + "@adonisjs/redis" + ], "aceProviders": ["@adonisjs/repl"] } diff --git a/app/Models/Course.ts b/app/Models/Course.ts index 9563947..7ca6ca5 100644 --- a/app/Models/Course.ts +++ b/app/Models/Course.ts @@ -22,6 +22,9 @@ export default class Course extends BaseModel { @column({ isPrimary: true }) public id: string; + @column() + public bg_image: string; + @column() public title: string; diff --git a/app/Repositories/CourseRepository.ts b/app/Repositories/CourseRepository.ts index 1e8b102..ec53a0b 100644 --- a/app/Repositories/CourseRepository.ts +++ b/app/Repositories/CourseRepository.ts @@ -1,3 +1,13 @@ +/** + * Services + */ +import CloudinaryService from '@ioc:Adonis/Addons/Cloudinary'; + +/** + * Helpers + */ +import { cuid } from '@ioc:Adonis/Core/Helpers'; + /** * Datatypes */ @@ -150,14 +160,24 @@ export default class CourseRepository { * @returns Created course */ public async create(data: CreateCourseValidator['schema']['props']): Promise { - const createdCourse = await this.Course.create({ + /** + * 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 teacher_id: data.teacher_id, category_id: data.category_id, }); - return createdCourse; + + return course; } /** @@ -187,7 +207,7 @@ export default class CourseRepository { * * @param id Course id * @param data Data to update - * @returns Updated course + * @returns Updated course or null */ public async update(id: string | number, data: UpdateCourseValidator['schema']['props']): Promise { const course = await this.Course.query() diff --git a/app/Repositories/UserRepository.ts b/app/Repositories/UserRepository.ts index 60d39bb..199a97a 100644 --- a/app/Repositories/UserRepository.ts +++ b/app/Repositories/UserRepository.ts @@ -98,12 +98,12 @@ export default class UserRepository { * @returns Created user */ public async create(data: CreateUserValidator['schema']['props']): Promise { - const createdUser = await this.User.create(data); + const user = await this.User.create(data); - await createdUser.load('contacts'); - await createdUser.load('roles'); + await user.load('contacts'); + await user.load('roles'); - return createdUser; + return user; } /** @@ -111,7 +111,7 @@ export default class UserRepository { * * @param id User id * @param data Data to update - * @returns User or null if user is not found + * @returns User or null */ public async update(id: number | string, data: UpdateUserValidator['schema']['props']): Promise { const user = await this.User.query().preload('contacts').preload('roles').where('id', id).first(); diff --git a/app/Services/CloudinaryService.ts b/app/Services/CloudinaryService.ts new file mode 100644 index 0000000..291b5df --- /dev/null +++ b/app/Services/CloudinaryService.ts @@ -0,0 +1,55 @@ +import * as cloudinary from 'cloudinary'; +import { UploadApiResponse, UploadApiOptions, ResponseCallback } from 'cloudinary'; +import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser'; +import { CloudinaryConfig, CloudinaryInterface } from '@ioc:Adonis/Addons/Cloudinary'; + +export default class CloudinaryService implements CloudinaryInterface { + private readonly config: CloudinaryConfig; + + private cloudinary: typeof cloudinary; + + constructor(config: CloudinaryConfig) { + /** + * Cloudinary config + */ + cloudinary.v2.config({ + api_key: config.apiKey, + api_secret: config.apiSecret, + cloud_name: config.cloudName, + secure: config.secure, + }); + + this.cloudinary = cloudinary; + this.config = config; + } + + public async upload( + file: MultipartFileContract, + publicId: string | undefined = undefined, + uploadOptions?: UploadApiOptions, + callback?: ResponseCallback + ): Promise { + const path = CloudinaryService.getPathFromFile(file); + const response = await this.cloudinary.v2.uploader.upload( + path, + { public_id: publicId, ...uploadOptions }, + callback + ); + + return response; + } + + public getConfig(): CloudinaryConfig { + return this.config; + } + + public static getPathFromFile(file: MultipartFileContract): string { + const path = file.tmpPath ?? file.filePath; + + if (!path) { + throw new Error('Cannot get file path from file'); + } + + return path; + } +} diff --git a/app/Services/MeService.ts b/app/Services/MeService.ts index f42d77d..0714883 100644 --- a/app/Services/MeService.ts +++ b/app/Services/MeService.ts @@ -32,6 +32,7 @@ export default class MeService { await user.load('roles'); await user.load('contacts'); + await user.load('courses'); return { success: true, diff --git a/app/Validators/Course/CreateCourseValidator.ts b/app/Validators/Course/CreateCourseValidator.ts index 8da3021..417dd06 100644 --- a/app/Validators/Course/CreateCourseValidator.ts +++ b/app/Validators/Course/CreateCourseValidator.ts @@ -7,6 +7,10 @@ export default class CreateCourseValidator { public schema = schema.create({ title: schema.string(), description: schema.string(), + background_image: schema.file({ + size: '2mb', + extnames: ['jpg', 'jpeg', 'png'], + }), 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/User/UpdateUserValidator.ts b/app/Validators/User/UpdateUserValidator.ts index efaed0f..9e18ab9 100644 --- a/app/Validators/User/UpdateUserValidator.ts +++ b/app/Validators/User/UpdateUserValidator.ts @@ -7,8 +7,8 @@ export default class UpdateUserValidator { public schema = schema.create({ first_name: schema.string(), last_name: schema.string(), - login: schema.string({}, [rules.unique({ table: 'users', column: 'login' })]), - email: schema.string({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]), + login: schema.string.optional({}, [rules.unique({ table: 'users', column: 'login' })]), + email: schema.string.optional({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]), password: schema.string(), }); diff --git a/config/cloudinary.ts b/config/cloudinary.ts new file mode 100644 index 0000000..17782fd --- /dev/null +++ b/config/cloudinary.ts @@ -0,0 +1,11 @@ +import Env from '@ioc:Adonis/Core/Env'; +import { CloudinaryConfig } from '@ioc:Adonis/Addons/Cloudinary'; + +const cloudinaryConfig: CloudinaryConfig = { + cloudName: Env.get('CLOUDINARY_CLOUD_NAME'), + apiKey: Env.get('CLOUDINARY_API_KEY'), + apiSecret: Env.get('CLOUDINARY_API_SECRET'), + secure: Env.get('CLOUDINARY_SECURE', true), +}; + +export default cloudinaryConfig; diff --git a/contracts/cloudinary.ts b/contracts/cloudinary.ts new file mode 100644 index 0000000..2935d9b --- /dev/null +++ b/contracts/cloudinary.ts @@ -0,0 +1,26 @@ +declare module '@ioc:Adonis/Addons/Cloudinary' { + import { ResponseCallback, TransformationOptions, UploadApiOptions, UploadApiResponse } from 'cloudinary'; + import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser'; + + export interface CloudinaryConfig { + cloudName: string; + apiKey: string; + apiSecret: string; + secure: boolean; + [key: string]: TransformationOptions; + } + + export interface CloudinaryInterface { + upload( + file: MultipartFileContract, + publicId: string | undefined, + uploadOptions?: UploadApiOptions, + callback?: ResponseCallback + ): Promise; + + getConfig(): CloudinaryConfig; + } + + const CloudinaryService: CloudinaryInterface; + export default CloudinaryService; +} diff --git a/database/migrations/1627300211260_courses.ts b/database/migrations/1627300211260_courses.ts index f1891b4..d3bc121 100644 --- a/database/migrations/1627300211260_courses.ts +++ b/database/migrations/1627300211260_courses.ts @@ -11,6 +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('title').notNullable(); table.string('description').notNullable(); table.string('teacher_id', 21).unsigned().references('users.id').onDelete('CASCADE'); diff --git a/env.ts b/env.ts index 217972f..d70d868 100644 --- a/env.ts +++ b/env.ts @@ -29,4 +29,8 @@ export default Env.rules({ REDIS_HOST: Env.schema.string({ format: 'host' }), REDIS_PORT: Env.schema.number(), REDIS_PASSWORD: Env.schema.string.optional(), + CLOUDINARY_CLOUD_NAME: Env.schema.string(), + CLOUDINARY_API_KEY: Env.schema.string(), + CLOUDINARY_API_SECRET: Env.schema.string(), + CLOUDINARY_SECURE: Env.schema.boolean(), }); diff --git a/package-lock.json b/package-lock.json index 7de7337..dd9b422 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1691,6 +1691,22 @@ "string-width": "^4.2.0" } }, + "cloudinary": { + "version": "1.26.3", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-1.26.3.tgz", + "integrity": "sha512-80ItEXYLbhOGPM42ksOsqG6320jbsU6kqszjVjl5QwDwzbjouktRlw/NdGva1beSu8sp/0lj1mtSGPymGsNiQA==", + "requires": { + "cloudinary-core": "^2.10.2", + "core-js": "3.6.5", + "lodash": "^4.17.11", + "q": "^1.5.1" + } + }, + "cloudinary-core": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/cloudinary-core/-/cloudinary-core-2.11.4.tgz", + "integrity": "sha512-F1BZczD6f5mB73D0c8gl/iuacVQQO+UhckNZxeeS9ZIVeIHbsfqwWiAZMQmIvEb7Wti/9MLU0xVwaWOak2THHA==" + }, "cluster-key-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", @@ -1827,6 +1843,11 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, + "core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -5180,6 +5201,11 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, "qs": { "version": "6.10.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", diff --git a/package.json b/package.json index 7ae0673..4fa5866 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@adonisjs/lucid": "^15.0.3", "@adonisjs/redis": "^7.0.7", "@adonisjs/repl": "^3.1.4", + "cloudinary": "^1.26.3", "luxon": "^1.28.0", "nanoid": "^3.1.23", "pg": "^8.6.0", diff --git a/providers/CloudinaryProvider.ts b/providers/CloudinaryProvider.ts new file mode 100644 index 0000000..6508067 --- /dev/null +++ b/providers/CloudinaryProvider.ts @@ -0,0 +1,14 @@ +import { ApplicationContract } from '@ioc:Adonis/Core/Application'; +import CloudinaryService from 'App/Services/CloudinaryService'; + +export default class CloudinaryProvider { + constructor(protected app: ApplicationContract) {} + + public register() { + this.app.container.singleton('Adonis/Addons/Cloudinary', () => { + const config = this.app.container.resolveBinding('Adonis/Core/Config').get('cloudinary', {}); + + return new CloudinaryService(config); + }); + } +} diff --git a/routes/apis/v1/index.ts b/routes/apis/v1/index.ts index 0451fd2..272a8f9 100644 --- a/routes/apis/v1/index.ts +++ b/routes/apis/v1/index.ts @@ -2,7 +2,9 @@ import Route from '@ioc:Adonis/Core/Route'; Route.group(() => { - Route.get('/', () => `Backend API.`).as('index'); + Route.get('/', () => { + return 'Backend API.'; + }).as('index'); /** *