feat: cloudinary service && bg image for course

This commit is contained in:
Sergey Yarkov 2021-08-15 16:49:31 +03:00
parent f2b43ee388
commit fb92e0e4e4
16 changed files with 187 additions and 12 deletions

View File

@ -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"]
}

View File

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

View File

@ -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<Course> {
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<Course | null> {
const course = await this.Course.query()

View File

@ -98,12 +98,12 @@ export default class UserRepository {
* @returns Created user
*/
public async create(data: CreateUserValidator['schema']['props']): Promise<User> {
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<User | null> {
const user = await this.User.query().preload('contacts').preload('roles').where('id', id).first();

View File

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

View File

@ -32,6 +32,7 @@ export default class MeService {
await user.load('roles');
await user.load('contacts');
await user.load('courses');
return {
success: true,

View File

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

View File

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

11
config/cloudinary.ts Normal file
View File

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

26
contracts/cloudinary.ts Normal file
View File

@ -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<UploadApiResponse>;
getConfig(): CloudinaryConfig;
}
const CloudinaryService: CloudinaryInterface;
export default CloudinaryService;
}

View File

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

4
env.ts
View File

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

26
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

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

View File

@ -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');
/**
*