@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.
- package/package.json +1 -1
- package/wiki/best-practices/architecture-decisions.md +0 -8
- package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
- package/wiki/best-practices/performance-optimization.md +3 -3
- package/wiki/best-practices/security-guidelines.md +2 -2
- package/wiki/best-practices/troubleshooting-tips.md +1 -1
- package/wiki/guides/core-concepts/components-guide.md +1 -1
- package/wiki/guides/core-concepts/components.md +2 -2
- package/wiki/guides/core-concepts/dependency-injection.md +1 -1
- package/wiki/guides/core-concepts/services.md +1 -1
- package/wiki/guides/tutorials/building-a-crud-api.md +1 -1
- package/wiki/guides/tutorials/ecommerce-api.md +2 -2
- package/wiki/guides/tutorials/realtime-chat.md +6 -6
- package/wiki/guides/tutorials/testing.md +1 -1
- package/wiki/references/base/bootstrapping.md +0 -2
- package/wiki/references/base/components.md +2 -2
- package/wiki/references/base/controllers.md +0 -1
- package/wiki/references/base/datasources.md +1 -1
- package/wiki/references/base/dependency-injection.md +1 -1
- package/wiki/references/base/filter-system/quick-reference.md +0 -14
- package/wiki/references/base/middlewares.md +0 -8
- package/wiki/references/base/providers.md +0 -9
- package/wiki/references/base/services.md +0 -1
- package/wiki/references/components/authentication/api.md +444 -0
- package/wiki/references/components/authentication/errors.md +177 -0
- package/wiki/references/components/authentication/index.md +571 -0
- package/wiki/references/components/authentication/usage.md +781 -0
- package/wiki/references/components/health-check.md +292 -103
- package/wiki/references/components/index.md +14 -12
- package/wiki/references/components/mail/api.md +505 -0
- package/wiki/references/components/mail/errors.md +176 -0
- package/wiki/references/components/mail/index.md +535 -0
- package/wiki/references/components/mail/usage.md +404 -0
- package/wiki/references/components/request-tracker.md +229 -25
- package/wiki/references/components/socket-io/api.md +1051 -0
- package/wiki/references/components/socket-io/errors.md +119 -0
- package/wiki/references/components/socket-io/index.md +410 -0
- package/wiki/references/components/socket-io/usage.md +322 -0
- package/wiki/references/components/static-asset/api.md +261 -0
- package/wiki/references/components/static-asset/errors.md +89 -0
- package/wiki/references/components/static-asset/index.md +617 -0
- package/wiki/references/components/static-asset/usage.md +364 -0
- package/wiki/references/components/swagger.md +390 -110
- package/wiki/references/components/template/api-page.md +125 -0
- package/wiki/references/components/template/errors-page.md +100 -0
- package/wiki/references/components/template/index.md +104 -0
- package/wiki/references/components/template/setup-page.md +134 -0
- package/wiki/references/components/template/single-page.md +132 -0
- package/wiki/references/components/template/usage-page.md +127 -0
- package/wiki/references/components/websocket/api.md +508 -0
- package/wiki/references/components/websocket/errors.md +123 -0
- package/wiki/references/components/websocket/index.md +453 -0
- package/wiki/references/components/websocket/usage.md +475 -0
- package/wiki/references/helpers/cron/index.md +224 -0
- package/wiki/references/helpers/crypto/index.md +537 -0
- package/wiki/references/helpers/env/index.md +214 -0
- package/wiki/references/helpers/error/index.md +232 -0
- package/wiki/references/helpers/index.md +16 -15
- package/wiki/references/helpers/inversion/index.md +608 -0
- package/wiki/references/helpers/logger/index.md +600 -0
- package/wiki/references/helpers/network/api.md +986 -0
- package/wiki/references/helpers/network/index.md +620 -0
- package/wiki/references/helpers/queue/index.md +589 -0
- package/wiki/references/helpers/redis/index.md +495 -0
- package/wiki/references/helpers/socket-io/api.md +497 -0
- package/wiki/references/helpers/socket-io/index.md +513 -0
- package/wiki/references/helpers/storage/api.md +705 -0
- package/wiki/references/helpers/storage/index.md +583 -0
- package/wiki/references/helpers/template/index.md +66 -0
- package/wiki/references/helpers/template/single-page.md +126 -0
- package/wiki/references/helpers/testing/index.md +510 -0
- package/wiki/references/helpers/types/index.md +512 -0
- package/wiki/references/helpers/uid/index.md +272 -0
- package/wiki/references/helpers/websocket/api.md +736 -0
- package/wiki/references/helpers/websocket/index.md +574 -0
- package/wiki/references/helpers/worker-thread/index.md +470 -0
- package/wiki/references/quick-reference.md +3 -18
- package/wiki/references/utilities/jsx.md +1 -8
- package/wiki/references/utilities/statuses.md +0 -7
- package/wiki/references/components/authentication.md +0 -476
- package/wiki/references/components/mail.md +0 -687
- package/wiki/references/components/socket-io.md +0 -562
- package/wiki/references/components/static-asset.md +0 -1277
- package/wiki/references/helpers/cron.md +0 -108
- package/wiki/references/helpers/crypto.md +0 -132
- package/wiki/references/helpers/env.md +0 -83
- package/wiki/references/helpers/error.md +0 -97
- package/wiki/references/helpers/inversion.md +0 -176
- package/wiki/references/helpers/logger.md +0 -296
- package/wiki/references/helpers/network.md +0 -396
- package/wiki/references/helpers/queue.md +0 -150
- package/wiki/references/helpers/redis.md +0 -142
- package/wiki/references/helpers/socket-io.md +0 -932
- package/wiki/references/helpers/storage.md +0 -665
- package/wiki/references/helpers/testing.md +0 -133
- package/wiki/references/helpers/types.md +0 -167
- package/wiki/references/helpers/uid.md +0 -167
- 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
|