mirror of
https://github.com/sergeyyarkov/educt-server.git
synced 2026-10-11 10:19:20 +03:00
feat: cloudinary service && bg image for course
This commit is contained in:
parent
f2b43ee388
commit
fb92e0e4e4
@ -23,6 +23,13 @@
|
|||||||
"Contracts": "contracts"
|
"Contracts": "contracts"
|
||||||
},
|
},
|
||||||
"preloads": ["./routes/index", "./start/kernel"],
|
"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"]
|
"aceProviders": ["@adonisjs/repl"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,9 @@ export default class Course extends BaseModel {
|
|||||||
@column({ isPrimary: true })
|
@column({ isPrimary: true })
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
|
@column()
|
||||||
|
public bg_image: string;
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
public title: string;
|
public title: string;
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Services
|
||||||
|
*/
|
||||||
|
import CloudinaryService from '@ioc:Adonis/Addons/Cloudinary';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers
|
||||||
|
*/
|
||||||
|
import { cuid } from '@ioc:Adonis/Core/Helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datatypes
|
* Datatypes
|
||||||
*/
|
*/
|
||||||
@ -150,14 +160,24 @@ 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> {
|
||||||
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,
|
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
|
||||||
teacher_id: data.teacher_id,
|
teacher_id: data.teacher_id,
|
||||||
category_id: data.category_id,
|
category_id: data.category_id,
|
||||||
});
|
});
|
||||||
return createdCourse;
|
|
||||||
|
return course;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -187,7 +207,7 @@ export default class CourseRepository {
|
|||||||
*
|
*
|
||||||
* @param id Course id
|
* @param id Course id
|
||||||
* @param data Data to update
|
* @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> {
|
public async update(id: string | number, data: UpdateCourseValidator['schema']['props']): Promise<Course | null> {
|
||||||
const course = await this.Course.query()
|
const course = await this.Course.query()
|
||||||
|
|||||||
@ -98,12 +98,12 @@ export default class UserRepository {
|
|||||||
* @returns Created user
|
* @returns Created user
|
||||||
*/
|
*/
|
||||||
public async create(data: CreateUserValidator['schema']['props']): Promise<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 user.load('contacts');
|
||||||
await createdUser.load('roles');
|
await user.load('roles');
|
||||||
|
|
||||||
return createdUser;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,7 +111,7 @@ export default class UserRepository {
|
|||||||
*
|
*
|
||||||
* @param id User id
|
* @param id User id
|
||||||
* @param data Data to update
|
* @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> {
|
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();
|
const user = await this.User.query().preload('contacts').preload('roles').where('id', id).first();
|
||||||
|
|||||||
55
app/Services/CloudinaryService.ts
Normal file
55
app/Services/CloudinaryService.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ export default class MeService {
|
|||||||
|
|
||||||
await user.load('roles');
|
await user.load('roles');
|
||||||
await user.load('contacts');
|
await user.load('contacts');
|
||||||
|
await user.load('courses');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@ -7,6 +7,10 @@ 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({
|
||||||
|
size: '2mb',
|
||||||
|
extnames: ['jpg', 'jpeg', 'png'],
|
||||||
|
}),
|
||||||
teacher_id: schema.string({}, [rules.exists({ table: 'users', column: 'id' })]),
|
teacher_id: schema.string({}, [rules.exists({ table: 'users', column: 'id' })]),
|
||||||
category_id: schema.string({}, [rules.exists({ table: 'categories', column: 'id' })]),
|
category_id: schema.string({}, [rules.exists({ table: 'categories', column: 'id' })]),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,8 +7,8 @@ export default class UpdateUserValidator {
|
|||||||
public schema = schema.create({
|
public schema = schema.create({
|
||||||
first_name: schema.string(),
|
first_name: schema.string(),
|
||||||
last_name: schema.string(),
|
last_name: schema.string(),
|
||||||
login: schema.string({}, [rules.unique({ table: 'users', column: 'login' })]),
|
login: schema.string.optional({}, [rules.unique({ table: 'users', column: 'login' })]),
|
||||||
email: schema.string({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]),
|
email: schema.string.optional({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]),
|
||||||
password: schema.string(),
|
password: schema.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
11
config/cloudinary.ts
Normal file
11
config/cloudinary.ts
Normal 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
26
contracts/cloudinary.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -11,6 +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('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');
|
||||||
|
|||||||
4
env.ts
4
env.ts
@ -29,4 +29,8 @@ export default Env.rules({
|
|||||||
REDIS_HOST: Env.schema.string({ format: 'host' }),
|
REDIS_HOST: Env.schema.string({ format: 'host' }),
|
||||||
REDIS_PORT: Env.schema.number(),
|
REDIS_PORT: Env.schema.number(),
|
||||||
REDIS_PASSWORD: Env.schema.string.optional(),
|
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
26
package-lock.json
generated
@ -1691,6 +1691,22 @@
|
|||||||
"string-width": "^4.2.0"
|
"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": {
|
"cluster-key-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
|
||||||
@ -1827,6 +1843,11 @@
|
|||||||
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
|
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
|
||||||
"dev": true
|
"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": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
"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==",
|
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"q": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
||||||
|
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
|
||||||
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.10.1",
|
"version": "6.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz",
|
||||||
|
|||||||
@ -37,6 +37,7 @@
|
|||||||
"@adonisjs/lucid": "^15.0.3",
|
"@adonisjs/lucid": "^15.0.3",
|
||||||
"@adonisjs/redis": "^7.0.7",
|
"@adonisjs/redis": "^7.0.7",
|
||||||
"@adonisjs/repl": "^3.1.4",
|
"@adonisjs/repl": "^3.1.4",
|
||||||
|
"cloudinary": "^1.26.3",
|
||||||
"luxon": "^1.28.0",
|
"luxon": "^1.28.0",
|
||||||
"nanoid": "^3.1.23",
|
"nanoid": "^3.1.23",
|
||||||
"pg": "^8.6.0",
|
"pg": "^8.6.0",
|
||||||
|
|||||||
14
providers/CloudinaryProvider.ts
Normal file
14
providers/CloudinaryProvider.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,9 @@
|
|||||||
import Route from '@ioc:Adonis/Core/Route';
|
import Route from '@ioc:Adonis/Core/Route';
|
||||||
|
|
||||||
Route.group(() => {
|
Route.group(() => {
|
||||||
Route.get('/', () => `Backend API.`).as('index');
|
Route.get('/', () => {
|
||||||
|
return 'Backend API.';
|
||||||
|
}).as('index');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user