├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── adonis-typings └── index.ts ├── instructions.ts ├── package.json ├── providers └── CloudinaryProvider.ts ├── src └── Cloudinary.ts ├── templates └── cloudinary.txt └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:adonis/typescriptApp" 4 | ], 5 | "rules": { 6 | "no-unexpected-multiline": "error", 7 | "arrow-body-style": ["error", "as-needed"], 8 | "arrow-parens": ["error", "as-needed"], 9 | "block-spacing": ["error", "always"], 10 | "camelcase": ["error", { "ignoreImports": true }], 11 | "comma-dangle": ["error", "always-multiline"], 12 | "comma-spacing": "error", 13 | "curly": ["error", "all"], 14 | "eol-last": "error", 15 | "eqeqeq": ["error", "smart"], 16 | "func-call-spacing": "error", 17 | "indent": "off", 18 | "@typescript-eslint/indent": ["error", "tab"], 19 | "key-spacing": "error", 20 | "keyword-spacing": "error", 21 | "linebreak-style": "error", 22 | "max-len": ["error", { "code": 120 }], 23 | "no-var": "error", 24 | "object-curly-spacing": ["error", "always"], 25 | "prefer-const": "error", 26 | "quote-props": ["error", "consistent-as-needed", { "keywords": true }], 27 | "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], 28 | "space-before-blocks": "error", 29 | "space-before-function-paren": "off", 30 | "@typescript-eslint/space-before-function-paren": ["error", { "anonymous": "never", "named": "never", "asyncArrow": "always" }], 31 | "space-in-parens": ["error", "never"], 32 | "spaced-comment": ["error", "always", { "block": { "balanced": true, "exceptions": ["*"] } }], 33 | "switch-colon-spacing": "error" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | .DS_Store 4 | npm-debug.log 5 | .idea 6 | out 7 | .nyc_output 8 | test-helpers/tmp 9 | test/functional/database 10 | .DS_STORE 11 | .vscode/ 12 | *.sublime-project 13 | *.sublime-workspace 14 | *.log 15 | build 16 | dist 17 | yarn.lock 18 | package-lock.json 19 | shrinkwrap.yaml 20 | test/__app 21 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | .DS_Store 4 | npm-debug.log 5 | .idea 6 | out 7 | .nyc_output 8 | test-helpers/tmp 9 | test/functional/database 10 | .DS_STORE 11 | .vscode/ 12 | *.sublime-project 13 | *.sublime-workspace 14 | *.log 15 | adonis-typings 16 | providers 17 | src 18 | templates 19 | dist 20 | yarn.lock 21 | package-lock.json 22 | shrinkwrap.yaml 23 | test/__app 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Liam Edwards 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AdonisJS Cloudinary 2 | 3 | [![npm-image]][npm-url] [![license-image]][license-url] [![typescript-image]][typescript-url] 4 | 5 | A Cloudinary Wrapper for Adonis 5 6 | 7 | ## Installation 8 | 9 | Install with either `npm` or `yarn`. 10 | 11 | ```bash 12 | npm i adonisjs-cloudinary 13 | yarn add adonisjs-cloudinary 14 | ``` 15 | 16 | And initialise the package 17 | ```bash 18 | node ace configure adonisjs-cloudinary 19 | ``` 20 | 21 | ## How to use 22 | 23 | ### Step 1: Get your API key and API secret 24 | 25 | Upon creating a Cloudinary account, you will be given your API key and API secret. If you already have an account, 26 | you can find these in your account settings under the "Security" section. 27 | 28 | Your cloud name will be under found in the "Account" section of your settings. 29 | 30 | ### Step 2: Initialisation 31 | 32 | Add variables to `.env` file of project. 33 | 34 | ```txt 35 | ... 36 | CLOUDINARY_CLOUD_NAME=YOUR_CLOUD_NAME 37 | CLOUDINARY_API_KEY=YOUR_KEY 38 | CLOUDINARY_API_SECRET=YOUR_SECRET 39 | ``` 40 | 41 | ### Step 3: Upload an image 42 | 43 | You can upload an image to Cloudinary using the `upload` method passing in the file path and a public ID. 44 | 45 | ```ts 46 | import cloudinary from '@ioc:Adonis/Addons/Cloudinary' 47 | 48 | await cloudinary.upload(filePath, publicId) 49 | ``` 50 | 51 | You can alternatively pass in a file object 52 | 53 | ```ts 54 | public async store({ request }: HttpContextContract) { 55 | const file = request.file('your_file') 56 | if (file) { 57 | await cloudinary.upload(file, file.clientName) 58 | } 59 | } 60 | ``` 61 | 62 | The upload method returns an object the contains the image's public ID, URL, secure URL, and more if the upload is 63 | successful. 64 | 65 | ### Step 4: Show your image 66 | 67 | To get the URL for your image, you can use the `show` method, passing in your public ID and optionally an object 68 | containing your transformation options. 69 | 70 | ```ts 71 | const url = cloudinary.show('your_public_id') 72 | ``` 73 | 74 | By default, this method will use the transformation options found in the `cloudinary.ts` config file: 75 | 76 | ```ts 77 | { 78 | transformation: { 79 | format: 'png', 80 | }, 81 | width: 150, 82 | height: 150, 83 | crop: 'fit', 84 | } 85 | ``` 86 | 87 | [npm-image]: https://img.shields.io/npm/v/adonisjs-cloudinary?logo=npm&style=for-the-badge 88 | [npm-url]: https://www.npmjs.com/package/adonisjs-cloudinary 89 | 90 | [license-image]: https://img.shields.io/npm/l/adonisjs-cloudinary?style=for-the-badge&color=blueviolet 91 | [license-url]: https://github.com/liam-edwards/adonisjs-cloudinary/blob/master/LICENSE 92 | 93 | [typescript-image]: https://img.shields.io/npm/types/adonisjs-cloudinary?color=294E80&label=%20&logo=typescript&style=for-the-badge 94 | [typescript-url]: https://github.com/liam-edwards/adonisjs-cloudinar 95 | -------------------------------------------------------------------------------- /adonis-typings/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * adonisjs-cloudinary 3 | * 4 | * (c) Liam Edwards 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare module '@ioc:Adonis/Addons/Cloudinary' { 11 | import { 12 | DeliveryType, 13 | ResourceType, 14 | ResponseCallback, 15 | TransformationOptions, 16 | UploadApiOptions, 17 | UploadApiResponse, 18 | } from 'cloudinary' 19 | import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser' 20 | 21 | export interface CloudinaryConfig { 22 | cloudName: string 23 | apiKey: string 24 | apiSecret: string 25 | secure: boolean 26 | [key: string]: TransformationOptions 27 | } 28 | 29 | export function upload( 30 | filePath: string, 31 | publicId: string|null, 32 | uploadOptions?: UploadApiOptions, 33 | callback?: ResponseCallback 34 | ): Promise 35 | 36 | export function upload( 37 | file: MultipartFileContract, 38 | publicId: string|null, 39 | uploadOptions?: UploadApiOptions, 40 | callback?: ResponseCallback 41 | ): Promise 42 | 43 | export function unsignedUpload( 44 | filePath: string, 45 | uploadPreset: string, 46 | publicId: string|null, 47 | uploadOptions?: UploadApiOptions, 48 | callback?: ResponseCallback 49 | ): Promise 50 | 51 | export function unsignedUpload( 52 | file: MultipartFileContract, 53 | uploadPreset: string, 54 | publicId: string|null, 55 | uploadOptions?: UploadApiOptions, 56 | callback?: ResponseCallback 57 | ): Promise 58 | 59 | export function getResult(): UploadApiResponse 60 | 61 | export function getPublicId(): string 62 | 63 | export function show( 64 | publicId, 65 | options?: TransformationOptions 66 | ): string 67 | 68 | export function secureShow( 69 | publicId, 70 | options?: TransformationOptions 71 | ): string 72 | 73 | export function destroy( 74 | publicId, 75 | options?: { 76 | /* eslint-disable camelcase */ 77 | resource_type?: ResourceType, 78 | /* eslint-enable camelcase */ 79 | type?: DeliveryType, 80 | invalidate?: boolean, 81 | } 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /instructions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * adonisjs-cloudinary 3 | * 4 | * (c) Liam Edwards 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 11 | import * as sinkStatic from '@adonisjs/sink' 12 | import { join } from 'path' 13 | 14 | function getStub(...relativePaths: string[]) { 15 | return join(__dirname, 'templates', ...relativePaths) 16 | } 17 | 18 | export default async function instructions(projectRoot: string, app: ApplicationContract, sink: typeof sinkStatic) { 19 | // Config 20 | const configPath = app.configPath('cloudinary.ts') 21 | const cloudinaryConfig = new sink.files.TemplateLiteralFile( 22 | projectRoot, 23 | configPath, 24 | getStub('cloudinary.txt') 25 | ) 26 | cloudinaryConfig.apply().commit() 27 | const configDir = app.directoriesMap.get('config') || 'config' 28 | sink.logger.action('create').succeeded(`${configDir}/cloudinary.ts`) 29 | 30 | // .env 31 | const env = new sink.files.EnvFile(projectRoot) 32 | env.set('CLOUDINARY_CLOUD_NAME', '') 33 | env.set('CLOUDINARY_API_KEY', '') 34 | env.set('CLOUDINARY_API_SECRET', '') 35 | env.set('CLOUDINARY_SECURE', '') 36 | env.commit() 37 | sink.logger.action('update').succeeded('.env,.env.example') 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adonisjs-cloudinary", 3 | "version": "0.1.6", 4 | "description": "", 5 | "main": "./build/providers/CloudinaryProvider.js", 6 | "files": [ 7 | "build/adonis-typings", 8 | "build/providers", 9 | "build/src", 10 | "build/templates", 11 | "build/instructions.js" 12 | ], 13 | "scripts": { 14 | "lint": "eslint . --ext=.ts", 15 | "clean": "rm -rf build", 16 | "compile": "npm run lint && npm run clean && tsc && npm run copyfiles", 17 | "copyfiles": "copyfiles \"templates/**/*.txt\" build", 18 | "build": "npm run compile", 19 | "test": "echo \"Error: no test specified\" && exit 1" 20 | }, 21 | "types": "./build/adonis-typings/index.d.ts", 22 | "keywords": [ 23 | "AdonisJS", 24 | "Cloudinary", 25 | "Adonis Cloudinary" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/liam-edwards/adonisjs-cloudinary.git" 30 | }, 31 | "author": "Liam Edwards", 32 | "license": "MIT", 33 | "devDependencies": { 34 | "@adonisjs/core": "^5.1.10", 35 | "@adonisjs/mrm-preset": "^4.1.2", 36 | "@adonisjs/require-ts": "^2.0.7", 37 | "@adonisjs/sink": "^5.1.5", 38 | "@types/node": "^16.4.0", 39 | "@typescript-eslint/eslint-plugin": "^4.28.4", 40 | "@typescript-eslint/parser": "^4.28.4", 41 | "adonis-preset-ts": "^2.1.0", 42 | "copyfiles": "^2.4.1", 43 | "eslint": "^7.31.0", 44 | "eslint-plugin-adonis": "^1.3.3", 45 | "typescript": "^4.3.5" 46 | }, 47 | "peerDependencies": { 48 | "@adonisjs/core": "^5.1.10", 49 | "@adonisjs/require-ts": "^2.0.7" 50 | }, 51 | "dependencies": { 52 | "cloudinary": "^1.26.2" 53 | }, 54 | "adonisjs": { 55 | "instructions": "./build/instructions.js", 56 | "env": { 57 | "CLOUDINARY_CLOUD_NAME": "", 58 | "CLOUDINARY_API_KEY": "", 59 | "CLOUDINARY_API_SECRET": "", 60 | "CLOUDINARY_SECURE": true 61 | }, 62 | "types": "adonisjs-cloudinary", 63 | "providers": [ 64 | "adonisjs-cloudinary" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /providers/CloudinaryProvider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * adonisjs-cloudinary 3 | * 4 | * (c) Liam Edwards 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 11 | import Cloudinary from '../src/Cloudinary' 12 | import * as cloudinary from 'cloudinary' 13 | 14 | export default class CloudinaryProvider { 15 | public static needsApplication = true 16 | constructor(protected app: ApplicationContract) {} 17 | 18 | public register(): void { 19 | this.app.container.singleton('Adonis/Addons/Cloudinary', _ => { 20 | const config = this.app.container 21 | .resolveBinding('Adonis/Core/Config') 22 | .get('cloudinary', {}) 23 | 24 | return new Cloudinary(config, cloudinary.v2) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Cloudinary.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * adonisjs-cloudinary 3 | * 4 | * (c) Liam Edwards 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { CloudinaryConfig } from '@ioc:Adonis/Addons/Cloudinary' 11 | import { 12 | DeliveryType, 13 | ResourceType, 14 | ResponseCallback, 15 | TransformationOptions, 16 | UploadApiOptions, 17 | UploadApiResponse, 18 | } from 'cloudinary' 19 | import { MultipartFileContract } from '@ioc:Adonis/Core/BodyParser' 20 | 21 | export default class Cloudinary { 22 | private readonly config: CloudinaryConfig 23 | private readonly cloudinary 24 | private uploadResponse: UploadApiResponse 25 | 26 | constructor(config: CloudinaryConfig, cloudinary) { 27 | /* eslint-disable camelcase */ 28 | cloudinary.config({ 29 | cloud_name: config.cloudName, 30 | api_key: config.apiKey, 31 | api_secret: config.apiSecret, 32 | secure: config.secure, 33 | }) 34 | /* eslint-enable camelcase */ 35 | this.config = config 36 | this.cloudinary = cloudinary 37 | } 38 | 39 | public getCloudinary() { 40 | return this.cloudinary 41 | } 42 | 43 | private static getPathFromFile(file: MultipartFileContract): string { 44 | const path = file.tmpPath ?? file.filePath 45 | if (!path) { 46 | throw new Error('File\'s tmpPath or filePath must exist') 47 | } 48 | return path 49 | } 50 | 51 | public async upload( 52 | file: string | MultipartFileContract, 53 | publicId: string|null = null, 54 | uploadOptions: UploadApiOptions = {}, 55 | callback?: ResponseCallback 56 | ) { 57 | let filePath 58 | if (typeof file === 'string') { 59 | filePath = file 60 | } else { 61 | filePath = Cloudinary.getPathFromFile(file) 62 | } 63 | return this.uploadResponse = await this.cloudinary.uploader.upload(filePath, { 64 | /* eslint-disable camelcase */ 65 | public_id: publicId, 66 | /* eslint-enable camelcase */ 67 | ...uploadOptions, 68 | }, callback) 69 | } 70 | 71 | public async unsignedUpload( 72 | file: string | MultipartFileContract, 73 | uploadPreset: string, 74 | publicId: string|null = null, 75 | uploadOptions: UploadApiOptions = {}, 76 | callback?: ResponseCallback 77 | ) { 78 | let filePath 79 | if (typeof file === 'string') { 80 | filePath = file 81 | } else { 82 | filePath = Cloudinary.getPathFromFile(file) 83 | } 84 | return this.uploadResponse = await this.cloudinary.uploader.unsigned_upload( 85 | filePath, 86 | uploadPreset, 87 | { 88 | /* eslint-disable camelcase */ 89 | public_id: publicId, 90 | /* eslint-enable camelcase */ 91 | ...uploadOptions, 92 | }, 93 | callback 94 | ) 95 | } 96 | 97 | public getResult() { 98 | return this.uploadResponse 99 | } 100 | 101 | public getPublicId() { 102 | return this.uploadResponse.public_id 103 | } 104 | 105 | public show(publicId, options: TransformationOptions = {}) { 106 | const defaults = this.config.scaling 107 | // @ts-ignore 108 | options = { ...defaults, ...options } 109 | return this.cloudinary.url(publicId, options) 110 | } 111 | 112 | public secureShow(publicId, options: TransformationOptions = {}) { 113 | // @ts-ignore 114 | return this.show(publicId, { ...options, secure: true }) 115 | } 116 | 117 | public async destroy(publicId, options?: { 118 | /* eslint-disable camelcase */ 119 | resource_type?: ResourceType, 120 | /* eslint-enable camelcase */ 121 | type?: DeliveryType, 122 | invalidate?: boolean, 123 | }) { 124 | return await this.cloudinary.uploader.destroy(publicId, options) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /templates/cloudinary.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * adonisjs-cloudinary 3 | * 4 | * (c) Liam Edwards 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import Env from '@ioc:Adonis/Core/Env' 11 | import { CloudinaryConfig } from '@ioc:Adonis/Addons/Cloudinary' 12 | 13 | const cloudinaryConfig: CloudinaryConfig = { 14 | cloudName: Env.get('CLOUDINARY_CLOUD_NAME'), 15 | apiKey: Env.get('CLOUDINARY_API_KEY'), 16 | apiSecret: Env.get('CLOUDINARY_API_SECRET'), 17 | secure: Env.get('CLOUDINARY_SECURE', true), 18 | 19 | scaling: { 20 | transformation: { 21 | format: 'png', 22 | }, 23 | width: 150, 24 | height: 150, 25 | crop: 'fit', 26 | }, 27 | } 28 | 29 | export default cloudinaryConfig 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", 3 | "files": [ 4 | "./node_modules/@adonisjs/core/build/adonis-typings/index.d.ts" 5 | ], 6 | "compilerOptions": { 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "skipLibCheck": true 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "build" 14 | ] 15 | } 16 | --------------------------------------------------------------------------------