Skip to content

Commit

Permalink
Merge pull request #72 from getlarge/71-feat-create-lib-to-handle-fil…
Browse files Browse the repository at this point in the history
…e-upload-for-fastify-based-apps

feat: create lib to handle file upload for fastify based apps
  • Loading branch information
getlarge authored Jun 13, 2024
2 parents 0c873ce + 8268d0c commit 30cdce4
Show file tree
Hide file tree
Showing 40 changed files with 1,773 additions and 23 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This repository contains several helpful packages for NestJS that I have develop
| [Async-Local-Storage](./packages/async-local-storage/README.md) | A NestJS module to provide async local storage for your application | [![npm](https://img.shields.io/npm/v/@getlarge/nestjs-tools-async-local-storage?style=flat)](https://npmjs.org/package/@getlarge/nestjs-tools-async-local-storage) |
| [File-Storage](./packages/file-storage/README.md) | A NestJS module supporting FS and S3 strategies | [![npm](https://img.shields.io/npm/v/@getlarge/nestjs-tools-file-storage?style=flat)](https://npmjs.org/package/@getlarge/nestjs-tools-file-storage) |
| [Lock](./packages/lock/README.md) | A NestJS module to provide a distributed lock for your application | [![npm](https://img.shields.io/npm/v/@getlarge/nestjs-tools-lock?style=flat)](https://npmjs.org/package/@getlarge/nestjs-tools-lock) |
| [Fastify-Upload](./packages/fastify-upload/README.md) | A NestJS module to provide file upload support for Fastify | [![npm](https://img.shields.io/npm/v/@getlarge/nestjs-tools-fastify-upload?style=flat)](https://npmjs.org/package/@getlarge/nestjs-tools-fastify-upload) |

## Installation and usage

Expand All @@ -28,4 +29,5 @@ Check the README of each package for more details.

Check out the following projects for examples of using these packages:

- the [ticketing repository](https://github.com/getlarge/ticketing) is a real-world example of AMQP-Transport, File-Storage, Lock,
- The [ticketing repository](https://github.com/getlarge/ticketing) is a real-world example of AMQP-Transport, File-Storage, Lock
- The [cat fostering repository](https://github.com/getlarge/catfostering) uses Fastify-Upload
531 changes: 510 additions & 21 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,13 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.554.0",
"@aws-sdk/lib-storage": "^3.554.0",
"@fastify/multipart": "^8.0.0",
"@nestjs/common": "^10.0.2",
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.0.2",
"@nestjs/microservices": "^10.0.2",
"@nestjs/platform-express": "^10.0.2",
"@nestjs/platform-fastify": "^10.0.2",
"amqp-connection-manager": "^4.1.14",
"amqplib": "^0.10.4",
"ioredis": "^5.3.2",
Expand Down
33 changes: 33 additions & 0 deletions packages/fastify-upload/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/dependency-checks": [
"error",
{
"buildTargets": ["build"],
"checkMissingDependencies": true,
"checkObsoleteDependencies": true,
"checkVersionMismatches": true
}
]
}
}
]
}
117 changes: 117 additions & 0 deletions packages/fastify-upload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Fastify Upload

[![npm][npm-image]][npm-url]

[npm-image]: https://img.shields.io/npm/v/@getlarge/nestjs-tools-fastify-upload.svg?style=flat
[npm-url]: https://npmjs.org/package/@getlarge/nestjs-tools-fastify-upload

The Fastify Upload Service provides file upload support for NestJS applications using the Fastify adapter.
It uses the [@fastify/multipart](https://www.npmjs.com/package/@fastify/multipart) module.

The [nestjs-tools-file-storage](../file-storage/README.md) library can store the uploaded files in various storage backends.

## Installation

```bash
$ npm install --save @getlarge/nestjs-tools-fastify-upload
```

## Usage

```ts
import { Controller, Post, StreamableFile, UseInterceptors } from '@nestjs/common';
import { join } from 'node:path';

import {
AnyFilesInterceptor,
DiskStorage,
DiskStorageFile,
FileFieldsInterceptor,
FileInterceptor,
FilesInterceptor,
MemoryStorage,
MemoryStorageFile,
StreamStorage,
StreamStorageFile,
UploadedFile,
UploadedFiles,
} from '@getlarge/nestjs-tools-fastify-upload';

@Controller()
export class AppController {
@Post('single')
@UseInterceptors(
FileInterceptor('file', {
storage: new MemoryStorage(),
}),
)
uploadSingleFile(@UploadedFile() file: MemoryStorageFile): {
success: boolean;
} {
return { success: !!file };
}

@Post('multiple')
@UseInterceptors(FilesInterceptor('file', 10, { storage: new MemoryStorage() }))
uploadMultipleFiles(@UploadedFiles() files: MemoryStorageFile[]): {
success: boolean;
fileCount: number;
} {
return { success: !!files.length, fileCount: files.length };
}

@Post('any')
@UseInterceptors(AnyFilesInterceptor({ storage: new MemoryStorage() }))
uploadAnyFiles(@UploadedFiles() files: MemoryStorageFile[]): {
success: boolean;
fileCount: number;
} {
return { success: !!files.length, fileCount: files.length };
}

@Post('fields')
@UseInterceptors(
FileFieldsInterceptor([{ name: 'profile' }, { name: 'avatar' }], {
storage: new MemoryStorage(),
}),
)
uploadFileFieldsFiles(
@UploadedFiles()
files: {
profile?: MemoryStorageFile[];
avatar?: MemoryStorageFile[];
},
): { success: boolean; fileCount: number } {
return {
success: !!((files.profile?.length ?? 0) + (files.avatar?.length ?? 0)),
fileCount: (files.profile?.length ?? 0) + (files.avatar?.length ?? 0),
};
}

@Post('single-stream')
@UseInterceptors(
FileInterceptor('file', {
storage: new StreamStorage(),
}),
)
streamSingleFile(@UploadedFile() file: StreamStorageFile): StreamableFile {
return new StreamableFile(file.stream);
}

@Post('single-disk')
@UseInterceptors(
FileInterceptor('file', {
storage: new DiskStorage({
removeAfter: true,
}),
dest: join(process.cwd(), 'uploads'),
}),
)
persistSingleFile(@UploadedFile() file: DiskStorageFile): {
success: boolean;
filepath: string;
} {
return { success: !!file, filepath: file.path };
}
}
```
11 changes: 11 additions & 0 deletions packages/fastify-upload/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'nestjs-tools-fastify-upload',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/packages/fastify-upload',
};
30 changes: 30 additions & 0 deletions packages/fastify-upload/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@getlarge/nestjs-tools-fastify-upload",
"version": "0.0.1",
"description": "Fastify Upload utilies for NestJS",
"keywords": [
"fastify",
"file",
"upload",
"nestjs"
],
"license": "Apache-2.0",
"author": "Edouard Maleix <[email protected]>",
"homepage": "https://github.com/getlarge/nestjs-tools/tree/main/packages/fastify-upload",
"publishConfig": {
"access": "public"
},
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@nestjs/common": "10",
"fastify": "4",
"@fastify/busboy": "2",
"@fastify/multipart": "8",
"rxjs": "7"
},
"type": "commonjs",
"main": "./src/index.js",
"typings": "./src/index.d.ts"
}
20 changes: 20 additions & 0 deletions packages/fastify-upload/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "nestjs-tools-fastify-upload",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/fastify-upload/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/fastify-upload",
"tsConfig": "packages/fastify-upload/tsconfig.lib.json",
"packageJson": "packages/fastify-upload/package.json",
"main": "packages/fastify-upload/src/index.ts",
"assets": ["packages/fastify-upload/*.md"]
}
}
}
}
4 changes: 4 additions & 0 deletions packages/fastify-upload/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './lib/decorators';
export * from './lib/interceptors';
export * from './lib/multipart';
export * from './lib/storage';
2 changes: 2 additions & 0 deletions packages/fastify-upload/src/lib/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './uploaded-file.decorator';
export * from './uploaded-files.decorator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

