Skip to content

Commit 2e274da

Browse files
committed
Feature Typeorm support
1 parent 3b6402d commit 2e274da

File tree

11 files changed

+131
-28
lines changed

11 files changed

+131
-28
lines changed

README.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ A powerful and flexible NestJS module for integrating MinIO object storage into
2727
- `@FileUpload()` - Handles multiple file uploads with built-in validation
2828
- `@FileField()` - Swagger-ready DTO field decorator
2929
- `@FileSchemaField()` - Mongoose schema integration for file fields
30+
- 🗃️ `@FileColumn()` decorator for TypeORM or any class-based model
3031
- 📁 Complete MinIO operations support (upload, download, delete, etc.)
3132
- 🔧 Configurable module options
3233
- 🎯 TypeScript support
3334
- 📝 Swagger documentation support
3435
- 🔄 RxJS integration
36+
- 🤖 Automatic presigned URL detection even for raw QueryBuilder results
3537

3638
## Installation
3739

@@ -176,7 +178,26 @@ export class UserController {
176178
}
177179
```
178180

179-
4. (Optional) Add file fields to your Mongoose schema:
181+
4. (Optional) Add file fields to your persistence models:
182+
183+
### TypeORM example
184+
185+
```typescript
186+
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
187+
import { FileColumn } from 'nestjs-minio-backend';
188+
189+
@Entity()
190+
export class User {
191+
@PrimaryGeneratedColumn('uuid')
192+
id: string;
193+
194+
@FileColumn({ bucketName: 'profiles' })
195+
@Column({ nullable: true })
196+
avatar?: string; // Stores the MinIO object path (bucket/objectName)
197+
}
198+
```
199+
200+
### Mongoose example
180201

181202
```typescript
182203
import { FileSchemaField } from 'nestjs-minio-backend';
@@ -196,6 +217,7 @@ These decorators provide:
196217
- 📝 Automatic Swagger documentation
197218
- ✅ Built-in validation
198219
- 🔄 Seamless MongoDB integration
220+
- 🤖 Automatic presigned URL generation even for raw QueryBuilder objects (bucket names are auto-detected from your MinIO config)
199221
- 🎯 Type safety with TypeScript
200222

