@venizia/ignis-docs 0.0.5 → 0.0.6-0

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 (98) hide show
  1. package/package.json +1 -1
  2. package/wiki/best-practices/architecture-decisions.md +0 -8
  3. package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
  4. package/wiki/best-practices/performance-optimization.md +3 -3
  5. package/wiki/best-practices/security-guidelines.md +2 -2
  6. package/wiki/best-practices/troubleshooting-tips.md +1 -1
  7. package/wiki/guides/core-concepts/components-guide.md +1 -1
  8. package/wiki/guides/core-concepts/components.md +2 -2
  9. package/wiki/guides/core-concepts/dependency-injection.md +1 -1
  10. package/wiki/guides/core-concepts/services.md +1 -1
  11. package/wiki/guides/tutorials/building-a-crud-api.md +1 -1
  12. package/wiki/guides/tutorials/ecommerce-api.md +2 -2
  13. package/wiki/guides/tutorials/realtime-chat.md +6 -6
  14. package/wiki/guides/tutorials/testing.md +1 -1
  15. package/wiki/references/base/bootstrapping.md +0 -2
  16. package/wiki/references/base/components.md +2 -2
  17. package/wiki/references/base/controllers.md +0 -1
  18. package/wiki/references/base/datasources.md +1 -1
  19. package/wiki/references/base/dependency-injection.md +1 -1
  20. package/wiki/references/base/filter-system/quick-reference.md +0 -14
  21. package/wiki/references/base/middlewares.md +0 -8
  22. package/wiki/references/base/providers.md +0 -9
  23. package/wiki/references/base/services.md +0 -1
  24. package/wiki/references/components/authentication/api.md +444 -0
  25. package/wiki/references/components/authentication/errors.md +177 -0
  26. package/wiki/references/components/authentication/index.md +571 -0
  27. package/wiki/references/components/authentication/usage.md +781 -0
  28. package/wiki/references/components/health-check.md +292 -103
  29. package/wiki/references/components/index.md +14 -12
  30. package/wiki/references/components/mail/api.md +505 -0
  31. package/wiki/references/components/mail/errors.md +176 -0
  32. package/wiki/references/components/mail/index.md +535 -0
  33. package/wiki/references/components/mail/usage.md +404 -0
  34. package/wiki/references/components/request-tracker.md +229 -25
  35. package/wiki/references/components/socket-io/api.md +1051 -0
  36. package/wiki/references/components/socket-io/errors.md +119 -0
  37. package/wiki/references/components/socket-io/index.md +410 -0
  38. package/wiki/references/components/socket-io/usage.md +322 -0
  39. package/wiki/references/components/static-asset/api.md +261 -0
  40. package/wiki/references/components/static-asset/errors.md +89 -0
  41. package/wiki/references/components/static-asset/index.md +617 -0
  42. package/wiki/references/components/static-asset/usage.md +364 -0
  43. package/wiki/references/components/swagger.md +390 -110
  44. package/wiki/references/components/template/api-page.md +125 -0
  45. package/wiki/references/components/template/errors-page.md +100 -0
  46. package/wiki/references/components/template/index.md +104 -0
  47. package/wiki/references/components/template/setup-page.md +134 -0
  48. package/wiki/references/components/template/single-page.md +132 -0
  49. package/wiki/references/components/template/usage-page.md +127 -0
  50. package/wiki/references/components/websocket/api.md +508 -0
  51. package/wiki/references/components/websocket/errors.md +123 -0
  52. package/wiki/references/components/websocket/index.md +453 -0
  53. package/wiki/references/components/websocket/usage.md +475 -0
  54. package/wiki/references/helpers/cron/index.md +224 -0
  55. package/wiki/references/helpers/crypto/index.md +537 -0
  56. package/wiki/references/helpers/env/index.md +214 -0
  57. package/wiki/references/helpers/error/index.md +232 -0
  58. package/wiki/references/helpers/index.md +16 -15
  59. package/wiki/references/helpers/inversion/index.md +608 -0
  60. package/wiki/references/helpers/logger/index.md +600 -0
  61. package/wiki/references/helpers/network/api.md +986 -0
  62. package/wiki/references/helpers/network/index.md +620 -0
  63. package/wiki/references/helpers/queue/index.md +589 -0
  64. package/wiki/references/helpers/redis/index.md +495 -0
  65. package/wiki/references/helpers/socket-io/api.md +497 -0
  66. package/wiki/references/helpers/socket-io/index.md +513 -0
  67. package/wiki/references/helpers/storage/api.md +705 -0
  68. package/wiki/references/helpers/storage/index.md +583 -0
  69. package/wiki/references/helpers/template/index.md +66 -0
  70. package/wiki/references/helpers/template/single-page.md +126 -0
  71. package/wiki/references/helpers/testing/index.md +510 -0
  72. package/wiki/references/helpers/types/index.md +512 -0
  73. package/wiki/references/helpers/uid/index.md +272 -0
  74. package/wiki/references/helpers/websocket/api.md +736 -0
  75. package/wiki/references/helpers/websocket/index.md +574 -0
  76. package/wiki/references/helpers/worker-thread/index.md +470 -0
  77. package/wiki/references/quick-reference.md +3 -18
  78. package/wiki/references/utilities/jsx.md +1 -8
  79. package/wiki/references/utilities/statuses.md +0 -7
  80. package/wiki/references/components/authentication.md +0 -476
  81. package/wiki/references/components/mail.md +0 -687
  82. package/wiki/references/components/socket-io.md +0 -562
  83. package/wiki/references/components/static-asset.md +0 -1277
  84. package/wiki/references/helpers/cron.md +0 -108
  85. package/wiki/references/helpers/crypto.md +0 -132
  86. package/wiki/references/helpers/env.md +0 -83
  87. package/wiki/references/helpers/error.md +0 -97
  88. package/wiki/references/helpers/inversion.md +0 -176
  89. package/wiki/references/helpers/logger.md +0 -296
  90. package/wiki/references/helpers/network.md +0 -396
  91. package/wiki/references/helpers/queue.md +0 -150
  92. package/wiki/references/helpers/redis.md +0 -142
  93. package/wiki/references/helpers/socket-io.md +0 -932
  94. package/wiki/references/helpers/storage.md +0 -665
  95. package/wiki/references/helpers/testing.md +0 -133
  96. package/wiki/references/helpers/types.md +0 -167
  97. package/wiki/references/helpers/uid.md +0 -167
  98. package/wiki/references/helpers/worker-thread.md +0 -178