import { getMultipartRequest } from '../multipart/request';
import type { StorageFile } from '../storage/storage';

export const UploadedFile = createParamDecorator((data: unknown, ctx: ExecutionContext): StorageFile | undefined => {
const req = getMultipartRequest(ctx.switchToHttp());
return req?.storageFile;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

import { getMultipartRequest } from '../multipart/request';
import type { StorageFile } from '../storage/storage';

export const UploadedFiles = createParamDecorator(
(data: unknown, ctx: ExecutionContext): Record<string, StorageFile[]> | StorageFile[] | undefined => {
const req = getMultipartRequest(ctx.switchToHttp());
return req?.storageFiles;
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { CallHandler, ExecutionContext, mixin, NestInterceptor, Type } from '@nestjs/common';
import { Observable, tap } from 'rxjs';

import { handleMultipartAnyFiles } from '../multipart/handlers/any-files';
import { type TransformedUploadOptions, transformUploadOptions, type UploadOptions } from '../multipart/options';
import { getMultipartRequest } from '../multipart/request';
import type { Storage } from '../storage';

export function AnyFilesInterceptor<S extends Storage>(options?: UploadOptions<S>): Type<NestInterceptor> {
class MixinInterceptor implements NestInterceptor {
private readonly options: TransformedUploadOptions<S>;

constructor() {
this.options = transformUploadOptions<S>(options);
}

async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<void>> {
const ctx = context.switchToHttp();
const req = getMultipartRequest(ctx);

const { body, files, remove } = await handleMultipartAnyFiles<S>(req, this.options);

req.body = body;
req.storageFiles = files;

return next.handle().pipe(tap(remove));
}
}

return mixin(MixinInterceptor);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { CallHandler, ExecutionContext, mixin, NestInterceptor, Type } from '@nestjs/common';
import { Observable, tap } from 'rxjs';

import {
handleMultipartFileFields,
UploadField,
UploadFieldMapEntry,
uploadFieldsToMap,
} from '../multipart/handlers/file-fields';
import { type TransformedUploadOptions, transformUploadOptions, type UploadOptions } from '../multipart/options';
import { getMultipartRequest } from '../multipart/request';
import type { Storage } from '../storage';

export function FileFieldsInterceptor<S extends Storage>(
uploadFields: UploadField[],
options?: UploadOptions<S>,
): Type<NestInterceptor> {
class MixinInterceptor implements NestInterceptor {
private readonly options: TransformedUploadOptions<S>;
private readonly fieldsMap: Map<string, UploadFieldMapEntry>;

constructor() {
this.options = transformUploadOptions(options);
this.fieldsMap = uploadFieldsToMap(uploadFields);
}

async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<void>> {
const ctx = context.switchToHttp();
const req = getMultipartRequest(ctx);

const { body, files, remove } = await handleMultipartFileFields(req, this.fieldsMap, this.options);

req.body = body;
req.storageFiles = files;

return next.handle().pipe(tap(remove));
}
}

return mixin(MixinInterceptor);
}
32 changes: 32 additions & 0 deletions packages/fastify-upload/src/lib/interceptors/file.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { CallHandler, ExecutionContext, mixin, NestInterceptor, Type } from '@nestjs/common';
import { Observable, tap } from 'rxjs';

import { handleMultipartSingleFile } from '../multipart/handlers/single-file';
import { type TransformedUploadOptions, transformUploadOptions, type UploadOptions } from '../multipart/options';
import { getMultipartRequest } from '../multipart/request';
import type { Storage } from '../storage';

export function FileInterceptor<S extends Storage>(
fieldname: string,
options?: UploadOptions<S>,
): Type<NestInterceptor> {
class MixinInterceptor implements NestInterceptor {
private readonly options: TransformedUploadOptions<S>;

constructor() {
this.options = transformUploadOptions(options);
}

async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<void>> {
const ctx = context.switchToHttp();
const req = getMultipartRequest(ctx);

const { file, body, remove } = await handleMultipartSingleFile(req, fieldname, this.options);
req.body = body;
req.storageFile = file;
return next.handle().pipe(tap(remove));
}
}

return mixin(MixinInterceptor);
}
Loading

0 comments on commit 30cdce4

Please sign in to comment.