201223
## Configuration
@@ -368,7 +390,7 @@ Swagger-ready DTO field decorator for file uploads.
368390
```
369391

370392
#### @FileSchemaField()
371-
Mongoose schema integration for file fields.
393+
Mongoose schema integration for file fields (wraps `@Prop` plus `@FileColumn` metadata).
372394

373395
```typescript
374396
@FileSchemaField({
@@ -377,6 +399,15 @@ Mongoose schema integration for file fields.
377399
})
378400
```
379401

402+
#### @FileColumn()
403+
Database-agnostic decorator for marking entity properties that store MinIO object references. Works with TypeORM, Mongoose, or any class-based model.
404+
405+
```typescript
406+
@FileColumn({
407+
bucketName?: string // Optionally enforce a specific bucket
408+
})
409+
```
410+
380411
## Contributing
381412

382413
1. Fork it ([https://github.com/UtilKit/nestjs-minio-backend/fork](https://github.com/UtilKit/nestjs-minio-backend/fork))

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nestjs-minio-backend",
3-
"version": "1.0.12",
3+
"version": "1.0.14",
44
"description": "NestJS module for MinIO integration",
55
"author": "Mishhub",
66
"license": "MIT",
@@ -60,10 +60,10 @@
6060
"prepublishOnly": "npm run build"
6161
},
6262
"peerDependencies": {
63-
"@nestjs/common": "^10.0.0",
64-
"@nestjs/core": "^10.0.0",
65-
"@nestjs/mongoose": "^10.0.0",
66-
"@nestjs/platform-express": "^10.0.0",
63+
"@nestjs/common": "^10.0.0 || ^11.0.0",
64+
"@nestjs/core": "^10.0.0 || ^11.0.0",
65+
"@nestjs/mongoose": "^10.0.0 || ^11.0.0",
66+
"@nestjs/platform-express": "^10.0.0 || ^11.0.0",
6767
"@nestjs/swagger": "^8.0.0",
6868
"class-validator": "^0.14.1",
6969
"minio": "^8.0.5",

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export const MINIO_CONFIG = 'MINIO_CONFIG';
2+
export const MINIO_FILE_FIELD_METADATA = 'MINIO_FILE_FIELD_METADATA';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { applyDecorators } from '@nestjs/common';
2+
import { MINIO_FILE_FIELD_METADATA } from '../constants';
3+
4+
export interface FileColumnOptions {
5+
bucketName?: string;
6+
}
7+
8+
export function FileColumn(options: FileColumnOptions = {}): PropertyDecorator {
9+
return applyDecorators((target: object, propertyKey: string | symbol) => {
10+
Reflect.defineMetadata(
11+
MINIO_FILE_FIELD_METADATA,
12+
{
13+
bucketName: options.bucketName,
14+
},
15+
target,
16+
propertyKey,
17+
);
18+
});
19+
}
20+

src/decorators/file-field.decorator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { applyDecorators } from '@nestjs/common';
22
import { ApiProperty } from '@nestjs/swagger';
33
import { IsOptional } from 'class-validator';
4+
import { FileColumn } from './file-column.decorator';
45

56
export interface FileFieldOptions {
67
bucketName: string;
@@ -13,6 +14,7 @@ export function FileField(options: FileFieldOptions): PropertyDecorator {
1314

1415
// Store metadata on the property
1516
return applyDecorators(
17+
FileColumn({ bucketName }),
1618
ApiProperty({
1719
type: 'string',
1820
format: 'binary',
Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1+
import { applyDecorators } from '@nestjs/common';
12
import { Prop, PropOptions } from '@nestjs/mongoose';
3+
import { FileColumn, FileColumnOptions } from './file-column.decorator';
24

3-
export type FileSchemaFieldOptions = PropOptions & {
4-
bucketName?: string;
5-
};
5+
export type FileSchemaFieldOptions = PropOptions & FileColumnOptions;
66

77
export function FileSchemaField(options: FileSchemaFieldOptions = {}): PropertyDecorator {
8-
// Add a metadata marker to identify this as a file field
98
const fileOptions = {
109
...(options as object),
1110
isFileField: true,
1211
bucketName: options.bucketName || 'media-files-bucket',
1312
};
1413

15-
// Use the standard Prop decorator with our custom metadata
16-
return Prop(fileOptions);
14+
return applyDecorators(FileColumn(options), Prop(fileOptions));
1715
}

src/decorators/file-upload.decorator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { applyDecorators, UseInterceptors } from '@nestjs/common';
22
import { FileFieldsInterceptor } from '@nestjs/platform-express';
33
import { ApiConsumes } from '@nestjs/swagger';
4-
import { FileFieldConfig } from 'src/interfaces/file-field.interface';
4+
import { FileFieldConfig } from '../interfaces/file-field.interface';
55
import { MinioFileInterceptor } from '../interceptors/file.interceptor';
66

77
export function FileUpload(fileFields: FileFieldConfig[]): PropertyDecorator {

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './decorators/file-field.decorator';
22
export * from './decorators/file-upload.decorator';
33
export * from './decorators/file-schema-field.decorator';
4+
export * from './decorators/file-column.decorator';
45
export * from './minio.service';
56
export * from './minio.module';
67
export * from './interfaces/file.interface';

src/interceptors/file-url-transform.interceptor.ts

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { map } from 'rxjs/operators';
44
import { MinioService } from '../minio.service';
55
import { Socket } from 'net';
66
import { IncomingMessage, ServerResponse } from 'http';
7+
import { MINIO_FILE_FIELD_METADATA } from '../constants';
78

89
@Injectable()
910
export class FileUrlTransformInterceptor implements NestInterceptor {
@@ -34,7 +35,7 @@ export class FileUrlTransformInterceptor implements NestInterceptor {
3435
* @returns The transformed data with URLs replaced by presigned URLs.
3536
*/
3637
// eslint-disable-next-line @typescript-eslint/no-explicit-any
37-
private async transformUrls(data: any): Promise<any> {
38+
private async transformUrls(data: any, visited = new WeakSet<object>()): Promise<any> {
3839
if (!data) return data;
3940

4041
// Skip processing for Node.js internal objects (HTTP, Socket, etc.)
@@ -47,6 +48,13 @@ export class FileUrlTransformInterceptor implements NestInterceptor {
4748
return data;
4849
}
4950

51+
if (typeof data === 'object' && data !== null) {
52+
if (visited.has(data)) {
53+
return data;
54+
}
55+
visited.add(data);
56+
}
57+
5058
// If it's a mongoose document, convert to plain object
5159
const obj = data.toJSON ? data.toJSON() : data;
5260

@@ -60,14 +68,19 @@ export class FileUrlTransformInterceptor implements NestInterceptor {
6068

6169
// Process each property recursively
6270
for (const [key, value] of Object.entries(obj)) {
63-
// Check if this field is decorated with FileSchemaField
64-
const isFileField = schema?.paths?.[key]?.options?.isFileField;
71+
// Check if this field is decorated with FileSchemaField or FileColumn
72+
const isFileField =
73+
schema?.paths?.[key]?.options?.isFileField ||
74+
this.hasFileFieldMetadata(data, key) ||
75+
this.hasFileFieldMetadata(obj, key);
6576

66-
if (isFileField && typeof value === 'string' && value.includes('/')) {
67-
const [bucketName, ...pathParts] = value.split('/');
68-
if (pathParts.length > 0) {
77+
const inferredPath = !isFileField ? this.extractMinioPath(value) : null;
78+
79+
if ((isFileField || inferredPath) && typeof value === 'string') {
80+
const split = inferredPath ?? this.splitBucketAndObject(value);
81+
if (split) {
6982
try {
70-
obj[key] = await this.minioService.getPresignedUrl(bucketName, pathParts.join('/'));
83+
obj[key] = await this.minioService.getPresignedUrl(split.bucketName, split.objectName);
7184
} catch (error) {
7285
this.logger.error(`Error generating presigned URL for ${key}:`, error);
7386
}
@@ -77,21 +90,59 @@ export class FileUrlTransformInterceptor implements NestInterceptor {
7790
else if (
7891
value &&
7992
typeof value === 'object' &&
80-
!Array.isArray(value) &&
81-
Object.getPrototypeOf(value) === Object.prototype
93+
!Array.isArray(value)
8294
) {
83-
obj[key] = await this.transformUrls(value);
95+
obj[key] = await this.transformUrls(value, visited);
8496
}
8597
// Handle arrays of objects recursively
8698
else if (Array.isArray(value)) {
8799
obj[key] = await Promise.all(
88100
value.map((item) =>
89-
typeof item === 'object' && item !== null ? this.transformUrls(item) : item,
101+
typeof item === 'object' && item !== null ? this.transformUrls(item, visited) : item,
90102
),
91103
);
92104
}
93105
}
94106

95107
return obj;
96108
}
109+
110+
private hasFileFieldMetadata(target: unknown, propertyKey: string): boolean {
111+
if (!target) return false;
112+
113+
const directMetadata = Reflect.getMetadata(MINIO_FILE_FIELD_METADATA, target, propertyKey);
114+
if (directMetadata) {
115+
return true;
116+
}
117+
118+
const prototype = typeof target === 'object' ? Object.getPrototypeOf(target) : undefined;
119+
if (!prototype) {
120+
return false;
121+
}
122+
123+
return Boolean(Reflect.getMetadata(MINIO_FILE_FIELD_METADATA, prototype, propertyKey));
124+
}
125+
126+
private extractMinioPath(value: unknown): { bucketName: string; objectName: string } | null {
127+
if (typeof value !== 'string') {
128+
return null;
129+
}
130+
131+
const split = this.splitBucketAndObject(value);
132+
return split;
133+
}
134+
135+
private splitBucketAndObject(value: string): { bucketName: string; objectName: string } | null {
136+
if (!value.includes('/')) {
137+
return null;
138+
}
139+
const [bucketName, ...pathParts] = value.split('/');
140+
if (!bucketName || pathParts.length === 0) {
141+
return null;
142+
}
143+
return {
144+
bucketName,
145+
objectName: pathParts.join('/'),
146+
};
147+
}
97148
}

0 commit comments

Comments
 (0)