@venizia/ignis-docs 0.0.1-4 → 0.0.1-6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/mcp-server/dist/common/config.d.ts +58 -21
  2. package/mcp-server/dist/common/config.d.ts.map +1 -1
  3. package/mcp-server/dist/common/config.js +67 -12
  4. package/mcp-server/dist/common/config.js.map +1 -1
  5. package/mcp-server/dist/helpers/docs.helper.d.ts +5 -5
  6. package/mcp-server/dist/helpers/docs.helper.d.ts.map +1 -1
  7. package/mcp-server/dist/helpers/docs.helper.js +31 -27
  8. package/mcp-server/dist/helpers/docs.helper.js.map +1 -1
  9. package/mcp-server/dist/helpers/github.helper.d.ts +37 -0
  10. package/mcp-server/dist/helpers/github.helper.d.ts.map +1 -0
  11. package/mcp-server/dist/helpers/github.helper.js +100 -0
  12. package/mcp-server/dist/helpers/github.helper.js.map +1 -0
  13. package/mcp-server/dist/helpers/index.d.ts +1 -0
  14. package/mcp-server/dist/helpers/index.d.ts.map +1 -1
  15. package/mcp-server/dist/helpers/index.js +1 -0
  16. package/mcp-server/dist/helpers/index.js.map +1 -1
  17. package/mcp-server/dist/index.js +60 -32
  18. package/mcp-server/dist/index.js.map +1 -1
  19. package/mcp-server/dist/tools/base.tool.d.ts +6 -10
  20. package/mcp-server/dist/tools/base.tool.d.ts.map +1 -1
  21. package/mcp-server/dist/tools/base.tool.js +3 -5
  22. package/mcp-server/dist/tools/base.tool.js.map +1 -1
  23. package/mcp-server/dist/tools/{get-doc-content.tool.d.ts → docs/get-document-content.tool.d.ts} +4 -4
  24. package/mcp-server/dist/tools/docs/get-document-content.tool.d.ts.map +1 -0
  25. package/mcp-server/dist/tools/{get-doc-content.tool.js → docs/get-document-content.tool.js} +12 -11
  26. package/mcp-server/dist/tools/docs/get-document-content.tool.js.map +1 -0
  27. package/mcp-server/dist/tools/{get-doc-metadata.tool.d.ts → docs/get-document-metadata.tool.d.ts} +4 -4
  28. package/mcp-server/dist/tools/docs/get-document-metadata.tool.d.ts.map +1 -0
  29. package/mcp-server/dist/tools/{get-doc-metadata.tool.js → docs/get-document-metadata.tool.js} +11 -10
  30. package/mcp-server/dist/tools/docs/get-document-metadata.tool.js.map +1 -0
  31. package/mcp-server/dist/tools/docs/get-package-overview.tool.d.ts +50 -0
  32. package/mcp-server/dist/tools/docs/get-package-overview.tool.d.ts.map +1 -0
  33. package/mcp-server/dist/tools/docs/get-package-overview.tool.js +221 -0
  34. package/mcp-server/dist/tools/docs/get-package-overview.tool.js.map +1 -0
  35. package/mcp-server/dist/tools/docs/index.d.ts +7 -0
  36. package/mcp-server/dist/tools/docs/index.d.ts.map +1 -0
  37. package/mcp-server/dist/tools/docs/index.js +23 -0
  38. package/mcp-server/dist/tools/docs/index.js.map +1 -0
  39. package/mcp-server/dist/tools/{list-categories.tool.d.ts → docs/list-categories.tool.d.ts} +2 -2
  40. package/mcp-server/dist/tools/docs/list-categories.tool.d.ts.map +1 -0
  41. package/mcp-server/dist/tools/{list-categories.tool.js → docs/list-categories.tool.js} +7 -6
  42. package/mcp-server/dist/tools/docs/list-categories.tool.js.map +1 -0
  43. package/mcp-server/dist/tools/{list-docs.tool.d.ts → docs/list-documents.tool.d.ts} +4 -4
  44. package/mcp-server/dist/tools/docs/list-documents.tool.d.ts.map +1 -0
  45. package/mcp-server/dist/tools/{list-docs.tool.js → docs/list-documents.tool.js} +10 -9
  46. package/mcp-server/dist/tools/docs/list-documents.tool.js.map +1 -0
  47. package/mcp-server/dist/tools/{search-docs.tool.d.ts → docs/search-documents.tool.d.ts} +4 -4
  48. package/mcp-server/dist/tools/docs/search-documents.tool.d.ts.map +1 -0
  49. package/mcp-server/dist/tools/{search-docs.tool.js → docs/search-documents.tool.js} +19 -18
  50. package/mcp-server/dist/tools/docs/search-documents.tool.js.map +1 -0
  51. package/mcp-server/dist/tools/github/index.d.ts +5 -0
  52. package/mcp-server/dist/tools/github/index.d.ts.map +1 -0
  53. package/mcp-server/dist/tools/github/index.js +21 -0
  54. package/mcp-server/dist/tools/github/index.js.map +1 -0
  55. package/mcp-server/dist/tools/github/list-project-files.tool.d.ts +28 -0
  56. package/mcp-server/dist/tools/github/list-project-files.tool.d.ts.map +1 -0
  57. package/mcp-server/dist/tools/github/list-project-files.tool.js +98 -0
  58. package/mcp-server/dist/tools/github/list-project-files.tool.js.map +1 -0
  59. package/mcp-server/dist/tools/github/search-code.tool.d.ts +42 -0
  60. package/mcp-server/dist/tools/github/search-code.tool.d.ts.map +1 -0
  61. package/mcp-server/dist/tools/github/search-code.tool.js +194 -0
  62. package/mcp-server/dist/tools/github/search-code.tool.js.map +1 -0
  63. package/mcp-server/dist/tools/github/verify-dependencies.tool.d.ts +55 -0
  64. package/mcp-server/dist/tools/github/verify-dependencies.tool.d.ts.map +1 -0
  65. package/mcp-server/dist/tools/github/verify-dependencies.tool.js +167 -0
  66. package/mcp-server/dist/tools/github/verify-dependencies.tool.js.map +1 -0
  67. package/mcp-server/dist/tools/github/view-source-file.tool.d.ts +26 -0
  68. package/mcp-server/dist/tools/github/view-source-file.tool.d.ts.map +1 -0
  69. package/mcp-server/dist/tools/github/view-source-file.tool.js +91 -0
  70. package/mcp-server/dist/tools/github/view-source-file.tool.js.map +1 -0
  71. package/mcp-server/dist/tools/index.d.ts +3 -7
  72. package/mcp-server/dist/tools/index.d.ts.map +1 -1
  73. package/mcp-server/dist/tools/index.js +17 -13
  74. package/mcp-server/dist/tools/index.js.map +1 -1
  75. package/package.json +25 -6
  76. package/wiki/get-started/best-practices/api-usage-examples.md +42 -0
  77. package/wiki/get-started/best-practices/architectural-patterns.md +42 -1
  78. package/wiki/get-started/best-practices/code-style-standards.md +41 -0
  79. package/wiki/get-started/best-practices/contribution-workflow.md +40 -6
  80. package/wiki/get-started/best-practices/data-modeling.md +126 -0
  81. package/wiki/get-started/core-concepts/dependency-injection.md +13 -1
  82. package/wiki/get-started/mcp-docs-server.md +130 -32
  83. package/wiki/get-started/philosophy.md +198 -25
  84. package/wiki/public/logo.svg +1 -0
  85. package/wiki/references/base/application.md +5 -5
  86. package/wiki/references/base/controllers.md +3 -1
  87. package/wiki/references/base/dependency-injection.md +6 -5
  88. package/wiki/references/base/models.md +319 -1
  89. package/wiki/references/components/index.md +3 -1
  90. package/wiki/references/components/static-asset.md +1289 -0
  91. package/wiki/references/helpers/inversion.md +21 -11
  92. package/wiki/references/helpers/storage.md +538 -11
  93. package/wiki/references/src-details/core.md +15 -1
  94. package/wiki/references/src-details/docs.md +19 -9
  95. package/wiki/references/src-details/mcp-server.md +185 -234
  96. package/wiki/references/utilities/index.md +1 -1
  97. package/wiki/references/utilities/request.md +150 -0
  98. package/mcp-server/dist/tools/get-doc-content.tool.d.ts.map +0 -1
  99. package/mcp-server/dist/tools/get-doc-content.tool.js.map +0 -1
  100. package/mcp-server/dist/tools/get-doc-metadata.tool.d.ts.map +0 -1
  101. package/mcp-server/dist/tools/get-doc-metadata.tool.js.map +0 -1
  102. package/mcp-server/dist/tools/list-categories.tool.d.ts.map +0 -1
  103. package/mcp-server/dist/tools/list-categories.tool.js.map +0 -1
  104. package/mcp-server/dist/tools/list-docs.tool.d.ts.map +0 -1
  105. package/mcp-server/dist/tools/list-docs.tool.js.map +0 -1
  106. package/mcp-server/dist/tools/search-docs.tool.d.ts.map +0 -1
  107. package/mcp-server/dist/tools/search-docs.tool.js.map +0 -1
