Skip to content

Commit

Permalink
Merge pull request #26 from getlarge/25-feat-create-fastify-upload-lib
Browse files Browse the repository at this point in the history
feat: create Fastify upload lib
  • Loading branch information
getlarge authored Feb 9, 2024
2 parents 7ebc9f5 + 051b60f commit 4f8b685
Show file tree
Hide file tree
Showing 38 changed files with 1,351 additions and 18 deletions.
17 changes: 0 additions & 17 deletions infra/ory/keto/keto.yaml

This file was deleted.

18 changes: 18 additions & 0 deletions libs/microservices/shared/fastify-multipart/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
7 changes: 7 additions & 0 deletions libs/microservices/shared/fastify-multipart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# microservices-shared-fastify-multipart

This library was generated with [Nx](https://nx.dev).

## Running unit tests

Run `nx test microservices-shared-fastify-multipart` to execute the unit tests via [Jest](https://jestjs.io).
12 changes: 12 additions & 0 deletions libs/microservices/shared/fastify-multipart/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable */
export default {
displayName: 'microservices-shared-fastify-multipart',
preset: '../../../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory:
'../../../../coverage/libs/microservices/shared/fastify-multipart',
};
25 changes: 25 additions & 0 deletions libs/microservices/shared/fastify-multipart/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "microservices-shared-fastify-multipart",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/microservices/shared/fastify-multipart/src",
"projectType": "library",
"targets": {
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"libs/microservices/shared/fastify-multipart/**/*.ts"
]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/microservices/shared/fastify-multipart/jest.config.ts"
}
}
},
"tags": ["scope:shared", "type:utils", "platform:server"]
}
4 changes: 4 additions & 0 deletions libs/microservices/shared/fastify-multipart/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';
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,11 @@
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,14 @@
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,51 @@
import {
CallHandler,
ExecutionContext,
mixin,
NestInterceptor,
Type,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';

import { handleMultipartAnyFiles } from '../multipart/handlers/any-files';
import {
type TransformedUploadOptions,
type UploadOptions,
transformUploadOptions,
} 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));
}
}

const Interceptor = mixin(MixinInterceptor);

return Interceptor;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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,
type UploadOptions,
transformUploadOptions,
} 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));
}
}

const Interceptor = mixin(MixinInterceptor);

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

import { handleMultipartSingleFile } from '../multipart/handlers/single-file';
import {
type TransformedUploadOptions,
type UploadOptions,
transformUploadOptions,
} 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));
}
}

const Interceptor = mixin(MixinInterceptor);

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

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

export function FilesInterceptor<S extends Storage>(
fieldname: string,
maxCount = 1,
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 { body, files, remove } = await handleMultipartMultipleFiles(
req,
fieldname,
maxCount,
this.options,
);

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

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

const Interceptor = mixin(MixinInterceptor);

return Interceptor;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './any-files.interceptor';
export * from './file.interceptor';
export * from './file-fields.interceptor';
export * from './files.interceptor';
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
BadRequestException,
HttpException,
PayloadTooLargeException,
} from '@nestjs/common';

export function transformException(err: undefined): undefined;
export function transformException(err: HttpException): HttpException;
export function transformException(err: Error): Error;
export function transformException(
err: Error & { code: string },
): HttpException;
export function transformException(
err: Error | undefined,
): Error | undefined | HttpException {
if (
!err ||
err instanceof HttpException ||
!('code' in err) ||
typeof err.code !== 'string'
) {
return err;
}

switch (err.code) {
case 'FST_REQ_FILE_TOO_LARGE':
return new PayloadTooLargeException();
case 'FST_PARTS_LIMIT':
case 'FST_FILES_LIMIT':
case 'FST_PROTO_VIOLATION':
case 'FST_INVALID_MULTIPART_CONTENT_TYPE':
return new BadRequestException(err.message);
}

return err;
}
Loading

0 comments on commit 4f8b685

Please sign in to comment.