@@ -0,0 +1,322 @@
1
+ # Socket.IO -- Usage & Examples
2
+
3
+ > Server-side usage patterns, client helper setup, and advanced examples.
4
+
5
+ ## Server-Side Usage
6
+
7
+ ### Inject and Use in Services/Controllers
8
+
9
+ Inject `SocketIOServerHelper` to interact with Socket.IO:
10
+
11
+ ```typescript
12
+ import {
13
+ BaseService,
14
+ inject,
15
+ SocketIOBindingKeys,
16
+ SocketIOServerHelper,
17
+ CoreBindings,
18
+ BaseApplication,
19
+ } from '@venizia/ignis';
20
+
21
+ export class NotificationService extends BaseService {
22
+ // Lazy getter pattern -- helper is bound AFTER server starts
23
+ private _io: SocketIOServerHelper | null = null;
24
+
25
+ constructor(
26
+ @inject({ key: CoreBindings.APPLICATION_INSTANCE })
27
+ private application: BaseApplication,
28
+ ) {
29
+ super({ scope: NotificationService.name });
30
+ }
31
+
32
+ private get io(): SocketIOServerHelper {
33
+ if (!this._io) {
34
+ this._io = this.application.get<SocketIOServerHelper>({
35
+ key: SocketIOBindingKeys.SOCKET_IO_INSTANCE,
36
+ isOptional: true,
37
+ }) ?? null;
38
+ }
39
+
40
+ if (!this._io) {
41
+ throw new Error('SocketIO not initialized');
42
+ }
43
+
44
+ return this._io;
45
+ }
46
+
47
+ // Send to a specific client
48
+ notifyUser(opts: { userId: string; message: string }) {
49
+ this.io.send({
50
+ destination: opts.userId,
51
+ payload: {
52
+ topic: 'notification',
53
+ data: { message: opts.message, time: new Date().toISOString() },
54
+ },
55
+ });
56
+ }
57
+
58
+ // Send to a room
59
+ notifyRoom(opts: { room: string; message: string }) {
60
+ this.io.send({
61
+ destination: opts.room,
62
+ payload: {
63
+ topic: 'room:update',
64
+ data: { message: opts.message },
65
+ },
66
+ });
67
+ }
68
+
69
+ // Broadcast to all clients
70
+ broadcastAnnouncement(opts: { message: string }) {
71
+ this.io.send({
72
+ payload: {
73
+ topic: 'system:announcement',
74
+ data: { message: opts.message },
75
+ },
76
+ });
77
+ }
78
+ }
79
+ ```
80
+
81
+ > [!IMPORTANT]
82
+ > **Lazy getter pattern**: Since `SocketIOServerHelper` is bound via a post-start hook, it's not available during DI construction. Use a lazy getter that resolves from the application container on first access.
83
+
84
+ ## Client Helper
85
+
86
+ `SocketIOClientHelper` provides a managed Socket.IO client for connecting to Socket.IO servers -- useful for service-to-service communication, testing, or building relay services. It extends `BaseHelper` for scoped logging and wraps the `socket.io-client` library with authentication flow, lifecycle callbacks, and error-safe event subscription.
87
+
88
+ ### Client Setup
89
+
90
+ ```typescript
91
+ import {
92
+ SocketIOClientHelper,
93
+ } from '@venizia/ignis-helpers/socket-io';
94
+
95
+ const client = new SocketIOClientHelper({
96
+ identifier: 'notification-relay',
97
+ host: 'http://localhost:3000',
98
+ options: {
99
+ path: '/io',
100
+ extraHeaders: {
101
+ authorization: 'Bearer <token>',
102
+ },
103
+ },
104
+
105
+ // Lifecycle callbacks (all optional)
106
+ onConnected: () => {
107
+ console.log('Connected to server');
108
+ client.authenticate();
109
+ },
110
+ onDisconnected: (reason) => {
111
+ console.log('Disconnected:', reason);
112
+ },
113
+ onError: (error) => {
114
+ console.error('Connection error:', error);
115
+ },
116
+ onAuthenticated: () => {
117
+ console.log('Authentication successful');
118
+ },
119
+ onUnauthenticated: (message) => {
120
+ console.warn('Authentication failed:', message);
121
+ },
122
+ });
123
+ ```
124
+
125
+ #### Constructor Behavior
126
+
127
+ The constructor immediately calls `configure()`, which creates the `socket.io-client` `Socket` instance via `io(host, options)` and registers all internal event handlers (`connect`, `disconnect`, `connect_error`, `authenticated`, `unauthenticated`, `ping`). The socket is **not** connected until you call `client.connect()` (if using `autoConnect: false` in the options) or it connects automatically if `autoConnect` is not explicitly disabled.
128
+
129
+ #### `connect` vs `connection` Event
130
+
131
+ The client-side `socket.io-client` library fires the `connect` event (no "ion" suffix) when the connection is established. The server-side `socket.io` library fires `connection` (with the suffix). This is a Socket.IO convention, not an Ignis-specific behavior. The client helper registers on `'connect'` while the server helper registers on `SocketIOConstants.EVENT_CONNECT` which equals `'connection'`.
132
+
133
+ ### Authentication Flow
134
+
135
+ After connecting, the client must emit `authenticate` to start the auth handshake. The server validates credentials from the socket handshake (headers, query params, `auth` object) and responds with either `authenticated` or `unauthenticated`.
136
+
137
+ ```typescript
138
+ // Manual authentication after connection
139
+ client.authenticate();
140
+ ```
141
+
142
+ The `authenticate()` method has two guard conditions:
143
+ 1. The socket must be connected (`client.connected === true`)
144
+ 2. The current state must be `unauthorized` -- calling `authenticate()` while `authenticating` or already `authenticated` is a no-op with a warning log
145
+
146
+ #### Authentication Failure Details
147
+
148
+ The server sends two distinct error messages depending on how the `authenticateFn` fails:
149
+
150
+ | Failure Mode | Message | Cause |
151
+ |-------------|---------|-------|
152
+ | `authenticateFn` returned `false` | `"Invalid token to authenticate! Please login again!"` | Credentials were checked but deemed invalid |
153
+ | `authenticateFn` threw an error | `"Failed to authenticate connection! Please login again!"` | An unexpected error occurred during validation |
154
+
155
+ Both failure paths set the client state back to `unauthorized`, emit the `unauthenticated` event to the client with the message, and then disconnect the socket after the message is delivered (via `setImmediate` callback).
156
+
157
+ ### Event Subscription
158
+
159
+ Subscribe to custom events with automatic error safety. Handlers are wrapped in a dual try-catch that catches both synchronous throws and asynchronous rejections:
160
+
161
+ ```typescript
162
+ // Subscribe to a single event
163
+ client.subscribe({
164
+ event: 'chat:message',
165
+ handler: (data: { from: string; text: string }) => {
166
+ console.log(`${data.from}: ${data.text}`);
167
+ },
168
+ });
169
+
170
+ // Subscribe with duplicate detection disabled
171
+ client.subscribe({
172
+ event: 'chat:message',
173
+ handler: (data) => { /* second handler */ },
174
+ ignoreDuplicate: false, // default: true -- set to false to allow multiple handlers
175
+ });
176
+
177
+ // Subscribe to multiple events at once
178
+ client.subscribeMany({
179
+ events: {
180
+ 'user:joined': (data) => console.log('User joined:', data),
181
+ 'user:left': (data) => console.log('User left:', data),
182
+ 'room:updated': (data) => console.log('Room updated:', data),
183
+ },
184
+ });
185
+ ```
186
+
187
+ #### Deduplication Behavior
188
+
189
+ By default (`ignoreDuplicate: true`), `subscribe()` checks `socket.hasListeners(event)` before registering. If listeners already exist for the event, the call is a no-op and logs an info message. Set `ignoreDuplicate: false` to allow multiple handlers for the same event.
190
+
191
+ ### Unsubscribing
192
+
193
+ ```typescript
194
+ // Remove all handlers for an event
195
+ client.unsubscribe({ event: 'chat:message' });
196
+
197
+ // Remove a specific handler
198
+ client.unsubscribe({ event: 'chat:message', handler: myHandler });
199
+
200
+ // Remove handlers for multiple events
201
+ client.unsubscribeMany({ events: ['chat:message', 'user:joined', 'room:updated'] });
202
+ ```
203
+
204
+ ### Emitting Events
205
+
206
+ ```typescript
207
+ client.emit({
208
+ topic: 'chat:send',
209
+ data: { text: 'Hello world' },
210
+ doLog: true, // optional: log the emission
211
+ cb: () => { // optional: callback via setImmediate after emit
212
+ console.log('Message sent');
213
+ },
214
+ });
215
+ ```
216
+
217
+ The `emit()` method throws if the socket is not connected or if no `topic` is provided. Unlike `send()` on the server helper, this method does **not** silently swallow errors.
218
+
219
+ ### Room Management
220
+
221
+ ```typescript
222
+ // Request to join rooms (server validates via validateRoomFn)
223
+ client.joinRooms({ rooms: ['chat-room-1', 'notifications'] });
224
+
225
+ // Request to leave rooms
226
+ client.leaveRooms({ rooms: ['chat-room-1'] });
227
+ ```
228
+
229
+ Both methods emit Socket.IO events (`join` / `leave`) to the server. The actual join/leave happens server-side. If the socket is not connected, the call is a no-op with a warning log.
230
+
231
+ ### Connection Management
232
+
233
+ ```typescript
234
+ // Manually connect (useful when autoConnect: false in options)
235
+ client.connect();
236
+
237
+ // Disconnect from server
238
+ client.disconnect();
239
+
240
+ // Check current state
241
+ const state = client.getState(); // 'unauthorized' | 'authenticating' | 'authenticated'
242
+
243
+ // Get raw socket.io-client Socket instance
244
+ const rawSocket = client.getSocketClient();
245
+ ```
246
+
247
+ ### Shutdown
248
+
249
+ ```typescript
250
+ // Clean shutdown: removes all listeners, disconnects, resets state
251
+ client.shutdown();
252
+ ```
253
+
254
+ The `shutdown()` method:
255
+ 1. Calls `removeAllListeners()` on the underlying socket to prevent memory leaks
256
+ 2. Disconnects if still connected
257
+ 3. Resets state to `unauthorized`
258
+
259
+ ## Advanced Usage
260
+
261
+ ### Complete Example
262
+
263
+ A full working example is available at `examples/socket-io-test/`. It demonstrates:
264
+
265
+ | Feature | Implementation |
266
+ |---------|---------------|
267
+ | Application setup | `src/application.ts` -- bindings, component registration, graceful shutdown |
268
+ | REST endpoints | `src/controllers/socket-test.controller.ts` -- 9 endpoints for Socket.IO management |
269
+ | Event handling | `src/services/socket-event.service.ts` -- chat, echo, room management |
270
+ | Automated test client | `client.ts` -- 15+ test cases covering all features |
271
+
272
+ #### REST API Endpoints
273
+
274
+ The example provides a REST API for managing Socket.IO:
275
+
276
+ | Method | Path | Description |
277
+ |--------|------|-------------|
278
+ | `GET` | `/socket/info` | Server status + connected client count |
279
+ | `GET` | `/socket/clients` | List all connected client IDs |
280
+ | `GET` | `/socket/health` | Health check (is SocketIO ready?) |
281
+ | `POST` | `/socket/broadcast` | Broadcast <code v-pre>{{ topic, data }}</code> to all clients |
282
+ | `POST` | `/socket/room/{roomId}/send` | Send <code v-pre>{{ topic, data }}</code> to a room |
283
+ | `POST` | `/socket/client/{clientId}/send` | Send <code v-pre>{{ topic, data }}</code> to a specific client |
284
+ | `POST` | `/socket/client/{clientId}/join` | Join client to <code v-pre>{{ rooms: string[] }}</code> |
285
+ | `POST` | `/socket/client/{clientId}/leave` | Remove client from <code v-pre>{{ rooms: string[] }}</code> |
286
+ | `GET` | `/socket/client/{clientId}/rooms` | List rooms a client belongs to |
287
+
288
+ #### Running the Example
289
+
290
+ ```bash
291
+ # Start the server
292
+ cd examples/socket-io-test
293
+ bun run server:dev
294
+
295
+ # In another terminal -- run automated tests
296
+ bun client.ts
297
+ ```
298
+
299
+ The automated client tests the following features:
300
+
301
+ - Authentication (valid and invalid tokens)
302
+ - Ping/pong keepalive
303
+ - Room join/leave with validation
304
+ - Client-to-client messaging
305
+ - Room broadcasting
306
+ - Global broadcasting
307
+ - REST API for Socket.IO management
308
+ - Graceful disconnection
309
+
310
+ Review the example code to understand production-ready patterns for:
311
+
312
+ - Binding multiple handlers in a single `setupSocketIO()` method
313
+ - Lazy getter pattern for accessing `SocketIOServerHelper` in services
314
+ - Custom event registration via `CLIENT_CONNECTED_HANDLER`
315
+ - Room validation logic preventing unauthorized room access
316
+ - Graceful shutdown sequence in `application.stop()`
317
+
318
+ ## See Also
319
+
320
+ - [Setup & Configuration](./) -- Quick reference, installation, bindings, constants
321
+ - [API Reference](./api) -- Architecture, method signatures, internals, types
322
+ - [Error Reference](./errors) -- Error conditions and troubleshooting
@@ -0,0 +1,261 @@
1
+ # Static Asset -- API Reference
2
+
3
+ > Controller factory, storage interface, type definitions, and component internals.
4
+
5
+ ## Controller Factory
6
+
7
+ The `AssetControllerFactory.defineAssetController()` method dynamically creates controller classes at runtime. For each storage backend in the options:
8
+
9
+ 1. A new class extending `BaseController` is created with `@controller({ path: basePath })`
10
+ 2. The class name is set dynamically via `Object.defineProperty(_controller, 'name', { value: name })`
11
+ 3. Routes are bound in the controller's `binding()` method using `this.bindRoute().to()`
12
+ 4. The controller is registered via `this.application.controller()`
13
+
14
+ ```
15
+ StaticAssetComponent.binding()
16
+ | iterates options
17
+ AssetControllerFactory.defineAssetController({ controller, storage, helper, ... })
18
+ | creates
19
+ @controller({ path: basePath })
20
+ class _controller extends BaseController { ... }
21
+ | registered via
22
+ this.application.controller(_controller)
23
+ ```
24
+
25
+ ### IAssetControllerOptions
26
+
27
+ The factory method accepts the following options:
28
+
29
+ ```typescript
30
+ interface IAssetControllerOptions {
31
+ controller: {
32
+ name: string;
33
+ basePath: string;
34
+ isStrict?: boolean; // Default: true
35
+ };
36
+ storage: TStaticAssetStorageType;
37
+ helper: IStorageHelper;
38
+ useMetaLink?: boolean;
39
+ metaLink?: TMetaLinkConfig;
40
+ options?: TStaticAssetExtraOptions;
41
+ }
42
+ ```
43
+
44
+ ## StaticAssetStorageTypes
45
+
46
+ A constants class following the Ignis pattern with `static readonly` fields, a `SCHEME_SET`, and an `isValid()` method:
47
+
48
+ ```typescript
49
+ class StaticAssetStorageTypes {
50
+ static readonly DISK = 'disk';
51
+ static readonly MINIO = 'minio';
52
+
53
+ static readonly SCHEME_SET = new Set([this.DISK, this.MINIO]);
54
+
55
+ static isValid(orgType: string): boolean {
56
+ return this.SCHEME_SET.has(orgType);
57
+ }
58
+ }
59
+
60
+ type TStaticAssetStorageType = TConstValue<typeof StaticAssetStorageTypes>;
61
+ // Resolves to: 'disk' | 'minio'
62
+ ```
63
+
64
+ ## MultipartBodySchema
65
+
66
+ The Zod schema used to validate the upload request body:
67
+
68
+ ```typescript
69
+ const MultipartBodySchema = z.object({
70
+ files: z.union([z.instanceof(File), z.array(z.instanceof(File))]).openapi({
71
+ type: 'array',
72
+ items: {
73
+ type: 'string',
74
+ format: 'binary',
75
+ },
76
+ }),
77
+ });
78
+ ```
79
+
80
+ This accepts either a single `File` or an array of `File` objects. The OpenAPI spec representation uses `type: 'array'` with `format: 'binary'` items for compatibility with Swagger/OpenAPI tooling.
81
+
82
+ ## Header Sanitization
83
+
84
+ When streaming files (both inline and download), the controller forwards a specific set of whitelisted headers from the storage metadata to the response. All other metadata headers are dropped.
85
+
86
+ ### WHITELIST_HEADERS
87
+
88
+ The exact list of forwarded headers:
89
+
90
+ ```typescript
91
+ const WHITELIST_HEADERS = [
92
+ 'content-type',
93
+ 'content-encoding',
94
+ 'cache-control',
95
+ 'etag',
96
+ 'last-modified',
97
+ ] as const;
98
+ ```
99
+
100
+ These correspond to `HTTP.Headers.CONTENT_TYPE`, `HTTP.Headers.CONTENT_ENCODING`, `HTTP.Headers.CACHE_CONTROL`, `HTTP.Headers.ETAG`, and `HTTP.Headers.LAST_MODIFIED` from `@venizia/ignis-helpers`.
101
+
102
+ All header values are sanitized by stripping `\r` and `\n` characters via `String(value).replace(/[\r\n]/g, '')` to prevent HTTP header injection attacks. If no `content-type` header is present in the storage metadata, the controller falls back to `application/octet-stream`.
103
+
104
+ ### HTTP Security Headers
105
+
106
+ All file streaming responses include:
107
+
108
+ ```http
109
+ X-Content-Type-Options: nosniff
110
+ Content-Type: <from metadata or application/octet-stream>
111
+ Content-Length: <file size in bytes>
112
+ Content-Disposition: attachment; filename="..." (download endpoint only)
113
+ ```
114
+
115
+ Whitelisted metadata headers forwarded from storage: `content-type`, `content-encoding`, `cache-control`, `etag`, `last-modified`. All other metadata headers are dropped. Header values are sanitized (see [Header Sanitization](#header-sanitization)).
116
+
117
+ ## IStorageHelper Interface
118
+
119
+ All storage helpers implement this unified interface:
120
+
121
+ ```typescript
122
+ interface IStorageHelper {
123
+ isValidName(name: string): boolean;
124
+
125
+ // Bucket operations
126
+ isBucketExists(opts: { name: string }): Promise<boolean>;
127
+ getBuckets(): Promise<IBucketInfo[]>;
128
+ getBucket(opts: { name: string }): Promise<IBucketInfo | null>;
129
+ createBucket(opts: { name: string }): Promise<IBucketInfo | null>;
130
+ removeBucket(opts: { name: string }): Promise<boolean>;
131
+
132
+ // File operations
133
+ upload(opts: {
134
+ bucket: string;
135
+ files: IUploadFile[];
136
+ normalizeNameFn?: (opts: { originalName: string; folderPath?: string }) => string;
137
+ normalizeLinkFn?: (opts: { bucketName: string; normalizeName: string }) => string;
138
+ }): Promise<IUploadResult[]>;
139
+
140
+ getFile(opts: { bucket: string; name: string; options?: any }): Promise<Readable>;
141
+ getStat(opts: { bucket: string; name: string }): Promise<IFileStat>;
142
+ removeObject(opts: { bucket: string; name: string }): Promise<void>;
143
+ removeObjects(opts: { bucket: string; names: string[] }): Promise<void>;
144
+ listObjects(opts: IListObjectsOptions): Promise<IObjectInfo[]>;
145
+
146
+ // Utility
147
+ getFileType(opts: { mimeType: string }): string;
148
+ }
149
+ ```
150
+
151
+ ### Supporting Types
152
+
153
+ ```typescript
154
+ interface IUploadFile {
155
+ originalName: string;
156
+ mimetype: string;
157
+ buffer: Buffer;
158
+ size: number;
159
+ encoding?: string;
160
+ folderPath?: string;
161
+ [key: string | symbol]: any;
162
+ }
163
+
164
+ interface IUploadResult {
165
+ bucketName: string;
166
+ objectName: string;
167
+ link: string;
168
+ metaLink?: any;
169
+ metaLinkError?: any;
170
+ }
171
+
172
+ interface IFileStat {
173
+ size: number;
174
+ metadata: Record<string, any>;
175
+ lastModified?: Date;
176
+ etag?: string;
177
+ versionId?: string;
178
+ }
179
+
180
+ interface IBucketInfo {
181
+ name: string;
182
+ creationDate: Date;
183
+ }
184
+
185
+ interface IObjectInfo {
186
+ name?: string;
187
+ size?: number;
188
+ lastModified?: Date;
189
+ etag?: string;
190
+ prefix?: string;
191
+ }
192
+
193
+ interface IListObjectsOptions {
194
+ bucket: string;
195
+ prefix?: string;
196
+ useRecursive?: boolean;
197
+ maxKeys?: number;
198
+ }
199
+ ```
200
+
201
+ ### Storage Helper Hierarchy
202
+
203
+ ```
204
+ IStorageHelper (interface)
205
+ |
206
+ BaseStorageHelper (abstract class)
207
+ |
208
+ +-- DiskHelper (local filesystem)
209
+ +-- MinioHelper (S3-compatible)
210
+ ```
211
+
212
+ ## MetaLink SQL Schema
213
+
214
+ **Table:** `MetaLink`
215
+
216
+ | Field | Type | Nullable | Default | Description |
217
+ |-------|------|----------|---------|-------------|
218
+ | `id` | TEXT | No | -- | Primary key (UUID) |
219
+ | `created_at` | TIMESTAMP | No | `NOW()` | When record was created |
220
+ | `modified_at` | TIMESTAMP | No | `NOW()` | When record was last updated |
221
+ | `bucket_name` | TEXT | No | -- | Storage bucket name |
222
+ | `object_name` | TEXT | No | -- | File object name |
223
+ | `link` | TEXT | No | -- | Access URL to the file |
224
+ | `mimetype` | TEXT | No | -- | File MIME type |
225
+ | `size` | INTEGER | No | -- | File size in bytes |
226
+ | `etag` | TEXT | Yes | -- | Entity tag for versioning |
227
+ | `metadata` | JSONB | Yes | -- | Additional file metadata |
228
+ | `storage_type` | TEXT | No | -- | Storage type (`'disk'` or `'minio'`) |
229
+ | `is_synced` | BOOLEAN | No | `false` | Whether MetaLink is synchronized with storage |
230
+ | `principal_type` | TEXT | Yes | -- | Type of the associated principal (e.g., `'user'`, `'service'`) |
231
+ | `principal_id` | TEXT | Yes | -- | ID of the associated principal (always stored as string) |
232
+
233
+ **Indexes:** `bucket_name`, `object_name`, `storage_type`, `is_synced`.
234
+
235
+ > [!NOTE]
236
+ > The `isSynced` field is automatically set to `true` when files are uploaded or synced via the meta-links endpoint. When a file is deleted, the MetaLink record is removed entirely. The `principalType` and `principalId` fields are only populated during upload when the corresponding query parameters are provided.
237
+
238
+ ### MetaLink Tracking
239
+
240
+ When `useMetaLink: true`, the component:
241
+
242
+ - **On upload:** Creates a MetaLink database record for each uploaded file after fetching file stats from storage. Stores `principalType` and `principalId` from query parameters (if provided). The `principalId` is always coerced to a string via `String()`. If MetaLink creation fails, the upload still succeeds and the response includes `metaLink: null` with a `metaLinkError` message.
243
+ - **On delete:** Initiates MetaLink record deletion as fire-and-forget (the `.then()/.catch()` promise chain is not awaited). The HTTP response with `{ "success": true }` returns before the database deletion completes. Deletion errors are logged but do not fail the request.
244
+ - **On sync (PUT meta-links):** Checks if a MetaLink exists for the object (matched by `bucketName` + `objectName`). Updates it if found, creates a new one if not. Always sets `isSynced: true`. Returns `{ success: true, metaLink: ... }`.
245
+
246
+ ## Component Lifecycle
247
+
248
+ 1. **`binding()`** -- Reads `STATIC_ASSET_COMPONENT_OPTIONS` from the DI container
249
+ 2. **Iterates each storage key** -- For each entry, calls `AssetControllerFactory.defineAssetController()`
250
+ 3. **Generates default `normalizeLinkFn`** -- If not provided, creates links in the format <code v-pre>{basePath}/buckets/{bucket}/objects/{encodedName}</code>
251
+ 4. **Registers controller** -- Calls `this.application.controller()` with the dynamically created class
252
+ 5. **Logs binding** -- Logs the storage key, type, and MetaLink status for each registered backend
253
+
254
+ > [!TIP]
255
+ > When MetaLink deletion fails on object delete, the error is logged but the HTTP response still returns `{ "success": true }`. Check your application logs if MetaLink records are not being cleaned up. Since the deletion is fire-and-forget, the response may return before the deletion attempt even starts.
256
+
257
+ ## See Also
258
+
259
+ - [Setup & Configuration](./) - Quick Reference, Setup Steps, Configuration Options
260
+ - [Usage & Examples](./usage) - API Endpoints and Frontend Integration
261
+ - [Error Reference](./errors) - Name Validation and Troubleshooting
@@ -0,0 +1,89 @@
1
+ # Static Asset -- Error Reference
2
+
3
+ > Name validation rules and troubleshooting guide for common issues.
4
+
5
+ ## Name Validation
6
+
7
+ All bucket and object names go through `helper.isValidName()` before any storage operation. The validation blocks:
8
+
9
+ | Pattern | Example | Reason |
10
+ |---------|---------|--------|
11
+ | Path traversal | `../etc/passwd` | Contains `..`, `/`, or `\` |
12
+ | Hidden files | `.hidden` | Starts with `.` |
13
+ | Shell injection | `file;rm -rf /` | Contains `;`, `\|`, `&`, `$`, etc. |
14
+ | Header injection | `file\ninjected` | Contains `\n`, `\r`, or `\0` |
15
+ | Long names | 256+ chars | Exceeds 255 character limit |
16
+ | Empty names | `""`, `" "` | Empty or whitespace-only |
17
+
18
+ Every endpoint that accepts `bucketName` validates it and returns HTTP 400 `"Invalid bucket name"` on failure. Every endpoint that accepts `objectName` validates it separately and returns HTTP 400 `"Invalid object name"` on failure.
19
+
20
+ ## Troubleshooting
21
+
22
+ ### "Invalid bucket/object name"
23
+
24
+ **Cause:** The name fails `isValidName()` validation. Names cannot contain `..`, `/`, `\`, shell special characters, or start with `.`. Names must be <= 255 characters and non-empty.
25
+
26
+ **Fix:** Ensure names follow these rules:
27
+ - No path separators (`..`, `/`, `\`)
28
+ - No leading dot (`.hidden`)
29
+ - No shell special characters (`;`, `|`, `&`, `$`, `` ` ``, `<`, `>`, `(`, `)`, `{`, `}`, `[`, `]`, `!`, `#`)
30
+ - No control characters (`\n`, `\r`, `\0`)
31
+ - 255 characters or fewer
32
+ - Not empty or whitespace-only
33
+
34
+ > [!NOTE]
35
+ > Every bucket-related endpoint (`GET`, `POST`, `DELETE` on `/buckets/:bucketName`) validates the bucket name and returns HTTP 400 with `"Invalid bucket name"` if validation fails. Object-related endpoints validate both bucket and object names.
36
+
37
+ ### "Controller not registering"
38
+
39
+ **Cause:** Configuration key might be invalid or missing required fields.
40
+
41
+ **Fix:** Ensure each storage configuration has all required fields:
42
+
43
+ ```typescript
44
+ {
45
+ [uniqueKey]: {
46
+ controller: { name, basePath, isStrict },
47
+ storage: 'disk' | 'minio',
48
+ helper: IStorageHelper,
49
+ extra: {}
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### "Files not uploading (DiskHelper)"
55
+
56
+ **Cause:** The `basePath` directory does not exist or the process lacks filesystem permissions.
57
+
58
+ **Fix:** Ensure the `basePath` directory exists or can be created, and verify the process has read/write permissions to the target path.
59
+
60
+ ### "Files not uploading (MinioHelper)"
61
+
62
+ **Cause:** MinIO server connectivity or authentication failure.
63
+
64
+ **Fix:**
65
+ - Verify MinIO server is running
66
+ - Check credentials (`accessKey`, `secretKey`)
67
+ - Verify network connectivity (`endPoint`, `port`)
68
+ - Check if `useSSL` matches your server configuration
69
+
70
+ ### "Large file uploads failing"
71
+
72
+ **Cause:** Memory-based multipart parsing cannot handle the file size.
73
+
74
+ **Fix:** Switch to disk-based multipart parsing:
75
+
76
+ ```typescript
77
+ extra: {
78
+ parseMultipartBody: {
79
+ storage: 'disk',
80
+ uploadDir: './uploads',
81
+ },
82
+ }
83
+ ```
84
+
85
+ ## See Also
86
+
87
+ - [Setup & Configuration](./) - Quick Reference, Setup Steps, Configuration Options
88
+ - [Usage & Examples](./usage) - API Endpoints and Frontend Integration
89
+ - [API Reference](./api) - Controller Factory, Storage Interface, MetaLink Schema