",
6 | "license": "MIT",
7 | "url": "https://github.com/jeffminsungkim/nestjs-multer-extended#readme",
8 | "main": "dist/index.js",
9 | "files": [
10 | "dist"
11 | ],
12 | "scripts": {
13 | "commit": "git-cz",
14 | "coverage": "jest -c ./tests/jest-e2e.json --runInBand --coverage --coverageReporters=text-lcov | coveralls",
15 | "test": "jest --runInBand --coverage",
16 | "test:integration": "jest --config ./tests/jest-e2e.json --runInBand --coverage",
17 | "format": "prettier --write \"lib/**/*.ts\"",
18 | "lint": "tslint -p tsconfig.json -c tslint.json",
19 | "lint:fix": "tslint --fix -c tslint.json 'lib/**/*{.ts,.tsx}'",
20 | "build": "rimraf -rf dist && tsc -p tsconfig.json",
21 | "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md",
22 | "prepublish:npm": "npm run build",
23 | "publish:npm": "npm publish --access public",
24 | "prepublish:next": "npm run build",
25 | "publish:next": "npm publish --access public --tag next"
26 | },
27 | "repository": {
28 | "type": "git",
29 | "url": "https://github.com/jeffminsungkim/nestjs-multer-extended"
30 | },
31 | "keywords": [
32 | "nestjs",
33 | "nest",
34 | "multer",
35 | "multer-sharp",
36 | "sharp",
37 | "file upload",
38 | "file interceptor",
39 | "extend",
40 | "extended",
41 | "aws",
42 | "s3"
43 | ],
44 | "publishConfig": {
45 | "access": "public"
46 | },
47 | "peerDependencies": {
48 | "@nestjs/common": "^6.11.5 || ^7.0.0",
49 | "@nestjs/platform-express": "^6.11.5 || ^7.0.0"
50 | },
51 | "dependencies": {
52 | "aws-sdk": "^2.802.0",
53 | "mime-types": "^2.1.27",
54 | "sharp": "^0.26.0"
55 | },
56 | "devDependencies": {
57 | "@commitlint/cli": "13.1.0",
58 | "@commitlint/config-conventional": "13.1.0",
59 | "@nestjs/common": "7.6.18",
60 | "@nestjs/core": "7.6.18",
61 | "@nestjs/platform-express": "7.6.18",
62 | "@nestjs/testing": "7.6.18",
63 | "@types/express": "4.17.13",
64 | "@types/jest": "27.0.2",
65 | "@types/jest-when": "2.7.3",
66 | "@types/mime-types": "2.1.1",
67 | "@types/multer": "1.4.7",
68 | "@types/node": "14.17.18",
69 | "@types/sharp": "0.26.1",
70 | "@types/sinon": "10.0.3",
71 | "@types/supertest": "2.0.11",
72 | "commitizen": "4.2.4",
73 | "conventional-changelog-cli": "2.1.1",
74 | "coveralls": "3.1.1",
75 | "cz-conventional-changelog": "3.3.0",
76 | "husky": "7.0.2",
77 | "jest": "27.2.1",
78 | "jest-extended": "0.11.5",
79 | "jest-when": "3.4.0",
80 | "lint-staged": "11.1.2",
81 | "nestjs-config": "1.4.8",
82 | "prettier": "2.3.2",
83 | "pretty-quick": "3.1.1",
84 | "reflect-metadata": "0.1.13",
85 | "rxjs": "6.6.7",
86 | "sinon": "11.1.2",
87 | "supertest": "6.1.6",
88 | "ts-jest": "27.0.5",
89 | "ts-node": "10.2.1",
90 | "tsc-watch": "4.5.0",
91 | "tsconfig-paths": "3.11.0",
92 | "tslint": "6.1.3",
93 | "tslint-config-prettier": "1.18.0",
94 | "typescript": "4.0.5"
95 | },
96 | "husky": {
97 | "hooks": {
98 | "pre-commit": "lint-staged",
99 | "commit-message": "commitlint -E HUSKY_GIT_PARAMS"
100 | }
101 | },
102 | "config": {
103 | "commitizen": {
104 | "path": "./node_modules/cz-conventional-changelog"
105 | }
106 | },
107 | "lint-staged": {
108 | "*.ts": [
109 | "pretty-quick",
110 | "tslint -p tsconfig.json"
111 | ],
112 | "*.{js,json}": "pretty-quick"
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "semanticCommits": true,
4 | "packageRules": [
5 | {
6 | "depTypeList": ["devDependencies"],
7 | "automerge": true
8 | }
9 | ],
10 | "schedule": ["every weekday"]
11 | }
12 |
--------------------------------------------------------------------------------
/tests/fixtures/base-path.constants.ts:
--------------------------------------------------------------------------------
1 | export const IMAGE_UPLOAD_MODULE_BASE_PATH = 'image-upload-module';
2 | export const USER_PROFILE_IMAGE_UPLOAD_MODULE_BASE_PATH = 'user-profile-image-upload-module';
3 |
--------------------------------------------------------------------------------
/tests/fixtures/uid.ts:
--------------------------------------------------------------------------------
1 | export const uid = 'aec16138-a75a-4961-b8c1-8e803b6bf2cf';
2 |
--------------------------------------------------------------------------------
/tests/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "verbose": true,
5 | "testEnvironment": "node",
6 | "testRegex": "(.*(e2e-spec))\\.ts$",
7 | "transform": {
8 | "^.+\\.(t|j)s$": "ts-jest"
9 | },
10 | "coverageDirectory": "coverage"
11 | }
12 |
--------------------------------------------------------------------------------
/tests/src/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import { AppModule } from './app.module';
4 | import { MulterExceptions } from '../../lib/multer-sharp/enums';
5 | import { uid } from '../fixtures/uid';
6 | import {
7 | IMAGE_UPLOAD_MODULE_BASE_PATH,
8 | USER_PROFILE_IMAGE_UPLOAD_MODULE_BASE_PATH,
9 | } from '../fixtures/base-path.constants';
10 |
11 | import path from 'path';
12 | import request from 'supertest';
13 |
14 | describe('AppModule', () => {
15 | let app: INestApplication;
16 |
17 | beforeEach(async () => {
18 | const module: TestingModule = await Test.createTestingModule({
19 | imports: [AppModule],
20 | }).compile();
21 |
22 | app = module.createNestApplication();
23 | await app.init();
24 | });
25 |
26 | afterAll(async () => {
27 | await app.close();
28 | });
29 |
30 | describe('ImageUploadController /POST', () => {
31 | const basePath = `${IMAGE_UPLOAD_MODULE_BASE_PATH}`;
32 | const dynamicPath = `${IMAGE_UPLOAD_MODULE_BASE_PATH}/${uid}`;
33 |
34 | it(`should upload an image under the base path "${basePath}"`, async () => {
35 | const res = await request(app.getHttpServer())
36 | .post(`/image-upload/without-dynamic-key-path`)
37 | .set('Content-Type', 'multipart/form-data')
38 | .attach('file', path.resolve(__dirname, 'data/smile.jpg'));
39 |
40 | expect(res.status).toEqual(201);
41 | expect(res.body.key).toEqual(`${basePath}/smile.jpg`);
42 | });
43 |
44 | it(`should upload an image under the dynamic path "${dynamicPath}"`, async () => {
45 | const res = await request(app.getHttpServer())
46 | .post(`/image-upload/with-dynamic-key-path`)
47 | .set('Content-Type', 'multipart/form-data')
48 | .attach('file', path.resolve(__dirname, 'data/smile.jpg'));
49 |
50 | expect(res.status).toEqual(201);
51 | expect(res.body.key).toEqual(`${dynamicPath}/smile.jpg`);
52 | });
53 |
54 | it(`should upload an image with random filename`, async () => {
55 | const res = await request(app.getHttpServer())
56 | .post(`/image-upload/with-random-filename`)
57 | .set('Content-Type', 'multipart/form-data')
58 | .attach('file', path.resolve(__dirname, 'data/smile.jpg'));
59 |
60 | expect(res.status).toEqual(201);
61 | });
62 |
63 | it(`should not upload non image format`, async () => {
64 | const res = await request(app.getHttpServer())
65 | .post(`/image-upload/non-image-file`)
66 | .set('Content-Type', 'multipart/form-data')
67 | .attach('file', path.resolve(__dirname, 'data/Readme.md'));
68 |
69 | expect(res.status).toEqual(400);
70 | expect(res.body.message).toEqual(MulterExceptions.INVALID_IMAGE_FILE_TYPE);
71 | });
72 |
73 | it(`should upload non image format if the file filter is missing`, async () => {
74 | const res = await request(app.getHttpServer())
75 | .post(`/image-upload/non-image-file-no-filter`)
76 | .set('Content-Type', 'multipart/form-data')
77 | .attach('file', path.resolve(__dirname, 'data/Readme.md'));
78 |
79 | expect(res.status).toEqual(201);
80 | expect(res.body.key).toEqual(`${basePath}/Readme.md`);
81 | });
82 |
83 | it(`should not upload an image when its size exceed the limit`, async () => {
84 | const res = await request(app.getHttpServer())
85 | .post(`/image-upload/big-size-file`)
86 | .set('Content-Type', 'multipart/form-data')
87 | .attach('file', path.resolve(__dirname, 'data/cat.jpg'));
88 |
89 | expect(res.status).toEqual(413);
90 | expect(res.body.message).toEqual(MulterExceptions.LIMIT_FILE_SIZE);
91 | });
92 |
93 | it(`should upload an image when the size limit option sets higher than the file size`, async () => {
94 | const res = await request(app.getHttpServer())
95 | .post(`/image-upload/big-size-file-higher-limit`)
96 | .set('Content-Type', 'multipart/form-data')
97 | .attach('file', path.resolve(__dirname, 'data/cat.jpg'));
98 |
99 | expect(res.status).toEqual(201);
100 | expect(res.body.key).toEqual(`${basePath}/cat.jpg`);
101 | });
102 | });
103 |
104 | describe('UserProfileImageUploadController /POST', () => {
105 | const basePath = `${USER_PROFILE_IMAGE_UPLOAD_MODULE_BASE_PATH}`;
106 | const dynamicPath = `${USER_PROFILE_IMAGE_UPLOAD_MODULE_BASE_PATH}/${uid}`;
107 |
108 | it(`should upload an image under the base path "${basePath}"`, async () => {
109 | const res = await request(app.getHttpServer())
110 | .post(`/user-profile-image-upload/without-dynamic-key-path`)
111 | .set('Content-Type', 'multipart/form-data')
112 | .attach('file', path.resolve(__dirname, 'data/crying.jpg'));
113 |
114 | expect(res.status).toEqual(201);
115 | expect(res.body.key).toEqual(`${basePath}/crying.jpg`);
116 | });
117 |
118 | it(`should upload an image under the dynamic path "${dynamicPath}"`, async () => {
119 | const res = await request(app.getHttpServer())
120 | .post(`/user-profile-image-upload/with-dynamic-key-path`)
121 | .set('Content-Type', 'multipart/form-data')
122 | .attach('file', path.resolve(__dirname, 'data/crying.jpg'));
123 |
124 | expect(res.status).toEqual(201);
125 | expect(res.body.key).toEqual(`${dynamicPath}/crying.jpg`);
126 | });
127 |
128 | it(`should upload an image under the first path parameter :key(abcd1234)`, async () => {
129 | const res = await request(app.getHttpServer())
130 | .post(`/user-profile-image-upload/use-path-param-as-a-key/abcd1234`)
131 | .set('Content-Type', 'multipart/form-data')
132 | .attach('file', path.resolve(__dirname, 'data/crying.jpg'));
133 |
134 | expect(res.status).toEqual(201);
135 | expect(res.body.key).toEqual(`user-profile-image-upload-module/abcd1234/crying.jpg`);
136 | });
137 |
138 | it(`should upload an image under :key(abcd1234)/:id(msk)`, async () => {
139 | const res = await request(app.getHttpServer())
140 | .post(`/user-profile-image-upload/use-path-param-as-a-key/abcd1234/user/msk`)
141 | .set('Content-Type', 'multipart/form-data')
142 | .attach('file', path.resolve(__dirname, 'data/crying.jpg'));
143 |
144 | expect(res.status).toEqual(201);
145 | expect(res.body.key).toEqual(`user-profile-image-upload-module/abcd1234/msk/crying.jpg`);
146 | });
147 |
148 | it(`should upload both an original and a thumbnail image`, async () => {
149 | const res = await request(app.getHttpServer())
150 | .post(`/user-profile-image-upload/create-thumbnail-with-custom-options`)
151 | .set('Content-Type', 'multipart/form-data')
152 | .attach('file', path.resolve(__dirname, 'data/crying.jpg'));
153 | const { thumbnail, original } = res.body;
154 |
155 | expect(res.status).toEqual(201);
156 | expect(thumbnail.width).toEqual(250);
157 | expect(thumbnail.height).toEqual(250);
158 | expect(thumbnail.key).toEqual(`${basePath}/crying.jpg-thumbnail`);
159 | expect(original.key).toEqual(`${basePath}/crying.jpg-original`);
160 | });
161 |
162 | it(`should upload thumb and original under the dynamic path "${dynamicPath}/test"`, async () => {
163 | const res = await request(app.getHttpServer())
164 | .post(`/user-profile-image-upload/create-thumbnail-with-dynamic-key`)
165 | .set('Content-Type', 'multipart/form-data')
166 | .attach('file', path.resolve(__dirname, 'data/crying.jpg'));
167 | const { thumb, original } = res.body;
168 |
169 | expect(res.status).toEqual(201);
170 | expect(thumb.width).toEqual(200);
171 | expect(thumb.height).toEqual(200);
172 | expect(thumb.key).toEqual(`${dynamicPath}/test/crying.jpg-thumb`);
173 | expect(original.key).toEqual(`${dynamicPath}/test/crying.jpg-original`);
174 | });
175 |
176 | it(`should upload resized image`, async () => {
177 | const res = await request(app.getHttpServer())
178 | .post(`/user-profile-image-upload/resized`)
179 | .set('Content-Type', 'multipart/form-data')
180 | .attach('file', path.resolve(__dirname, 'data/go.jpeg'));
181 |
182 | expect(res.status).toEqual(201);
183 | expect(res.body.width).toEqual(500);
184 | expect(res.body.height).toEqual(450);
185 | expect(res.body.key).toEqual(`${basePath}/go.jpeg`);
186 | });
187 |
188 | it(`should upload images in different sizes`, async () => {
189 | const res = await request(app.getHttpServer())
190 | .post(`/user-profile-image-upload/different-sizes`)
191 | .set('Content-Type', 'multipart/form-data')
192 | .attach('file', path.resolve(__dirname, 'data/cat.jpg'));
193 | const { sm, md, lg } = res.body;
194 |
195 | expect(res.status).toEqual(201);
196 | expect(sm.width).toEqual(200);
197 | expect(sm.height).toEqual(200);
198 | expect(md.height).toEqual(300);
199 | expect(md.height).toEqual(300);
200 | expect(lg.height).toEqual(400);
201 | expect(lg.height).toEqual(400);
202 | expect(sm.key).toEqual(`${dynamicPath}/different-sizes/cat.jpg-sm`);
203 | expect(md.key).toEqual(`${dynamicPath}/different-sizes/cat.jpg-md`);
204 | expect(lg.key).toEqual(`${dynamicPath}/different-sizes/cat.jpg-lg`);
205 | });
206 | });
207 | });
208 |
--------------------------------------------------------------------------------
/tests/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigModule } from 'nestjs-config';
3 | import { UserProfileImageUploadModule } from './user-profile-image-upload/user-profile-upload.module';
4 | import { ImageUploadModule } from './image-upload/image-upload.module';
5 | import path from 'path';
6 |
7 | @Module({
8 | imports: [
9 | ConfigModule.load(path.resolve(__dirname, 'config', '**', '!(*.d).{ts,js}')),
10 | UserProfileImageUploadModule,
11 | ImageUploadModule,
12 | ],
13 | })
14 | export class AppModule {}
15 |
--------------------------------------------------------------------------------
/tests/src/config/aws.ts:
--------------------------------------------------------------------------------
1 | import { MulterExtendedS3Options } from '../../../lib/interfaces';
2 | import {
3 | IMAGE_UPLOAD_MODULE_BASE_PATH,
4 | USER_PROFILE_IMAGE_UPLOAD_MODULE_BASE_PATH,
5 | } from '../../fixtures/base-path.constants';
6 | import { Logger } from '@nestjs/common';
7 |
8 | export default {
9 | optionA: {
10 | accessKeyId: process.env.AWS_ACCESS_KEY_ID,
11 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
12 | region: process.env.AWS_S3_REGION,
13 | bucket: process.env.AWS_S3_BUCKET_NAME,
14 | basePath: USER_PROFILE_IMAGE_UPLOAD_MODULE_BASE_PATH,
15 | fileSize: process.env.AWS_S3_MAX_IMAGE_SIZE,
16 | } as MulterExtendedS3Options,
17 | optionB: {
18 | accessKeyId: process.env.AWS_ACCESS_KEY_ID,
19 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
20 | region: process.env.AWS_S3_REGION,
21 | bucket: process.env.AWS_S3_BUCKET_NAME,
22 | basePath: IMAGE_UPLOAD_MODULE_BASE_PATH,
23 | fileSize: 1 * 1024 * 1024,
24 | acl: 'private',
25 | logger: new Logger('Test Logger'),
26 | } as MulterExtendedS3Options,
27 | };
28 |
--------------------------------------------------------------------------------
/tests/src/data/Readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
6 | [circleci-url]: https://circleci.com/gh/nestjs/nest
7 |
8 | A progressive Node.js framework for building efficient and scalable server-side applications.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 | ## Description
26 |
27 | Nest is a framework for building efficient, scalable
28 | Node.js server-side applications. It uses modern
29 | JavaScript, is built with TypeScript
30 | (preserves compatibility with pure JavaScript) and combines elements of OOP (Object Oriented
31 | Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).
32 |
33 | Under the hood, Nest makes use of Express, but also, provides compatibility with a wide range of other libraries, like e.g. Fastify, allowing for easy use of the myriad third-party plugins which are available.
34 |
35 | ## Philosophy
36 |
37 | In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like Angular, React and Vue which improve developer productivity and enable the construction of fast, testable, extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers and tools for Node, none of them effectively solve the main problem - the architecture.
38 | Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, loosely coupled and easily maintainable applications.
39 |
40 | ## Getting started
41 |
42 | - To check out the [guide](https://docs.nestjs.com), visit
43 | [docs.nestjs.com](https://docs.nestjs.com). :books:
44 | - 要查看中文 [指南](readme_zh.md), 请访问 [docs.nestjs.cn](https://docs.nestjs.cn). :books:
45 |
46 | ## Consulting
47 |
48 | With official support, you can get expert help straight from Nest core team. We provide dedicated
49 | technical support, migration strategies, advice on best practices (and design decisions), PR
50 | reviews, and team augmentation. Read more about [support here](https://enterprise.nestjs.com).
51 |
52 | ## Support
53 |
54 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the
55 | amazing backers. If you'd like to join them, please
56 | [read more here](https://docs.nestjs.com/support).
57 |
58 | #### Principal Sponsor
59 |
60 |
61 |
62 | #### Silver Sponsors
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | #### Sponsors
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | ## Backers
114 |
115 |
116 |
117 | ## Stay in touch
118 |
119 | - Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
120 | - Website - [https://nestjs.com](https://nestjs.com/)
121 | - Twitter - [@nestframework](https://twitter.com/nestframework)
122 |
123 | ## License
124 |
125 | Nest is [MIT licensed](LICENSE).
126 |
--------------------------------------------------------------------------------
/tests/src/data/cat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffminsungkim/nestjs-multer-extended/45c45f6b62c8c8719dbeb8ce2e0c688050ed242c/tests/src/data/cat.jpg
--------------------------------------------------------------------------------
/tests/src/data/crying.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffminsungkim/nestjs-multer-extended/45c45f6b62c8c8719dbeb8ce2e0c688050ed242c/tests/src/data/crying.jpg
--------------------------------------------------------------------------------
/tests/src/data/go.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffminsungkim/nestjs-multer-extended/45c45f6b62c8c8719dbeb8ce2e0c688050ed242c/tests/src/data/go.jpeg
--------------------------------------------------------------------------------
/tests/src/data/smile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeffminsungkim/nestjs-multer-extended/45c45f6b62c8c8719dbeb8ce2e0c688050ed242c/tests/src/data/smile.jpg
--------------------------------------------------------------------------------
/tests/src/image-upload/image-upload.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
2 | import { AmazonS3FileInterceptor } from '../../../lib/interceptors';
3 | import { uid } from '../../fixtures/uid';
4 |
5 | @Controller('image-upload')
6 | export class ImageUploadController {
7 | @Post('without-dynamic-key-path')
8 | @UseInterceptors(AmazonS3FileInterceptor('file'))
9 | async uploadImageWithoutKeyOption(@UploadedFile() file: any): Promise {
10 | return file;
11 | }
12 |
13 | @Post('with-dynamic-key-path')
14 | @UseInterceptors(AmazonS3FileInterceptor('file', { dynamicPath: uid }))
15 | async uploadImageWithKeyOption(@UploadedFile() file: any): Promise {
16 | return file;
17 | }
18 |
19 | @Post('with-random-filename')
20 | @UseInterceptors(AmazonS3FileInterceptor('file', { randomFilename: true }))
21 | async uploadImageWithRandomFilenameKeyOption(@UploadedFile() file: any): Promise {
22 | return file;
23 | }
24 |
25 | @Post('non-image-file')
26 | @UseInterceptors(AmazonS3FileInterceptor('file'))
27 | async uploadNonImageFile(@UploadedFile() file: any): Promise {}
28 |
29 | @Post('non-image-file-no-filter')
30 | @UseInterceptors(AmazonS3FileInterceptor('file', { fileFilter: undefined }))
31 | async uploadNonImageFileWithoutFilter(@UploadedFile() file: any): Promise {
32 | return file;
33 | }
34 |
35 | @Post('big-size-file')
36 | @UseInterceptors(AmazonS3FileInterceptor('file'))
37 | async uploadBigImage(@UploadedFile() file: any): Promise {}
38 |
39 | @Post('big-size-file-higher-limit')
40 | @UseInterceptors(AmazonS3FileInterceptor('file', { limits: { fileSize: 3 * 1024 * 1024 } }))
41 | async uploadBigImageUsingCustomLimitOption(@UploadedFile() file: any): Promise {
42 | return file;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/src/image-upload/image-upload.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ImageUploadController } from './image-upload.controller';
3 | import { MulterExtendedModule } from '../../../lib/multer-extended.module';
4 | import { ConfigService } from 'nestjs-config';
5 |
6 | @Module({
7 | imports: [
8 | MulterExtendedModule.registerAsync({
9 | useFactory: (configService: ConfigService) => configService.get('aws.optionB'),
10 | inject: [ConfigService],
11 | }),
12 | ],
13 | controllers: [ImageUploadController],
14 | })
15 | export class ImageUploadModule {}
16 |
--------------------------------------------------------------------------------
/tests/src/user-profile-image-upload/user-profile-image-upload.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
2 | import { AmazonS3FileInterceptor } from '../../../lib/interceptors';
3 | import { uid } from '../../fixtures/uid';
4 |
5 | @Controller('user-profile-image-upload')
6 | export class UserProfileImageUploadController {
7 | @Post('without-dynamic-key-path')
8 | @UseInterceptors(AmazonS3FileInterceptor('file'))
9 | async uploadImageWithoutKeyOption(@UploadedFile() file: any): Promise {
10 | return file;
11 | }
12 |
13 | @Post('with-dynamic-key-path')
14 | @UseInterceptors(AmazonS3FileInterceptor('file', { dynamicPath: uid }))
15 | async uploadImageWithKeyOption(@UploadedFile() file: any): Promise {
16 | return file;
17 | }
18 |
19 | @Post('use-path-param-as-a-key/:key')
20 | @UseInterceptors(AmazonS3FileInterceptor('file', { dynamicPath: 'key' }))
21 | async uploadImageWithPathParamKey(@UploadedFile() file: any): Promise {
22 | return file;
23 | }
24 |
25 | @Post('use-path-param-as-a-key/:key/user/:id')
26 | @UseInterceptors(AmazonS3FileInterceptor('file', { dynamicPath: ['key', 'id'] }))
27 | async uploadImageWithMultiplePathParamKeys(@UploadedFile() file: any): Promise {
28 | return file;
29 | }
30 |
31 | @Post('create-thumbnail-with-custom-options')
32 | @UseInterceptors(
33 | AmazonS3FileInterceptor('file', {
34 | thumbnail: { suffix: 'thumbnail', width: 250, height: 250 },
35 | limits: { fileSize: 7 * 1024 * 1024 },
36 | }),
37 | )
38 | async uploadImageWithThumbnail(@UploadedFile() file: any): Promise {
39 | return file;
40 | }
41 |
42 | @Post('create-thumbnail-with-dynamic-key')
43 | @UseInterceptors(
44 | AmazonS3FileInterceptor('file', {
45 | thumbnail: { suffix: 'thumb', width: 200, height: 200 },
46 | limits: { fileSize: 2 * 1024 * 1024 },
47 | dynamicPath: `${uid}/test`,
48 | }),
49 | )
50 | async uploadImageWithDynamicKey(@UploadedFile() file: any): Promise {
51 | return file;
52 | }
53 |
54 | @Post('resized')
55 | @UseInterceptors(
56 | AmazonS3FileInterceptor('file', {
57 | resize: { width: 500, height: 450 },
58 | }),
59 | )
60 | async uploadResizedImage(@UploadedFile() file: any): Promise {
61 | return file;
62 | }
63 |
64 | @Post('different-sizes')
65 | @UseInterceptors(
66 | AmazonS3FileInterceptor('file', {
67 | resizeMultiple: [
68 | { suffix: 'sm', width: 200, height: 200 },
69 | { suffix: 'md', width: 300, height: 300 },
70 | { suffix: 'lg', width: 400, height: 400 },
71 | ],
72 | dynamicPath: `${uid}/different-sizes`,
73 | }),
74 | )
75 | async uploadImageWithDifferentSizes(@UploadedFile() file: any): Promise {
76 | return file;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/tests/src/user-profile-image-upload/user-profile-upload.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { UserProfileImageUploadController } from './user-profile-image-upload.controller';
3 | import { MulterExtendedModule } from '../../../lib/multer-extended.module';
4 | import { ConfigService } from 'nestjs-config';
5 |
6 | @Module({
7 | imports: [
8 | MulterExtendedModule.registerAsync({
9 | useFactory: (configService: ConfigService) => configService.get('aws.optionA'),
10 | inject: [ConfigService],
11 | }),
12 | ],
13 | controllers: [UserProfileImageUploadController],
14 | })
15 | export class UserProfileImageUploadModule {}
16 |
--------------------------------------------------------------------------------
/tests/unit/multer-sharp/multer-sharp.utils.spec.ts:
--------------------------------------------------------------------------------
1 | import 'jest-extended';
2 | import { HttpException, PayloadTooLargeException, BadRequestException } from '@nestjs/common';
3 | import AWS from 'aws-sdk';
4 | import {
5 | transformImage,
6 | isOriginalSuffix,
7 | getSharpOptionProps,
8 | getSharpOptions,
9 | transformException,
10 | } from '../../../lib/multer-sharp/multer-sharp.utils';
11 | import { S3StorageOptions } from '../../../lib/multer-sharp/interfaces/s3-storage.interface';
12 | import { SharpOptions } from '../../../lib/multer-sharp/interfaces/sharp-options.interface';
13 | import { MulterExceptions } from '../../../lib/multer-sharp/enums';
14 |
15 | describe('Shared Multer Sharp Utils', () => {
16 | describe('transformException', () => {
17 | describe('if error does not exist', () => {
18 | it('behave as identity', () => {
19 | const err = undefined;
20 | expect(transformException(err)).toEqual(err);
21 | });
22 | });
23 | describe('if error is instance of HttpException', () => {
24 | it('behave as identity', () => {
25 | const err = new HttpException('response', 500);
26 | expect(transformException(err)).toEqual(err);
27 | });
28 | });
29 | describe('if error exists and is not instance of HttpException', () => {
30 | describe('and is LIMIT_FILE_SIZE exception', () => {
31 | it('should return "PayloadTooLargeException"', () => {
32 | const err = { message: MulterExceptions.LIMIT_FILE_SIZE };
33 | expect(transformException(err as any)).toBeInstanceOf(PayloadTooLargeException);
34 | });
35 | });
36 | describe('and is multer exception but not a LIMIT_FILE_SIZE', () => {
37 | it('should return "BadRequestException"', () => {
38 | const err = { message: MulterExceptions.INVALID_IMAGE_FILE_TYPE };
39 | expect(transformException(err as any)).toBeInstanceOf(BadRequestException);
40 | });
41 | });
42 | });
43 | });
44 |
45 | describe('transformImage', () => {
46 | it('should return resolved image stream when the property is resize', () => {
47 | const option: SharpOptions = {
48 | resize: { width: 300, height: 350 },
49 | };
50 | expect(transformImage(option, option.resize)).toBeObject();
51 | });
52 | });
53 |
54 | describe('isOriginalSuffix', () => {
55 | it('should return true when the suffix is original', () => {
56 | expect(isOriginalSuffix('original')).toBeTruthy();
57 | });
58 |
59 | it('should return false when the suffix is not original', () => {
60 | expect(isOriginalSuffix('thumbnail')).toBeFalsy();
61 | });
62 | });
63 |
64 | describe('getSharpOptionProps', () => {
65 | let storageOpts: S3StorageOptions;
66 |
67 | beforeEach(() => {
68 | storageOpts = {
69 | s3: new AWS.S3(),
70 | resizeMultiple: [
71 | { suffix: 'xs', width: 100, height: 100 },
72 | { suffix: 'sm', width: 200, height: 200 },
73 | { suffix: 'md', width: 300, height: 300 },
74 | { suffix: 'lg', width: 400, height: 400 },
75 | ],
76 | resize: { width: 500, height: 450 },
77 | };
78 | });
79 |
80 | describe('The first set of property serves first', () => {
81 | it('should return an array of storage options when the resizeMultiple property set before the resize property', () => {
82 | expect(getSharpOptionProps(storageOpts)).toEqual([
83 | { suffix: 'xs', width: 100, height: 100 },
84 | { suffix: 'sm', width: 200, height: 200 },
85 | { suffix: 'md', width: 300, height: 300 },
86 | { suffix: 'lg', width: 400, height: 400 },
87 | ]);
88 | });
89 |
90 | it('should return an object value of resize property when the resizeMultiple property is missing', () => {
91 | delete storageOpts.resizeMultiple;
92 |
93 | expect(getSharpOptionProps(storageOpts)).toEqual({ width: 500, height: 450 });
94 | });
95 | });
96 | });
97 |
98 | describe('getSharpOptions', () => {
99 | let options: SharpOptions;
100 |
101 | beforeEach(() => {
102 | options = {
103 | resize: { width: 500, height: 450 },
104 | ignoreAspectRatio: true,
105 | };
106 | });
107 |
108 | it('should return SharpOptions', () => {
109 | expect(getSharpOptions(options)).toContainAllEntries([
110 | ['resizeMultiple', undefined],
111 | ['resize', { width: 500, height: 450 }],
112 | ['ignoreAspectRatio', true],
113 | ]);
114 | });
115 | });
116 | });
117 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "tests", "media", "dist"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "esModuleInterop": true,
9 | "target": "es6",
10 | "sourceMap": false,
11 | "outDir": "./dist",
12 | "rootDir": "./lib",
13 | "baseUrl": "./",
14 | "noLib": false,
15 | "noImplicitAny": false,
16 | "skipLibCheck": true
17 | },
18 | "include": ["lib/**/*.ts"],
19 | "exclude": ["node_modules", "dist"]
20 | }
21 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:recommended", "tslint-config-prettier"],
3 | "rules": {
4 | "arrow-return-shorthand": true,
5 | "callable-types": true,
6 | "class-name": true,
7 | "comment-format": [true, "check-space"],
8 | "curly": true,
9 | "eofline": true,
10 | "forin": true,
11 | "import-spacing": true,
12 | "indent": [true, "spaces"],
13 | "interface-over-type-literal": true,
14 | "label-position": true,
15 | "max-line-length": [true, 140],
16 | "max-union-size": false,
17 | "member-access": false,
18 | "member-ordering": [
19 | true,
20 | {
21 | "order": [
22 | "static-field",
23 | "instance-field",
24 | "static-method",
25 | "instance-method"
26 | ]
27 | }
28 | ],
29 | "no-arg": true,
30 | "no-bitwise": true,
31 | "no-console": true,
32 | "no-construct": true,
33 | "no-debugger": true,
34 | "no-duplicate-super": true,
35 | "no-empty": false,
36 | "no-empty-interface": true,
37 | "no-eval": true,
38 | "no-inferrable-types": [true, "ignore-params"],
39 | "no-misused-new": true,
40 | "no-non-null-assertion": true,
41 | "no-shadowed-variable": true,
42 | "no-string-literal": false,
43 | "no-string-throw": true,
44 | "no-switch-case-fall-through": true,
45 | "no-trailing-whitespace": true,
46 | "no-unnecessary-initializer": true,
47 | "no-unused-expression": true,
48 | "no-var-keyword": true,
49 | "object-literal-sort-keys": false,
50 | "one-line": [
51 | true,
52 | "check-open-brace",
53 | "check-catch",
54 | "check-else",
55 | "check-whitespace"
56 | ],
57 | "prefer-const": true,
58 | "quotemark": [true, "single", "warn"],
59 | "radix": false,
60 | "semicolon": [true, "always", "ignore-interfaces"],
61 | "trailing-comma": true,
62 | "triple-equals": [true, "allow-null-check"],
63 | "typedef-whitespace": [
64 | true,
65 | {
66 | "call-signature": "nospace",
67 | "index-signature": "nospace",
68 | "parameter": "nospace",
69 | "property-declaration": "nospace",
70 | "variable-declaration": "nospace"
71 | }
72 | ],
73 | "unified-signatures": true,
74 | "variable-name": false,
75 | "whitespace": [
76 | true,
77 | "check-branch",
78 | "check-decl",
79 | "check-operator",
80 | "check-separator",
81 | "check-type"
82 | ],
83 | "no-implicit-dependencies": false,
84 | "no-submodule-imports": false,
85 | "interface-name": false
86 | }
87 | }
88 |
--------------------------------------------------------------------------------