@@ -0,0 +1,1289 @@
1
+ # Static Asset Component
2
+
3
+ The Static Asset Component provides a flexible, extensible file management system with support for multiple storage backends through a unified interface.
4
+
5
+ ## Overview
6
+
7
+ | Feature | Description |
8
+ |---------|-------------|
9
+ | **Component** | `StaticAssetComponent` |
10
+ | **Architecture** | Factory-based controller generation with unified storage interface |
11
+ | **Storage Types** | `DiskHelper` (local filesystem), `MinioHelper` (S3-compatible) |
12
+ | **Extensibility** | Easy to add new storage backends (S3, Azure Blob, Google Cloud Storage) |
13
+ | **Dependencies** | Node.js `fs`, `path`, `stream`; MinIO client (optional) |
14
+
15
+ ## Key Features
16
+
17
+ ✅ **Unified Storage Interface** - Single API for all storage types
18
+ ✅ **Multiple Storage Instances** - Configure multiple storage backends simultaneously
19
+ ✅ **Factory Pattern** - Dynamic controller generation
20
+ ✅ **Built-in Security** - Comprehensive name validation, path traversal protection
21
+ ✅ **Type-Safe** - Full TypeScript support with strict interfaces
22
+ ✅ **Flexible Configuration** - Environment-based, production-ready setup
23
+ ✅ **Database Tracking (MetaLink)** - Optional database-backed file tracking with metadata
24
+
25
+ ---
26
+
27
+ ## Architecture
28
+
29
+ ### Storage Helper Hierarchy
30
+
31
+ ```typescript
32
+ IStorageHelper (interface)
33
+
34
+ BaseStorageHelper (abstract class)
35
+
36
+ ├── DiskHelper (local filesystem)
37
+ └── MinioHelper (S3-compatible)
38
+ ```
39
+
40
+ ### Component Flow
41
+
42
+ ```
43
+ Application Configuration
44
+
45
+ StaticAssetComponent
46
+
47
+ AssetControllerFactory
48
+
49
+ Dynamic Controller(s) ← uses → IStorageHelper
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Installation & Setup
55
+
56
+ ### Complete Setup Example
57
+
58
+ Here's a real-world example from the Vert application showing how to configure storage backends:
59
+
60
+ ```typescript
61
+ import {
62
+ applicationEnvironment,
63
+ BaseApplication,
64
+ DiskHelper,
65
+ int,
66
+ MinioHelper,
67
+ StaticAssetComponent,
68
+ StaticAssetComponentBindingKeys,
69
+ StaticAssetStorageTypes,
70
+ TStaticAssetsComponentOptions,
71
+ ValueOrPromise,
72
+ } from '@venizia/ignis';
73
+ import { EnvironmentKeys } from './common/environments';
74
+
75
+ export class Application extends BaseApplication {
76
+ configureComponents(): void {
77
+ // Configure Static Asset Component
78
+ this.bind<TStaticAssetsComponentOptions>({
79
+ key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
80
+ }).toValue({
81
+ // MinIO storage for user uploads and media
82
+ staticAsset: {
83
+ controller: {
84
+ name: 'AssetController',
85
+ basePath: '/assets',
86
+ isStrict: true,
87
+ },
88
+ storage: StaticAssetStorageTypes.MINIO,
89
+ helper: new MinioHelper({
90
+ endPoint: applicationEnvironment.get(EnvironmentKeys.APP_ENV_MINIO_HOST),
91
+ port: int(applicationEnvironment.get(EnvironmentKeys.APP_ENV_MINIO_API_PORT)),
92
+ accessKey: applicationEnvironment.get(EnvironmentKeys.APP_ENV_MINIO_ACCESS_KEY),
93
+ secretKey: applicationEnvironment.get(EnvironmentKeys.APP_ENV_MINIO_SECRET_KEY),
94
+ useSSL: false,
95
+ }),
96
+ extra: {
97
+ parseMultipartBody: {
98
+ storage: 'memory',
99
+ },
100
+ },
101
+ },
102
+ // Local disk storage for temporary files and cache
103
+ staticResource: {
104
+ controller: {
105
+ name: 'ResourceController',
106
+ basePath: '/resources',
107
+ isStrict: true,
108
+ },
109
+ storage: StaticAssetStorageTypes.DISK,
110
+ helper: new DiskHelper({
111
+ basePath: './app_data/resources',
112
+ }),
113
+ extra: {
114
+ parseMultipartBody: {
115
+ storage: 'memory',
116
+ },
117
+ },
118
+ },
119
+ });
120
+
121
+ // Register the component
122
+ this.component(StaticAssetComponent);
123
+ }
124
+
125
+ preConfigure() {
126
+ this.configureComponents();
127
+ }
128
+ }
129
+ ```
130
+
131
+ **Key Configuration Elements:**
132
+ - Each storage backend gets a unique key (`staticAsset`, `staticResource`)
133
+ - Each backend has its own controller configuration (name, basePath)
134
+ - Storage type is explicitly set using `StaticAssetStorageTypes`
135
+ - Helper instances are created with environment variables
136
+ - Extra options configure multipart body parsing
137
+
138
+ ### Environment Variables
139
+
140
+ Add these to your `.env` file:
141
+
142
+ ```bash
143
+ # MinIO Configuration
144
+ APP_ENV_MINIO_HOST=localhost
145
+ APP_ENV_MINIO_API_PORT=9000
146
+ APP_ENV_MINIO_ACCESS_KEY=minioadmin
147
+ APP_ENV_MINIO_SECRET_KEY=minioadmin
148
+ ```
149
+
150
+ ### Environment Keys Configuration
151
+
152
+ Define the environment keys in your application:
153
+
154
+ ```typescript
155
+ // src/common/environments.ts
156
+ import { EnvironmentKeys as BaseEnv } from '@venizia/ignis';
157
+
158
+ export class EnvironmentKeys extends BaseEnv {
159
+ // MinIO Configuration Keys
160
+ static readonly APP_ENV_MINIO_HOST = 'APP_ENV_MINIO_HOST';
161
+ static readonly APP_ENV_MINIO_API_PORT = 'APP_ENV_MINIO_API_PORT';
162
+ static readonly APP_ENV_MINIO_ACCESS_KEY = 'APP_ENV_MINIO_ACCESS_KEY';
163
+ static readonly APP_ENV_MINIO_SECRET_KEY = 'APP_ENV_MINIO_SECRET_KEY';
164
+ }
165
+ ```
166
+
167
+ ### Configuration Options
168
+
169
+ #### `TStaticAssetsComponentOptions`
170
+
171
+ ```typescript
172
+ type TStaticAssetsComponentOptions = {
173
+ [key: string]: {
174
+ // Controller configuration
175
+ controller: {
176
+ name: string; // Controller class name
177
+ basePath: string; // Base URL path (e.g., '/assets')
178
+ isStrict?: boolean; // Strict routing mode (default: true)
179
+ };
180
+
181
+ // Storage configuration
182
+ storage: 'disk' | 'minio'; // Storage type
183
+ helper: IStorageHelper; // Storage helper instance
184
+
185
+ // Extra options
186
+ extra?: {
187
+ parseMultipartBody?: {
188
+ storage?: 'memory' | 'disk';
189
+ uploadDir?: string;
190
+ };
191
+ normalizeNameFn?: (opts: { originalName: string; folderPath?: string }) => string;
192
+ normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
193
+ };
194
+ } & (
195
+ // MetaLink configuration (optional)
196
+ | { useMetaLink?: false }
197
+ | { useMetaLink: true; metaLink: TMetaLinkConfig }
198
+ );
199
+ };
200
+
201
+ type TMetaLinkConfig<Schema extends TMetaLinkSchema = TMetaLinkSchema> = {
202
+ model: typeof BaseEntity<Schema>; // MetaLink model class
203
+ repository: DefaultCRUDRepository<Schema>; // MetaLink repository instance
204
+ };
205
+ ```
206
+
207
+ ### Quick Start Options
208
+
209
+ **Option 1: MinIO Only**
210
+ ```typescript
211
+ this.bind({
212
+ key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
213
+ }).toValue({
214
+ cloudStorage: {
215
+ controller: { name: 'CloudController', basePath: '/cloud' },
216
+ storage: StaticAssetStorageTypes.MINIO,
217
+ helper: new MinioHelper({ /* ... */ }),
218
+ extra: { parseMultipartBody: { storage: 'memory' } },
219
+ },
220
+ });
221
+ this.component(StaticAssetComponent);
222
+ ```
223
+
224
+ **Option 2: Local Disk Only**
225
+ ```typescript
226
+ this.bind({
227
+ key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
228
+ }).toValue({
229
+ localStorage: {
230
+ controller: { name: 'LocalController', basePath: '/files' },
231
+ storage: StaticAssetStorageTypes.DISK,
232
+ helper: new DiskHelper({ basePath: './uploads' }),
233
+ extra: { parseMultipartBody: { storage: 'disk' } },
234
+ },
235
+ });
236
+ this.component(StaticAssetComponent);
237
+ ```
238
+
239
+ **Option 3: Multiple Storage Backends (Recommended)**
240
+ ```typescript
241
+ // Use different storage types for different purposes
242
+ this.bind({
243
+ key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
244
+ }).toValue({
245
+ userUploads: {
246
+ controller: { name: 'UploadsController', basePath: '/uploads' },
247
+ storage: StaticAssetStorageTypes.MINIO,
248
+ helper: new MinioHelper({ /* ... */ }),
249
+ extra: {},
250
+ },
251
+ tempFiles: {
252
+ controller: { name: 'TempController', basePath: '/temp' },
253
+ storage: StaticAssetStorageTypes.DISK,
254
+ helper: new DiskHelper({ basePath: './temp' }),
255
+ extra: {},
256
+ },
257
+ publicAssets: {
258
+ controller: { name: 'PublicController', basePath: '/public' },
259
+ storage: StaticAssetStorageTypes.DISK,
260
+ helper: new DiskHelper({ basePath: './public' }),
261
+ extra: {},
262
+ },
263
+ });
264
+ this.component(StaticAssetComponent);
265
+ ```
266
+
267
+ ---
268
+
269
+ ## MetaLink: Database File Tracking
270
+
271
+ MetaLink is an optional feature that tracks uploaded files in a database, enabling advanced file management, querying, and metadata storage.
272
+
273
+ ### What is MetaLink?
274
+
275
+ MetaLink creates a database record for every uploaded file, storing:
276
+ - File location (bucket, object name, access link)
277
+ - File metadata (mimetype, size, etag)
278
+ - Storage type (disk or minio)
279
+ - Timestamps (created, modified)
280
+ - Custom metadata (JSONB field)
281
+
282
+ ### Benefits
283
+
284
+ ✅ **Query uploaded files** - Find files by bucket, name, mimetype, etc.
285
+ ✅ **Track file history** - Know when files were uploaded
286
+ ✅ **Store metadata** - Keep custom information about files
287
+ ✅ **Database integration** - Associate files with other entities
288
+ ✅ **Audit trail** - Track what was uploaded and when
289
+ ✅ **Graceful errors** - Upload succeeds even if MetaLink creation fails
290
+
291
+ ### Database Schema
292
+
293
+ **Table:** `MetaLink`
294
+
295
+ ```sql
296
+ CREATE TABLE "MetaLink" (
297
+ id TEXT PRIMARY KEY,
298
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
299
+ modified_at TIMESTAMP NOT NULL DEFAULT NOW(),
300
+ bucket_name TEXT NOT NULL,
301
+ object_name TEXT NOT NULL,
302
+ link TEXT NOT NULL,
303
+ mimetype TEXT NOT NULL,
304
+ size INTEGER NOT NULL,
305
+ etag TEXT,
306
+ metadata JSONB,
307
+ storage_type TEXT NOT NULL,
308
+ is_synced BOOLEAN NOT NULL DEFAULT false
309
+ );
310
+
311
+ CREATE INDEX "IDX_MetaLink_bucketName" ON "MetaLink"(bucket_name);
312
+ CREATE INDEX "IDX_MetaLink_objectName" ON "MetaLink"(object_name);
313
+ CREATE INDEX "IDX_MetaLink_storageType" ON "MetaLink"(storage_type);
314
+ CREATE INDEX "IDX_MetaLink_isSynced" ON "MetaLink"(is_synced);
315
+ ```
316
+
317
+ **Schema Fields:**
318
+
319
+ | Field | Type | Description |
320
+ |-------|------|-------------|
321
+ | `id` | TEXT | Primary key (UUID) |
322
+ | `created_at` | TIMESTAMP | When record was created |
323
+ | `modified_at` | TIMESTAMP | When record was last updated |
324
+ | `bucket_name` | TEXT | Storage bucket name |
325
+ | `object_name` | TEXT | File object name |
326
+ | `link` | TEXT | Access URL to the file |
327
+ | `mimetype` | TEXT | File MIME type |
328
+ | `size` | INTEGER | File size in bytes |
329
+ | `etag` | TEXT | Entity tag for versioning |
330
+ | `metadata` | JSONB | Additional file metadata |
331
+ | `storage_type` | TEXT | Storage type ('disk' or 'minio') |
332
+ | `is_synced` | BOOLEAN | Whether MetaLink is synchronized with storage (default: false) |
333
+
334
+ ### Setup
335
+
336
+ #### Step 1: Create Model
337
+
338
+ ```typescript
339
+ import { BaseMetaLinkModel } from '@venizia/ignis';
340
+ import { model } from '@venizia/ignis';
341
+
342
+ @model({ type: 'entity' })
343
+ export class FileMetaLinkModel extends BaseMetaLinkModel {
344
+ // Inherits all fields from BaseMetaLinkModel
345
+ }
346
+ ```
347
+
348
+ #### Step 2: Create Repository
349
+
350
+ ```typescript
351
+ import { BaseMetaLinkRepository } from '@venizia/ignis';
352
+ import { repository, inject } from '@venizia/ignis';
353
+ import { IDataSource } from '@venizia/ignis';
354
+
355
+ @repository({})
356
+ export class FileMetaLinkRepository extends BaseMetaLinkRepository {
357
+ constructor(@inject({ key: 'datasources.postgres' }) dataSource: IDataSource) {
358
+ super({
359
+ entityClass: FileMetaLinkModel,
360
+ relations: {},
361
+ dataSource,
362
+ });
363
+ }
364
+ }
365
+ ```
366
+
367
+ #### Step 3: Create Database Table
368
+
369
+ The model has `skipMigrate: true`, so you need to create the table manually:
370
+
371
+ ```sql
372
+ -- Run this in your database
373
+ CREATE TABLE "MetaLink" (
374
+ id TEXT PRIMARY KEY,
375
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
376
+ modified_at TIMESTAMP NOT NULL DEFAULT NOW(),
377
+ bucket_name TEXT NOT NULL,
378
+ object_name TEXT NOT NULL,
379
+ link TEXT NOT NULL,
380
+ mimetype TEXT NOT NULL,
381
+ size INTEGER NOT NULL,
382
+ etag TEXT,
383
+ metadata JSONB,
384
+ storage_type TEXT NOT NULL,
385
+ is_synced BOOLEAN NOT NULL DEFAULT false
386
+ );
387
+
388
+ CREATE INDEX "IDX_MetaLink_bucketName" ON "MetaLink"(bucket_name);
389
+ CREATE INDEX "IDX_MetaLink_objectName" ON "MetaLink"(object_name);
390
+ CREATE INDEX "IDX_MetaLink_storageType" ON "MetaLink"(storage_type);
391
+ CREATE INDEX "IDX_MetaLink_isSynced" ON "MetaLink"(is_synced);
392
+ ```
393
+
394
+ #### Step 4: Configure Component
395
+
396
+ ```typescript
397
+ import { FileMetaLinkModel, FileMetaLinkRepository } from './your-models';
398
+
399
+ export class Application extends BaseApplication {
400
+ configureComponents(): void {
401
+ // Register repository
402
+ this.repository(FileMetaLinkRepository);
403
+
404
+ // Configure Static Asset Component with MetaLink
405
+ this.bind<TStaticAssetsComponentOptions>({
406
+ key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
407
+ }).toValue({
408
+ uploads: {
409
+ controller: {
410
+ name: 'UploadController',
411
+ basePath: '/uploads',
412
+ isStrict: true,
413
+ },
414
+ storage: StaticAssetStorageTypes.MINIO,
415
+ helper: new MinioHelper({ /* ... */ }),
416
+ useMetaLink: true,
417
+ metaLink: {
418
+ model: FileMetaLinkModel,
419
+ repository: this.getSync(FileMetaLinkRepository),
420
+ },
421
+ extra: {
422
+ parseMultipartBody: { storage: 'memory' },
423
+ },
424
+ },
425
+ });
426
+
427
+ this.component(StaticAssetComponent);
428
+ }
429
+ }
430
+ ```
431
+
432
+ ### API Response with MetaLink
433
+
434
+ When `useMetaLink: true`, upload responses include the database record:
435
+
436
+ ```json
437
+ [
438
+ {
439
+ "bucketName": "user-uploads",
440
+ "objectName": "document.pdf",
441
+ "link": "/uploads/buckets/user-uploads/objects/document.pdf",
442
+ "metaLink": {
443
+ "id": "550e8400-e29b-41d4-a716-446655440000",
444
+ "bucketName": "user-uploads",
445
+ "objectName": "document.pdf",
446
+ "link": "/uploads/buckets/user-uploads/objects/document.pdf",
447
+ "mimetype": "application/pdf",
448
+ "size": 1048576,
449
+ "etag": "abc123def456",
450
+ "metadata": {
451
+ "originalName": "My Document.pdf",
452
+ "uploadedBy": "user123"
453
+ },
454
+ "storageType": "minio",
455
+ "isSynced": true,
456
+ "createdAt": "2025-12-15T03:00:00.000Z",
457
+ "modifiedAt": "2025-12-15T03:00:00.000Z"
458
+ }
459
+ }
460
+ ]
461
+ ```
462
+
463
+ **Note:** The `isSynced` field is automatically set to `true` when files are uploaded, indicating the MetaLink is synchronized with the actual file in storage.
464
+
465
+ ### Error Handling
466
+
467
+ If MetaLink creation fails, the upload still succeeds:
468
+
469
+ ```json
470
+ [
471
+ {
472
+ "bucketName": "user-uploads",
473
+ "objectName": "document.pdf",
474
+ "link": "/uploads/buckets/user-uploads/objects/document.pdf",
475
+ "metaLink": null,
476
+ "metaLinkError": "Database connection error"
477
+ }
478
+ ]
479
+ ```
480
+
481
+ ### Querying MetaLinks
482
+
483
+ ```typescript
484
+ // Get all files in a bucket
485
+ const files = await fileMetaLinkRepository.find({
486
+ where: { bucketName: 'user-uploads' },
487
+ });
488
+
489
+ // Get files by mimetype
490
+ const pdfs = await fileMetaLinkRepository.find({
491
+ where: { mimetype: 'application/pdf' },
492
+ });
493
+
494
+ // Get files by storage type
495
+ const minioFiles = await fileMetaLinkRepository.find({
496
+ where: { storageType: 'minio' },
497
+ });
498
+
499
+ // Get synced files only
500
+ const syncedFiles = await fileMetaLinkRepository.find({
501
+ where: { isSynced: true },
502
+ });
503
+
504
+ // Get unsynced files (for manual sync operations)
505
+ const unsyncedFiles = await fileMetaLinkRepository.find({
506
+ where: { isSynced: false },
507
+ });
508
+
509
+ // Count synced files
510
+ const syncedCount = await fileMetaLinkRepository.count({
511
+ where: { isSynced: true },
512
+ });
513
+
514
+ // Get recent uploads
515
+ const recent = await fileMetaLinkRepository.find({
516
+ orderBy: { createdAt: 'desc' },
517
+ limit: 10,
518
+ });
519
+ ```
520
+
521
+ ### Automatic Cleanup
522
+
523
+ When you delete a file, MetaLink records are automatically deleted:
524
+
525
+ ```http
526
+ DELETE /uploads/buckets/user-uploads/objects/document.pdf
527
+ ```
528
+
529
+ - Deletes file from storage
530
+ - Deletes MetaLink record from database
531
+ - Returns `{ "success": true }`
532
+
533
+ ---
534
+
535
+ ## Storage Helpers
536
+
537
+ ### DiskHelper (Local Filesystem)
538
+
539
+ Stores files on the local filesystem using a bucket-based directory structure.
540
+
541
+ #### Constructor
542
+
543
+ ```typescript
544
+ new DiskHelper({
545
+ basePath: string; // Base directory for storage
546
+ scope?: string; // Logger scope
547
+ identifier?: string; // Helper identifier
548
+ })
549
+ ```
550
+
551
+ #### Example
552
+
553
+ ```typescript
554
+ const diskHelper = new DiskHelper({
555
+ basePath: './app_data/storage',
556
+ });
557
+ ```
558
+
559
+ **Directory Structure:**
560
+ ```
561
+ app_data/storage/
562
+ ├── bucket-1/
563
+ │ ├── file1.pdf
564
+ │ └── file2.jpg
565
+ ├── bucket-2/
566
+ │ └── document.docx
567
+ ```
568
+
569
+ #### Features
570
+
571
+ - Automatic directory creation
572
+ - Built-in path validation
573
+ - Metadata stored in file stats
574
+ - Stream-based file operations
575
+
576
+ ---
577
+
578
+ ### MinioHelper (S3-Compatible Storage)
579
+
580
+ Connects to MinIO or any S3-compatible object storage.
581
+
582
+ #### Constructor
583
+
584
+ ```typescript
585
+ new MinioHelper({
586
+ endPoint: string; // MinIO server hostname
587
+ port: number; // API port (default: 9000)
588
+ useSSL: boolean; // Use HTTPS
589
+ accessKey: string; // Access key
590
+ secretKey: string; // Secret key
591
+ })
592
+ ```
593
+
594
+ #### Example
595
+
596
+ ```typescript
597
+ const minioHelper = new MinioHelper({
598
+ endPoint: 'minio.example.com',
599
+ port: 9000,
600
+ useSSL: true,
601
+ accessKey: process.env.MINIO_ACCESS_KEY,
602
+ secretKey: process.env.MINIO_SECRET_KEY,
603
+ });
604
+ ```
605
+
606
+ ---
607
+
608
+ ## IStorageHelper Interface
609
+
610
+ All storage helpers implement this unified interface:
611
+
612
+ ```typescript
613
+ interface IStorageHelper {
614
+ // Name validation
615
+ isValidName(name: string): boolean;
616
+
617
+ // Bucket operations
618
+ isBucketExists(opts: { name: string }): Promise<boolean>;
619
+ getBuckets(): Promise<IBucketInfo[]>;
620
+ getBucket(opts: { name: string }): Promise<IBucketInfo | null>;
621
+ createBucket(opts: { name: string }): Promise<IBucketInfo | null>;
622
+ removeBucket(opts: { name: string }): Promise<boolean>;
623
+
624
+ // File operations
625
+ upload(opts: {
626
+ bucket: string;
627
+ files: IUploadFile[];
628
+ normalizeNameFn?: (opts: { originalName: string }) => string;
629
+ normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
630
+ }): Promise<IUploadResult[]>;
631
+
632
+ getFile(opts: { bucket: string; name: string; options?: any }): Promise<Readable>;
633
+ getStat(opts: { bucket: string; name: string }): Promise<IFileStat>;
634
+ removeObject(opts: { bucket: string; name: string }): Promise<void>;
635
+ removeObjects(opts: { bucket: string; names: string[] }): Promise<void>;
636
+ listObjects(opts: IListObjectsOptions): Promise<IObjectInfo[]>;
637
+
638
+ // Utility
639
+ getFileType(opts: { mimeType: string }): string;
640
+ }
641
+ ```
642
+
643
+ ---
644
+
645
+ ## API Endpoints
646
+
647
+ The component dynamically generates REST endpoints for each configured storage backend.
648
+
649
+ ### Common Endpoints
650
+
651
+ All storage backends expose the same API structure:
652
+
653
+ #### **Get All Buckets**
654
+
655
+ ```http
656
+ GET /{basePath}/buckets
657
+ ```
658
+
659
+ **Response:**
660
+ ```json
661
+ [
662
+ { "name": "my-bucket", "creationDate": "2025-01-01T00:00:00.000Z" }
663
+ ]
664
+ ```
665
+
666
+ ---
667
+
668
+ #### **Get Bucket by Name**
669
+
670
+ ```http
671
+ GET /{basePath}/buckets/:bucketName
672
+ ```
673
+
674
+ **Parameters:**
675
+ - `bucketName` (path): Bucket name
676
+
677
+ **Validation:**
678
+ - ✅ Bucket name validated with `isValidName()`
679
+ - ❌ Returns 400 if invalid
680
+
681
+ **Response:**
682
+ ```json
683
+ { "name": "my-bucket", "creationDate": "2025-01-01T00:00:00.000Z" }
684
+ ```
685
+
686
+ ---
687
+
688
+ #### **Create Bucket**
689
+
690
+ ```http
691
+ POST /{basePath}/buckets/:bucketName
692
+ ```
693
+
694
+ **Parameters:**
695
+ - `bucketName` (path): Name of the new bucket
696
+
697
+ **Response:**
698
+ ```json
699
+ { "name": "my-bucket", "creationDate": "2025-12-13T00:00:00.000Z" }
700
+ ```
701
+
702
+ ---
703
+
704
+ #### **Delete Bucket**
705
+
706
+ ```http
707
+ DELETE /{basePath}/buckets/:bucketName
708
+ ```
709
+
710
+ **Parameters:**
711
+ - `bucketName` (path): Bucket to delete
712
+
713
+ **Response:**
714
+ ```json
715
+ { "success": true }
716
+ ```
717
+
718
+ ---
719
+
720
+ #### **Upload Files**
721
+
722
+ ```http
723
+ POST /{basePath}/buckets/:bucketName/upload
724
+ ```
725
+
726
+ **Request Body:**
727
+ - `multipart/form-data` with file fields
728
+ - Each file can optionally include `folderPath` for organization
729
+
730
+ **Response (without MetaLink):**
731
+ ```json
732
+ [
733
+ {
734
+ "bucketName": "my-bucket",
735
+ "objectName": "file.pdf",
736
+ "link": "/assets/buckets/my-bucket/objects/file.pdf"
737
+ }
738
+ ]
739
+ ```
740
+
741
+ **Response (with MetaLink enabled):**
742
+ ```json
743
+ [
744
+ {
745
+ "bucketName": "my-bucket",
746
+ "objectName": "file.pdf",
747
+ "link": "/assets/buckets/my-bucket/objects/file.pdf",
748
+ "metaLink": {
749
+ "id": "uuid",
750
+ "bucketName": "my-bucket",
751
+ "objectName": "file.pdf",
752
+ "link": "/assets/buckets/my-bucket/objects/file.pdf",
753
+ "mimetype": "application/pdf",
754
+ "size": 1024,
755
+ "etag": "abc123",
756
+ "metadata": {},
757
+ "storageType": "minio",
758
+ "isSynced": true,
759
+ "createdAt": "2025-12-15T03:00:00.000Z",
760
+ "modifiedAt": "2025-12-15T03:00:00.000Z"
761
+ }
762
+ }
763
+ ]
764
+ ```
765
+
766
+ **Example:**
767
+ ```typescript
768
+ const formData = new FormData();
769
+ formData.append('file', fileBlob, 'document.pdf');
770
+
771
+ const response = await fetch('/assets/buckets/uploads/upload', {
772
+ method: 'POST',
773
+ body: formData,
774
+ });
775
+
776
+ const result = await response.json();
777
+ console.log(result[0].metaLink); // Database record (if MetaLink enabled)
778
+ ```
779
+
780
+ ---
781
+
782
+ #### **Get Object (Stream)**
783
+
784
+ ```http
785
+ GET /{basePath}/buckets/:bucketName/objects/:objectName
786
+ ```
787
+
788
+ **Parameters:**
789
+ - `bucketName` (path): Bucket name
790
+ - `objectName` (path): Object name (URL-encoded)
791
+
792
+ **Validation:**
793
+ - ✅ Both bucket and object names validated
794
+ - ❌ Returns 400 if either is invalid
795
+
796
+ **Response:**
797
+ - Streams file content with appropriate headers
798
+ - Content-Type: From metadata or `application/octet-stream`
799
+ - Content-Length: File size in bytes
800
+ - X-Content-Type-Options: `nosniff`
801
+
802
+ ---
803
+
804
+ #### **Download Object**
805
+
806
+ ```http
807
+ GET /{basePath}/buckets/:bucketName/objects/:objectName/download
808
+ ```
809
+
810
+ **Parameters:**
811
+ - `bucketName` (path): Bucket name
812
+ - `objectName` (path): Object name (URL-encoded)
813
+
814
+ **Response:**
815
+ - Streams file with download headers
816
+ - Content-Disposition: `attachment; filename="..."`
817
+ - Triggers browser download dialog
818
+
819
+ **Example:**
820
+ ```typescript
821
+ const downloadUrl = `/assets/buckets/uploads/objects/${encodeURIComponent('document.pdf')}/download`;
822
+ window.open(downloadUrl, '_blank');
823
+ ```
824
+
825
+ ---
826
+
827
+ #### **Delete Object**
828
+
829
+ ```http
830
+ DELETE /{basePath}/buckets/:bucketName/objects/:objectName
831
+ ```
832
+
833
+ **Parameters:**
834
+ - `bucketName` (path): Bucket name
835
+ - `objectName` (path): Object to delete
836
+
837
+ **Response:**
838
+ ```json
839
+ { "success": true }
840
+ ```
841
+
842
+ ---
843
+
844
+ #### **List Objects**
845
+
846
+ ```http
847
+ GET /{basePath}/buckets/:bucketName/objects
848
+ ```
849
+
850
+ **Query Parameters:**
851
+ - `prefix` (optional): Filter by prefix
852
+ - `recursive` (optional, boolean): Recursive listing
853
+ - `maxKeys` (optional, number): Maximum objects to return
854
+
855
+ **Response:**
856
+ ```json
857
+ [
858
+ {
859
+ "name": "file1.pdf",
860
+ "size": 1024,
861
+ "lastModified": "2025-12-13T00:00:00.000Z",
862
+ "etag": "abc123"
863
+ }
864
+ ]
865
+ ```
866
+
867
+ ---
868
+
869
+ #### **Delete Object**
870
+
871
+ ```http
872
+ DELETE /{basePath}/buckets/:bucketName/objects/:objectName
873
+ ```
874
+
875
+ **Parameters:**
876
+ - `bucketName` (path): Bucket name
877
+ - `objectName` (path): Object to delete (URL-encoded)
878
+
879
+ **Validation:**
880
+ - ✅ Both bucket and object names validated
881
+ - ❌ Returns 400 if either is invalid
882
+
883
+ **Behavior:**
884
+ - Deletes file from storage
885
+ - If MetaLink enabled, also deletes database record
886
+ - MetaLink deletion errors are logged but don't fail the request
887
+
888
+ **Response:**
889
+ ```json
890
+ {
891
+ "success": true
892
+ }
893
+ ```
894
+
895
+ **Example:**
896
+ ```typescript
897
+ const bucketName = 'user-uploads';
898
+ const objectName = 'document.pdf';
899
+
900
+ await fetch(`/assets/buckets/${bucketName}/objects/${encodeURIComponent(objectName)}`, {
901
+ method: 'DELETE',
902
+ });
903
+
904
+ // File deleted from storage
905
+ // MetaLink record deleted from database (if enabled)
906
+ ```
907
+
908
+ ---
909
+
910
+ #### **Sync MetaLink** (MetaLink only)
911
+
912
+ ```http
913
+ PUT /{basePath}/buckets/:bucketName/objects/:objectName/meta-links
914
+ ```
915
+
916
+ **Availability:** Only available when `useMetaLink: true`
917
+
918
+ **Parameters:**
919
+ - `bucketName` (path): Bucket name
920
+ - `objectName` (path): Object name (URL-encoded)
921
+
922
+ **Validation:**
923
+ - ✅ Both bucket and object names validated
924
+ - ❌ Returns 400 if either is invalid
925
+
926
+ **Behavior:**
927
+ - Fetches current file metadata from storage
928
+ - If MetaLink exists: Updates with latest metadata
929
+ - If MetaLink doesn't exist: Creates new MetaLink record
930
+ - Sets `isSynced: true` to mark as synchronized
931
+
932
+ **Use Cases:**
933
+ - Manually sync files that exist in storage but not in database
934
+ - Update MetaLink metadata after file changes
935
+ - Rebuild MetaLink records after database restore
936
+ - Bulk synchronization operations
937
+
938
+ **Response (MetaLink created):**
939
+ ```json
940
+ {
941
+ "id": "uuid",
942
+ "bucketName": "user-uploads",
943
+ "objectName": "document.pdf",
944
+ "link": "/assets/buckets/user-uploads/objects/document.pdf",
945
+ "mimetype": "application/pdf",
946
+ "size": 1048576,
947
+ "etag": "abc123",
948
+ "metadata": {},
949
+ "storageType": "minio",
950
+ "isSynced": true,
951
+ "createdAt": "2025-12-15T03:00:00.000Z",
952
+ "modifiedAt": "2025-12-15T03:00:00.000Z"
953
+ }
954
+ ```
955
+
956
+ **Response (MetaLink updated):**
957
+ ```json
958
+ {
959
+ "id": "existing-uuid",
960
+ "bucketName": "user-uploads",
961
+ "objectName": "document.pdf",
962
+ "link": "/assets/buckets/user-uploads/objects/document.pdf",
963
+ "mimetype": "application/pdf",
964
+ "size": 1048576,
965
+ "etag": "abc123updated",
966
+ "metadata": {},
967
+ "storageType": "minio",
968
+ "isSynced": true,
969
+ "createdAt": "2025-12-15T02:00:00.000Z",
970
+ "modifiedAt": "2025-12-15T03:00:00.000Z"
971
+ }
972
+ ```
973
+
974
+ **Example:**
975
+ ```typescript
976
+ // Sync a single file
977
+ const bucketName = 'user-uploads';
978
+ const objectName = 'document.pdf';
979
+
980
+ const response = await fetch(
981
+ `/assets/buckets/${bucketName}/objects/${encodeURIComponent(objectName)}/meta-links`,
982
+ { method: 'PUT' }
983
+ );
984
+
985
+ const metaLink = await response.json();
986
+ console.log('Synced:', metaLink.isSynced); // true
987
+
988
+ // Bulk sync example: sync all files in storage
989
+ const objects = await fetch(`/assets/buckets/${bucketName}/objects`).then(r => r.json());
990
+
991
+ for (const obj of objects) {
992
+ await fetch(
993
+ `/assets/buckets/${bucketName}/objects/${encodeURIComponent(obj.name)}/meta-links`,
994
+ { method: 'PUT' }
995
+ );
996
+ }
997
+ ```
998
+
999
+ ---
1000
+
1001
+ ## Security Features
1002
+
1003
+ ### Built-in Name Validation
1004
+
1005
+ All storage helpers implement comprehensive name validation:
1006
+
1007
+ ```typescript
1008
+ isValidName(name: string): boolean {
1009
+ // ❌ Prevents path traversal
1010
+ if (name.includes('..') || name.includes('/') || name.includes('\\'))
1011
+ return false;
1012
+
1013
+ // ❌ Prevents hidden files
1014
+ if (name.startsWith('.')) return false;
1015
+
1016
+ // ❌ Prevents shell injection
1017
+ const dangerousChars = /[;|&$`<>(){}[\]!#]/;
1018
+ if (dangerousChars.test(name)) return false;
1019
+
1020
+ // ❌ Prevents header injection
1021
+ if (name.includes('\n') || name.includes('\r') || name.includes('\0'))
1022
+ return false;
1023
+
1024
+ // ❌ Prevents DoS (long names)
1025
+ if (name.length > 255) return false;
1026
+
1027
+ // ❌ Prevents empty names
1028
+ if (name.trim().length === 0) return false;
1029
+
1030
+ return true;
1031
+ }
1032
+ ```
1033
+
1034
+ **Blocked Patterns:**
1035
+ ```
1036
+ ../etc/passwd ❌ Path traversal
1037
+ .hidden ❌ Hidden file
1038
+ file;rm -rf / ❌ Shell injection
1039
+ file\ninjected ❌ Header injection
1040
+ very_long_name... ❌ > 255 characters
1041
+ ```
1042
+
1043
+ ### HTTP Security Headers
1044
+
1045
+ All responses include security headers:
1046
+
1047
+ ```http
1048
+ X-Content-Type-Options: nosniff
1049
+ Content-Disposition: attachment; filename="..."
1050
+ ```
1051
+
1052
+ ---
1053
+
1054
+ ## Usage Examples
1055
+
1056
+ ### Example 1: Multiple Storage Backends
1057
+
1058
+ ```typescript
1059
+ this.bind<TStaticAssetsComponentOptions>({
1060
+ key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
1061
+ }).toValue({
1062
+ // User uploads → MinIO
1063
+ uploads: {
1064
+ controller: { name: 'UploadController', basePath: '/uploads' },
1065
+ storage: StaticAssetStorageTypes.MINIO,
1066
+ helper: new MinioHelper({ /* ... */ }),
1067
+ extra: { parseMultipartBody: { storage: 'memory' } },
1068
+ },
1069
+
1070
+ // Temporary files → Local disk
1071
+ temp: {
1072
+ controller: { name: 'TempController', basePath: '/temp' },
1073
+ storage: StaticAssetStorageTypes.DISK,
1074
+ helper: new DiskHelper({ basePath: './temp' }),
1075
+ extra: { parseMultipartBody: { storage: 'disk' } },
1076
+ },
1077
+
1078
+ // Public assets → Local disk
1079
+ public: {
1080
+ controller: { name: 'PublicController', basePath: '/public' },
1081
+ storage: StaticAssetStorageTypes.DISK,
1082
+ helper: new DiskHelper({ basePath: './public' }),
1083
+ extra: { parseMultipartBody: { storage: 'memory' } },
1084
+ },
1085
+ });
1086
+ ```
1087
+
1088
+ **Result:** 3 independent storage systems with different endpoints:
1089
+ - `/uploads/buckets/...`
1090
+ - `/temp/buckets/...`
1091
+ - `/public/buckets/...`
1092
+
1093
+ ### Example 2: Frontend Integration
1094
+
1095
+ ```typescript
1096
+ // Upload file to MinIO
1097
+ async function uploadFile(file: File) {
1098
+ const formData = new FormData();
1099
+ formData.append('file', file);
1100
+
1101
+ const response = await fetch('/assets/buckets/user-uploads/upload', {
1102
+ method: 'POST',
1103
+ body: formData,
1104
+ });
1105
+
1106
+ const result = await response.json();
1107
+ return result[0].link;
1108
+ }
1109
+
1110
+ // Download file
1111
+ function downloadFile(bucketName: string, objectName: string) {
1112
+ const url = `/assets/buckets/${bucketName}/objects/${encodeURIComponent(objectName)}/download`;
1113
+ window.open(url, '_blank');
1114
+ }
1115
+
1116
+ // List files in bucket
1117
+ async function listFiles(bucketName: string, prefix?: string) {
1118
+ const url = new URL(`/assets/buckets/${bucketName}/objects`, window.location.origin);
1119
+ if (prefix) url.searchParams.append('prefix', prefix);
1120
+
1121
+ const response = await fetch(url);
1122
+ return await response.json();
1123
+ }
1124
+ ```
1125
+
1126
+ ### Example 3: Custom Filename Normalization
1127
+
1128
+ ```typescript
1129
+ {
1130
+ uploads: {
1131
+ controller: { name: 'UploadController', basePath: '/uploads' },
1132
+ storage: StaticAssetStorageTypes.MINIO,
1133
+ helper: new MinioHelper({ /* ... */ }),
1134
+ extra: {
1135
+ parseMultipartBody: { storage: 'memory' },
1136
+ normalizeNameFn: ({ originalName }) => {
1137
+ // Add timestamp prefix
1138
+ return `${Date.now()}_${originalName.toLowerCase().replace(/\s/g, '_')}`;
1139
+ },
1140
+ normalizeLinkFn: ({ bucketName, normalizeName }) => {
1141
+ // Custom link format
1142
+ return `/api/files/${bucketName}/${encodeURIComponent(normalizeName)}`;
1143
+ },
1144
+ },
1145
+ },
1146
+ }
1147
+ ```
1148
+
1149
+ ---
1150
+
1151
+ ## Custom Storage Implementation
1152
+
1153
+ You can implement your own storage backend by extending `BaseStorageHelper`:
1154
+
1155
+ ```typescript
1156
+ import { BaseStorageHelper, IUploadFile, IUploadResult } from '@venizia/ignis-helpers';
1157
+
1158
+ class S3Helper extends BaseStorageHelper {
1159
+ constructor(config: S3Config) {
1160
+ super({ scope: 'S3Helper', identifier: 'S3Helper' });
1161
+ // Initialize S3 client
1162
+ }
1163
+
1164
+ async isBucketExists(opts: { name: string }): Promise<boolean> {
1165
+ // Implementation
1166
+ }
1167
+
1168
+ async upload(opts: {
1169
+ bucket: string;
1170
+ files: IUploadFile[];
1171
+ }): Promise<IUploadResult[]> {
1172
+ // Implementation
1173
+ }
1174
+
1175
+ // Implement other IStorageHelper methods...
1176
+ }
1177
+
1178
+ // Usage
1179
+ this.bind<TStaticAssetsComponentOptions>({
1180
+ key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
1181
+ }).toValue({
1182
+ s3Storage: {
1183
+ controller: { name: 'S3Controller', basePath: '/s3-assets' },
1184
+ storage: 'custom-s3',
1185
+ helper: new S3Helper({ /* ... */ }),
1186
+ extra: {},
1187
+ },
1188
+ });
1189
+ ```
1190
+
1191
+ ---
1192
+
1193
+ ## Troubleshooting
1194
+
1195
+ ### Issue: "Invalid bucket/object name" errors
1196
+
1197
+ **Cause:** Name fails `isValidName()` validation
1198
+
1199
+ **Solution:** Ensure names:
1200
+ - Don't contain `..`, `/`, `\`
1201
+ - Don't start with `.`
1202
+ - Don't contain shell special characters
1203
+ - Are <= 255 characters
1204
+ - Are not empty or whitespace-only
1205
+
1206
+ ### Issue: Controller not registering
1207
+
1208
+ **Cause:** Configuration key might be invalid or missing required fields
1209
+
1210
+ **Solution:** Ensure each storage configuration has all required fields:
1211
+ ```typescript
1212
+ {
1213
+ [uniqueKey]: {
1214
+ controller: { name, basePath, isStrict },
1215
+ storage: 'disk' | 'minio',
1216
+ helper: IStorageHelper,
1217
+ extra: {}
1218
+ }
1219
+ }
1220
+ ```
1221
+
1222
+ ### Issue: Files not uploading
1223
+
1224
+ **DiskHelper:**
1225
+ - Ensure `basePath` directory exists or can be created
1226
+ - Check filesystem permissions
1227
+
1228
+ **MinioHelper:**
1229
+ - Verify MinIO server is running
1230
+ - Check credentials (accessKey, secretKey)
1231
+ - Verify network connectivity (endPoint, port)
1232
+
1233
+ ### Issue: Large file uploads failing
1234
+
1235
+ **Solution:** Switch to disk-based multipart parsing:
1236
+
1237
+ ```typescript
1238
+ extra: {
1239
+ parseMultipartBody: {
1240
+ storage: 'disk', // Use disk instead of memory
1241
+ uploadDir: './uploads', // Temporary upload directory
1242
+ },
1243
+ }
1244
+ ```
1245
+
1246
+ ---
1247
+
1248
+ ## Docker Setup for Development
1249
+
1250
+ ### Docker Compose for MinIO
1251
+
1252
+ ```yaml
1253
+ version: '3.8'
1254
+ services:
1255
+ minio:
1256
+ image: minio/minio:latest
1257
+ container_name: minio
1258
+ ports:
1259
+ - "9000:9000" # API port
1260
+ - "9001:9001" # Console port
1261
+ environment:
1262
+ MINIO_ROOT_USER: minioadmin
1263
+ MINIO_ROOT_PASSWORD: minioadmin
1264
+ command: server /data --console-address ":9001"
1265
+ volumes:
1266
+ - minio_data:/data
1267
+
1268
+ volumes:
1269
+ minio_data:
1270
+ ```
1271
+
1272
+ **Start MinIO:**
1273
+ ```bash
1274
+ docker-compose up -d
1275
+ ```
1276
+
1277
+ **Access MinIO Console:**
1278
+ ```
1279
+ http://localhost:9001
1280
+ ```
1281
+
1282
+ ---
1283
+
1284
+ ## Related Documentation
1285
+
1286
+ - [Storage Helpers](../helpers/storage.md) - DiskHelper, MinioHelper, BaseStorageHelper
1287
+ - [Request Utilities](../utilities/request.md) - `parseMultipartBody`, `createContentDispositionHeader`
1288
+ - [Components Overview](./index.md)
1289
+ - [Controllers](../base/controllers.md)