@navios/core 0.3.0 → 0.5.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/README.md +96 -3
- package/docs/README.md +310 -3
- package/docs/adapters.md +308 -0
- package/docs/application-setup.md +524 -0
- package/docs/attributes.md +689 -0
- package/docs/controllers.md +373 -0
- package/docs/endpoints.md +444 -0
- package/docs/exceptions.md +316 -0
- package/docs/guards.md +550 -0
- package/docs/modules.md +251 -0
- package/docs/quick-start.md +295 -0
- package/docs/services.md +428 -0
- package/docs/testing.md +704 -0
- package/lib/_tsup-dts-rollup.d.mts +313 -280
- package/lib/_tsup-dts-rollup.d.ts +313 -280
- package/lib/index.d.mts +47 -26
- package/lib/index.d.ts +47 -26
- package/lib/index.js +633 -1068
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +632 -1061
- package/lib/index.mjs.map +1 -1
- package/package.json +11 -12
- package/project.json +17 -4
- package/src/__tests__/config.service.spec.mts +11 -9
- package/src/__tests__/controller.spec.mts +1 -2
- package/src/attribute.factory.mts +1 -1
- package/src/config/config.provider.mts +2 -2
- package/src/config/config.service.mts +4 -4
- package/src/decorators/controller.decorator.mts +1 -1
- package/src/decorators/endpoint.decorator.mts +9 -10
- package/src/decorators/header.decorator.mts +1 -1
- package/src/decorators/multipart.decorator.mts +5 -5
- package/src/decorators/stream.decorator.mts +5 -6
- package/src/factories/endpoint-adapter.factory.mts +21 -0
- package/src/factories/http-adapter.factory.mts +20 -0
- package/src/factories/index.mts +6 -0
- package/src/factories/multipart-adapter.factory.mts +21 -0
- package/src/factories/reply.factory.mts +21 -0
- package/src/factories/request.factory.mts +21 -0
- package/src/factories/stream-adapter.factory.mts +20 -0
- package/src/index.mts +1 -1
- package/src/interfaces/abstract-execution-context.inteface.mts +13 -0
- package/src/interfaces/abstract-http-adapter.interface.mts +20 -0
- package/src/interfaces/abstract-http-cors-options.interface.mts +59 -0
- package/src/interfaces/abstract-http-handler-adapter.interface.mts +13 -0
- package/src/interfaces/abstract-http-listen-options.interface.mts +4 -0
- package/src/interfaces/can-activate.mts +4 -2
- package/src/interfaces/http-header.mts +18 -0
- package/src/interfaces/index.mts +6 -0
- package/src/logger/console-logger.service.mts +28 -44
- package/src/logger/index.mts +1 -2
- package/src/logger/logger.service.mts +9 -128
- package/src/logger/logger.tokens.mts +21 -0
- package/src/metadata/handler.metadata.mts +7 -5
- package/src/navios.application.mts +65 -172
- package/src/navios.environment.mts +30 -0
- package/src/navios.factory.mts +53 -12
- package/src/services/guard-runner.service.mts +19 -9
- package/src/services/index.mts +0 -2
- package/src/services/module-loader.service.mts +4 -3
- package/src/tokens/endpoint-adapter.token.mts +8 -0
- package/src/tokens/execution-context.token.mts +2 -2
- package/src/tokens/http-adapter.token.mts +8 -0
- package/src/tokens/index.mts +4 -1
- package/src/tokens/multipart-adapter.token.mts +8 -0
- package/src/tokens/reply.token.mts +1 -5
- package/src/tokens/request.token.mts +1 -7
- package/src/tokens/stream-adapter.token.mts +8 -0
- package/tsconfig.json +6 -1
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +12 -0
- package/tsup.config.mts +1 -0
- package/docs/recipes/prisma.md +0 -60
- package/examples/simple-test/api/index.mts +0 -64
- package/examples/simple-test/config/config.service.mts +0 -14
- package/examples/simple-test/config/configuration.mts +0 -7
- package/examples/simple-test/index.mts +0 -16
- package/examples/simple-test/src/acl/acl-modern.guard.mts +0 -15
- package/examples/simple-test/src/acl/acl.guard.mts +0 -14
- package/examples/simple-test/src/acl/app.guard.mts +0 -27
- package/examples/simple-test/src/acl/one-more.guard.mts +0 -15
- package/examples/simple-test/src/acl/public.attribute.mts +0 -21
- package/examples/simple-test/src/app.module.mts +0 -9
- package/examples/simple-test/src/user/user.controller.mts +0 -72
- package/examples/simple-test/src/user/user.module.mts +0 -14
- package/examples/simple-test/src/user/user.service.mts +0 -14
- package/src/adapters/endpoint-adapter.service.mts +0 -72
- package/src/adapters/handler-adapter.interface.mts +0 -21
- package/src/adapters/index.mts +0 -4
- package/src/adapters/multipart-adapter.service.mts +0 -131
- package/src/adapters/stream-adapter.service.mts +0 -91
- package/src/logger/logger.factory.mts +0 -36
- package/src/logger/pino-wrapper.mts +0 -64
- package/src/services/controller-adapter.service.mts +0 -124
- package/src/services/execution-context.mts +0 -54
- package/src/tokens/application.token.mts +0 -9
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
# Attribute System
|
|
2
|
+
|
|
3
|
+
Navios provides a powerful attribute system that allows you to attach metadata to classes and methods. Attributes are similar to annotations or decorators in other frameworks and provide a flexible way to extend functionality.
|
|
4
|
+
|
|
5
|
+
## What are Attributes?
|
|
6
|
+
|
|
7
|
+
Attributes are metadata that can be attached to classes, methods, or other elements in your application. They are created using the `AttributeFactory` and can store typed data that can be retrieved and used by your application logic.
|
|
8
|
+
|
|
9
|
+
## Creating Attributes
|
|
10
|
+
|
|
11
|
+
### Basic Attribute
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { AttributeFactory } from '@navios/core'
|
|
15
|
+
|
|
16
|
+
// Create a simple attribute without data
|
|
17
|
+
export const Deprecated = AttributeFactory.createAttribute(Symbol('Deprecated'))
|
|
18
|
+
|
|
19
|
+
// Usage
|
|
20
|
+
@Deprecated()
|
|
21
|
+
export class OldController {
|
|
22
|
+
@Deprecated()
|
|
23
|
+
async oldMethod() {
|
|
24
|
+
// This method is marked as deprecated
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Schema-Based Attribute
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { AttributeFactory } from '@navios/core'
|
|
33
|
+
|
|
34
|
+
import { z } from 'zod'
|
|
35
|
+
|
|
36
|
+
// Create an attribute with a Zod schema for validation
|
|
37
|
+
const CacheOptionsSchema = z.object({
|
|
38
|
+
ttl: z.number().min(0),
|
|
39
|
+
key: z.string().optional(),
|
|
40
|
+
tags: z.array(z.string()).optional(),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const Cache = AttributeFactory.createAttribute(
|
|
44
|
+
Symbol('Cache'),
|
|
45
|
+
CacheOptionsSchema,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// Usage with typed data
|
|
49
|
+
@Controller()
|
|
50
|
+
export class UserController {
|
|
51
|
+
@Cache({ ttl: 300, key: 'user-list', tags: ['users'] })
|
|
52
|
+
@Endpoint(userListEndpoint)
|
|
53
|
+
async getUsers() {
|
|
54
|
+
// This endpoint will be cached for 5 minutes
|
|
55
|
+
return this.userService.findAll()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Attribute Types
|
|
61
|
+
|
|
62
|
+
### Class Attributes
|
|
63
|
+
|
|
64
|
+
Attributes that can be applied to classes:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const RoleSchema = z.object({
|
|
68
|
+
roles: z.array(z.string()),
|
|
69
|
+
requireAll: z.boolean().default(false),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
export const RequireRoles = AttributeFactory.createAttribute(
|
|
73
|
+
Symbol('RequireRoles'),
|
|
74
|
+
RoleSchema,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@RequireRoles({ roles: ['admin', 'moderator'] })
|
|
78
|
+
@Controller()
|
|
79
|
+
export class AdminController {
|
|
80
|
+
// All methods in this controller require admin or moderator role
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Method Attributes
|
|
85
|
+
|
|
86
|
+
Attributes that can be applied to methods:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const RateLimitSchema = z.object({
|
|
90
|
+
requests: z.number().min(1),
|
|
91
|
+
windowMs: z.number().min(1000),
|
|
92
|
+
message: z.string().optional(),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
export const RateLimit = AttributeFactory.createAttribute(
|
|
96
|
+
Symbol('RateLimit'),
|
|
97
|
+
RateLimitSchema,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@Controller()
|
|
101
|
+
export class ApiController {
|
|
102
|
+
@RateLimit({ requests: 100, windowMs: 60000 })
|
|
103
|
+
@Endpoint(dataEndpoint)
|
|
104
|
+
async createData() {
|
|
105
|
+
// Limited to 100 requests per minute
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Reading Attributes
|
|
111
|
+
|
|
112
|
+
### AttributeFactory Methods
|
|
113
|
+
|
|
114
|
+
The `AttributeFactory` provides several methods for reading attributes:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { AttributeFactory } from '@navios/core'
|
|
118
|
+
|
|
119
|
+
// Get attribute value from a single metadata object
|
|
120
|
+
AttributeFactory.get(attribute, metadata)
|
|
121
|
+
// Returns: attribute value or null if not found
|
|
122
|
+
|
|
123
|
+
// Check if attribute exists on metadata object
|
|
124
|
+
AttributeFactory.has(attribute, metadata)
|
|
125
|
+
// Returns: boolean
|
|
126
|
+
|
|
127
|
+
// Get all instances of an attribute from metadata object
|
|
128
|
+
AttributeFactory.getAll(attribute, metadata)
|
|
129
|
+
// Returns: array of values or null if none found
|
|
130
|
+
|
|
131
|
+
// Get the last/most specific attribute from an array of metadata objects
|
|
132
|
+
// Searches from right to left (most specific to least specific)
|
|
133
|
+
AttributeFactory.getLast(attribute, [
|
|
134
|
+
moduleMetadata,
|
|
135
|
+
controllerMetadata,
|
|
136
|
+
handlerMetadata,
|
|
137
|
+
])
|
|
138
|
+
// Returns: attribute value from the most specific level or null
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Reading Attributes from Metadata
|
|
142
|
+
|
|
143
|
+
You can read attributes from metadata objects using `AttributeFactory`:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { AttributeFactory } from '@navios/core'
|
|
147
|
+
|
|
148
|
+
@Module({
|
|
149
|
+
controllers: [UserController],
|
|
150
|
+
})
|
|
151
|
+
@RequireRoles({ roles: ['admin'] })
|
|
152
|
+
export class AdminModule {}
|
|
153
|
+
|
|
154
|
+
// Reading from metadata objects directly
|
|
155
|
+
const moduleMetadata = extractModuleMetadata(AdminModule)
|
|
156
|
+
const roleRequirement = AttributeFactory.get(RequireRoles, moduleMetadata)
|
|
157
|
+
// roleRequirement = { roles: ['admin'], requireAll: false }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Reading from Controller Metadata
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import { AttributeFactory, extractControllerMetadata } from '@navios/core'
|
|
164
|
+
|
|
165
|
+
@Cache({ ttl: 600 })
|
|
166
|
+
@Controller()
|
|
167
|
+
export class UserController {}
|
|
168
|
+
|
|
169
|
+
// Read the attribute
|
|
170
|
+
const metadata = extractControllerMetadata(UserController)
|
|
171
|
+
const cacheConfig = AttributeFactory.get(Cache, metadata)
|
|
172
|
+
// cacheConfig = { ttl: 600 }
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Reading from Execution Context
|
|
176
|
+
|
|
177
|
+
The most common way to read attributes is from the execution context in guards, interceptors, or middleware:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import type { AbstractExecutionContext } from '@navios/core'
|
|
181
|
+
|
|
182
|
+
import { AttributeFactory } from '@navios/core'
|
|
183
|
+
|
|
184
|
+
@Controller()
|
|
185
|
+
export class UserController {
|
|
186
|
+
@RateLimit({ requests: 10, windowMs: 60000 })
|
|
187
|
+
@Endpoint(createUserEndpoint)
|
|
188
|
+
async createUser() {}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Reading attributes in a guard or interceptor
|
|
192
|
+
function readAttributesFromContext(executionContext: AbstractExecutionContext) {
|
|
193
|
+
const handlerMetadata = executionContext.getHandler()
|
|
194
|
+
const controllerMetadata = executionContext.getController()
|
|
195
|
+
const moduleMetadata = executionContext.getModule()
|
|
196
|
+
|
|
197
|
+
// Read from specific metadata
|
|
198
|
+
const rateLimitConfig = AttributeFactory.get(RateLimit, handlerMetadata)
|
|
199
|
+
// rateLimitConfig = { requests: 10, windowMs: 60000 }
|
|
200
|
+
|
|
201
|
+
// Check if attribute exists
|
|
202
|
+
const hasRateLimit = AttributeFactory.has(RateLimit, handlerMetadata)
|
|
203
|
+
|
|
204
|
+
// Get attribute value from hierarchy (handler -> controller -> module)
|
|
205
|
+
const authRequired = AttributeFactory.getLast(RequireAuth, [
|
|
206
|
+
moduleMetadata,
|
|
207
|
+
controllerMetadata,
|
|
208
|
+
handlerMetadata,
|
|
209
|
+
])
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Common Attribute Patterns
|
|
214
|
+
|
|
215
|
+
### Authorization Attributes
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const PermissionSchema = z.object({
|
|
219
|
+
resource: z.string(),
|
|
220
|
+
action: z.string(),
|
|
221
|
+
ownership: z.boolean().default(false),
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
export const RequirePermission = AttributeFactory.createAttribute(
|
|
225
|
+
Symbol('RequirePermission'),
|
|
226
|
+
PermissionSchema,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@Controller()
|
|
230
|
+
export class PostController {
|
|
231
|
+
@RequirePermission({
|
|
232
|
+
resource: 'post',
|
|
233
|
+
action: 'delete',
|
|
234
|
+
ownership: true,
|
|
235
|
+
})
|
|
236
|
+
@Endpoint(deletePostEndpoint)
|
|
237
|
+
async deletePost({ params }: { params: { id: string } }) {
|
|
238
|
+
// User must have delete permission on posts and own the post
|
|
239
|
+
return this.postService.delete(params.id)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Validation Attributes
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
const ValidateParamsSchema = z.object({
|
|
248
|
+
schema: z.any(), // ZodSchema
|
|
249
|
+
transform: z.boolean().default(false),
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
export const ValidateParams = AttributeFactory.createAttribute(
|
|
253
|
+
Symbol('ValidateParams'),
|
|
254
|
+
ValidateParamsSchema,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
const UuidParamsSchema = z.object({
|
|
258
|
+
id: z.string().uuid(),
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
@Controller()
|
|
262
|
+
export class UserController {
|
|
263
|
+
@ValidateParams({ schema: UuidParamsSchema, transform: true })
|
|
264
|
+
@Endpoint(byIdEndpoint)
|
|
265
|
+
async getUserById({ params }: { params: { id: string } }) {
|
|
266
|
+
// params.id is validated as UUID
|
|
267
|
+
return this.userService.findById(params.id)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Logging Attributes
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const LogSchema = z.object({
|
|
276
|
+
level: z.enum(['debug', 'info', 'warn', 'error']),
|
|
277
|
+
message: z.string().optional(),
|
|
278
|
+
includeRequest: z.boolean().default(false),
|
|
279
|
+
includeResponse: z.boolean().default(false),
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
export const Log = AttributeFactory.createAttribute(Symbol('Log'), LogSchema)
|
|
283
|
+
|
|
284
|
+
@Controller()
|
|
285
|
+
export class UserController {
|
|
286
|
+
@Log({
|
|
287
|
+
level: 'info',
|
|
288
|
+
message: 'User login attempt',
|
|
289
|
+
includeRequest: true,
|
|
290
|
+
})
|
|
291
|
+
@Endpoint(loginEndpoint)
|
|
292
|
+
async login({ body }: { body: LoginDto }) {
|
|
293
|
+
// Login attempts are logged with request details
|
|
294
|
+
return this.authService.login(body)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Caching Attributes
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
const CacheSchema = z.object({
|
|
303
|
+
ttl: z.number().min(0),
|
|
304
|
+
key: z.string().optional(),
|
|
305
|
+
tags: z.array(z.string()).default([]),
|
|
306
|
+
invalidateOn: z.array(z.string()).optional(),
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
export const Cache = AttributeFactory.createAttribute(
|
|
310
|
+
Symbol('Cache'),
|
|
311
|
+
CacheSchema,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
@Controller()
|
|
315
|
+
export class UserController {
|
|
316
|
+
@Cache({
|
|
317
|
+
ttl: 300,
|
|
318
|
+
key: 'user-{id}',
|
|
319
|
+
tags: ['users'],
|
|
320
|
+
invalidateOn: ['user-updated', 'user-deleted'],
|
|
321
|
+
})
|
|
322
|
+
@Endpoint(getUserByIdEndpoint)
|
|
323
|
+
async getUserById({ params }: { params: { id: string } }) {
|
|
324
|
+
// Response cached for 5 minutes with dynamic key
|
|
325
|
+
return this.userService.findById(params.id)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Practical Examples
|
|
331
|
+
|
|
332
|
+
### Using AttributeFactory.getLast for Hierarchical Configuration
|
|
333
|
+
|
|
334
|
+
`AttributeFactory.getLast` is particularly useful when you want to support attribute inheritance from module → controller → handler levels:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import type { AbstractExecutionContext, CanActivate } from '@navios/core'
|
|
338
|
+
|
|
339
|
+
import { AttributeFactory, inject, Injectable, Logger } from '@navios/di'
|
|
340
|
+
|
|
341
|
+
// Define a Public attribute that can skip authentication
|
|
342
|
+
const PublicSymbol = Symbol.for('Public')
|
|
343
|
+
export const Public = AttributeFactory.createAttribute(PublicSymbol)
|
|
344
|
+
|
|
345
|
+
// Define roles attribute with schema validation
|
|
346
|
+
const RolesSchema = z.object({
|
|
347
|
+
roles: z.array(z.enum(['VIEWER', 'USER', 'ADMIN', 'OWNER'])),
|
|
348
|
+
})
|
|
349
|
+
export const Roles = AttributeFactory.createAttribute(
|
|
350
|
+
Symbol.for('Roles'),
|
|
351
|
+
RolesSchema,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
@Injectable()
|
|
355
|
+
export class SmartAuthGuard implements CanActivate {
|
|
356
|
+
private logger = inject(Logger, { context: 'SmartAuthGuard' })
|
|
357
|
+
|
|
358
|
+
async canActivate(
|
|
359
|
+
executionContext: AbstractExecutionContext,
|
|
360
|
+
): Promise<boolean> {
|
|
361
|
+
// Check if endpoint is public (searches handler -> controller -> module)
|
|
362
|
+
const isPublic = AttributeFactory.getLast(Public, [
|
|
363
|
+
executionContext.getModule(),
|
|
364
|
+
executionContext.getController(),
|
|
365
|
+
executionContext.getHandler(),
|
|
366
|
+
])
|
|
367
|
+
|
|
368
|
+
if (isPublic) {
|
|
369
|
+
this.logger.debug('Public endpoint, allowing access')
|
|
370
|
+
return true
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Get required roles with inheritance
|
|
374
|
+
const roleConfig = AttributeFactory.getLast(Roles, [
|
|
375
|
+
executionContext.getModule(),
|
|
376
|
+
executionContext.getController(),
|
|
377
|
+
executionContext.getHandler(),
|
|
378
|
+
])
|
|
379
|
+
|
|
380
|
+
const request = executionContext.getRequest()
|
|
381
|
+
const user = request.user
|
|
382
|
+
|
|
383
|
+
if (!user) {
|
|
384
|
+
return false // Not authenticated
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!roleConfig) {
|
|
388
|
+
return true // Authenticated but no specific role requirements
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Check if user has required roles
|
|
392
|
+
return roleConfig.roles.some((role) => user.roles.includes(role))
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Usage - attributes are inherited hierarchically
|
|
397
|
+
@Roles({ roles: ['USER'] }) // Default: all endpoints require USER role
|
|
398
|
+
@Module({
|
|
399
|
+
controllers: [UserController],
|
|
400
|
+
guards: [SmartAuthGuard],
|
|
401
|
+
})
|
|
402
|
+
export class UserModule {}
|
|
403
|
+
|
|
404
|
+
@Roles({ roles: ['ADMIN'] }) // Override: all endpoints in this controller require ADMIN
|
|
405
|
+
@Controller()
|
|
406
|
+
export class UserController {
|
|
407
|
+
@Public() // Override: this specific endpoint is public
|
|
408
|
+
@Endpoint(healthCheckEndpoint)
|
|
409
|
+
async healthCheck() {
|
|
410
|
+
return { status: 'ok' }
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
@Endpoint(getUsersEndpoint)
|
|
414
|
+
async getUsers() {
|
|
415
|
+
// Inherits ADMIN requirement from controller
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@Roles({ roles: ['OWNER'] }) // Override: this endpoint requires OWNER role
|
|
419
|
+
@Endpoint(deleteUserEndpoint)
|
|
420
|
+
async deleteUser() {
|
|
421
|
+
// Most specific: requires OWNER role
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Reading Multiple Attributes
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
@Injectable()
|
|
430
|
+
export class ConfigurableGuard implements CanActivate {
|
|
431
|
+
async canActivate(
|
|
432
|
+
executionContext: AbstractExecutionContext,
|
|
433
|
+
): Promise<boolean> {
|
|
434
|
+
const handler = executionContext.getHandler()
|
|
435
|
+
const controller = executionContext.getController()
|
|
436
|
+
const module = executionContext.getModule()
|
|
437
|
+
|
|
438
|
+
// Check multiple attributes at handler level
|
|
439
|
+
const isPublic = AttributeFactory.get(Public, handler)
|
|
440
|
+
const requiredRoles = AttributeFactory.get(Roles, handler)
|
|
441
|
+
const cacheConfig = AttributeFactory.get(Cache, handler)
|
|
442
|
+
|
|
443
|
+
// Use getLast for fallback chain
|
|
444
|
+
const authConfig = AttributeFactory.getLast(AuthConfig, [
|
|
445
|
+
module,
|
|
446
|
+
controller,
|
|
447
|
+
handler,
|
|
448
|
+
])
|
|
449
|
+
|
|
450
|
+
// Implement your logic based on multiple attributes
|
|
451
|
+
if (isPublic) return true
|
|
452
|
+
if (authConfig?.disabled) return true
|
|
453
|
+
|
|
454
|
+
// Continue with role checking...
|
|
455
|
+
return this.checkRoles(requiredRoles, executionContext)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Implementing Attribute Handlers
|
|
461
|
+
|
|
462
|
+
### Creating Middleware for Attributes
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
import type { AbstractExecutionContext } from '@navios/core'
|
|
466
|
+
|
|
467
|
+
import { AttributeFactory, inject, Injectable, Logger } from '@navios/di'
|
|
468
|
+
|
|
469
|
+
@Injectable()
|
|
470
|
+
export class CacheMiddleware {
|
|
471
|
+
private cacheService = inject(CacheService)
|
|
472
|
+
private logger = inject(Logger, { context: 'CacheMiddleware' })
|
|
473
|
+
|
|
474
|
+
async handle(executionContext: AbstractExecutionContext, next: Function) {
|
|
475
|
+
// Read cache configuration from attributes
|
|
476
|
+
const cacheConfig = AttributeFactory.getLast(Cache, [
|
|
477
|
+
executionContext.getModule(),
|
|
478
|
+
executionContext.getController(),
|
|
479
|
+
executionContext.getHandler(),
|
|
480
|
+
])
|
|
481
|
+
|
|
482
|
+
if (!cacheConfig) {
|
|
483
|
+
return next()
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const request = executionContext.getRequest()
|
|
487
|
+
|
|
488
|
+
// Generate cache key
|
|
489
|
+
const key = this.generateCacheKey(cacheConfig.key, request)
|
|
490
|
+
|
|
491
|
+
// Try to get from cache
|
|
492
|
+
const cached = await this.cacheService.get(key)
|
|
493
|
+
if (cached) {
|
|
494
|
+
this.logger.debug(`Cache hit for key: ${key}`)
|
|
495
|
+
return cached
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Execute endpoint
|
|
499
|
+
const result = await next()
|
|
500
|
+
|
|
501
|
+
// Store in cache
|
|
502
|
+
await this.cacheService.set(key, result, cacheConfig.ttl)
|
|
503
|
+
this.logger.debug(`Cached result for key: ${key}`)
|
|
504
|
+
|
|
505
|
+
return result
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private generateCacheKey(template: string, request: any): string {
|
|
509
|
+
return template.replace(/{(\w+)}/g, (match, param) => {
|
|
510
|
+
return request.params[param] || match
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Using Attributes in Guards
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
import type { AbstractExecutionContext, CanActivate } from '@navios/core'
|
|
520
|
+
|
|
521
|
+
import { AttributeFactory, inject, Injectable } from '@navios/di'
|
|
522
|
+
|
|
523
|
+
@Injectable()
|
|
524
|
+
export class RoleGuard implements CanActivate {
|
|
525
|
+
private authService = inject(AuthService)
|
|
526
|
+
|
|
527
|
+
async canActivate(
|
|
528
|
+
executionContext: AbstractExecutionContext,
|
|
529
|
+
): Promise<boolean> {
|
|
530
|
+
// Use getLast to get the most specific role requirement
|
|
531
|
+
const requiredRoles = AttributeFactory.getLast(RequireRoles, [
|
|
532
|
+
executionContext.getModule(),
|
|
533
|
+
executionContext.getController(),
|
|
534
|
+
executionContext.getHandler(),
|
|
535
|
+
])
|
|
536
|
+
|
|
537
|
+
if (!requiredRoles) {
|
|
538
|
+
return true // No role requirement
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const request = executionContext.getRequest()
|
|
542
|
+
const user = await this.authService.getCurrentUser(request)
|
|
543
|
+
if (!user) {
|
|
544
|
+
return false
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Check if user has required roles
|
|
548
|
+
const hasRequiredRole = requiredRoles.requireAll
|
|
549
|
+
? requiredRoles.roles.every((role) => user.roles.includes(role))
|
|
550
|
+
: requiredRoles.roles.some((role) => user.roles.includes(role))
|
|
551
|
+
|
|
552
|
+
return hasRequiredRole
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
## Advanced Attribute Usage
|
|
558
|
+
|
|
559
|
+
### Composable Attributes
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// Create multiple attributes that work together
|
|
563
|
+
export const Authenticated = AttributeFactory.createAttribute(
|
|
564
|
+
Symbol('Authenticated'),
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
export const RequireOwnership = AttributeFactory.createAttribute(
|
|
568
|
+
Symbol('RequireOwnership'),
|
|
569
|
+
z.object({
|
|
570
|
+
resourceParam: z.string().default('id'),
|
|
571
|
+
userProperty: z.string().default('userId'),
|
|
572
|
+
}),
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
@Controller()
|
|
576
|
+
export class PostController {
|
|
577
|
+
@Authenticated()
|
|
578
|
+
@RequireOwnership({ resourceParam: 'id', userProperty: 'authorId' })
|
|
579
|
+
@Endpoint(updatePostEndpoint)
|
|
580
|
+
async updatePost({
|
|
581
|
+
params,
|
|
582
|
+
body,
|
|
583
|
+
}: {
|
|
584
|
+
params: { id: string }
|
|
585
|
+
body: UpdatePostDto
|
|
586
|
+
}) {
|
|
587
|
+
// User must be authenticated and own the post
|
|
588
|
+
return this.postService.update(params.id, body)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Dynamic Attributes
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
const ConditionalCacheSchema = z.object({
|
|
597
|
+
condition: z.function().args(z.any()).returns(z.boolean()),
|
|
598
|
+
ttl: z.number(),
|
|
599
|
+
key: z.string(),
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
export const ConditionalCache = AttributeFactory.createAttribute(
|
|
603
|
+
Symbol('ConditionalCache'),
|
|
604
|
+
ConditionalCacheSchema,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
@Controller()
|
|
608
|
+
export class UserController {
|
|
609
|
+
@ConditionalCache({
|
|
610
|
+
condition: (request) => request.user?.isPremium === true,
|
|
611
|
+
ttl: 3600,
|
|
612
|
+
key: 'premium-user-{id}',
|
|
613
|
+
})
|
|
614
|
+
@Endpoint(getPremiumDataEndpoint)
|
|
615
|
+
async getPremiumData({ params }: { params: { id: string } }) {
|
|
616
|
+
// Only cache for premium users
|
|
617
|
+
return this.userService.getPremiumData(params.id)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
## Best Practices
|
|
623
|
+
|
|
624
|
+
### 1. Use Descriptive Names
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
// ✅ Good - Clear intent
|
|
628
|
+
export const RequireAdminRole = AttributeFactory.createAttribute(
|
|
629
|
+
Symbol('RequireAdminRole'),
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
// ❌ Avoid - Unclear purpose
|
|
633
|
+
export const Admin = AttributeFactory.createAttribute(Symbol('Admin'))
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### 2. Validate Attribute Data
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
// ✅ Good - Use schemas for validation
|
|
640
|
+
const RateLimitSchema = z.object({
|
|
641
|
+
requests: z.number().min(1),
|
|
642
|
+
windowMs: z.number().min(1000),
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
export const RateLimit = AttributeFactory.createAttribute(
|
|
646
|
+
Symbol('RateLimit'),
|
|
647
|
+
RateLimitSchema,
|
|
648
|
+
)
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### 3. Document Attribute Behavior
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
/**
|
|
655
|
+
* Caches the endpoint response for the specified duration.
|
|
656
|
+
*
|
|
657
|
+
* @param ttl - Time to live in seconds
|
|
658
|
+
* @param key - Cache key template (supports {param} placeholders)
|
|
659
|
+
* @param tags - Cache tags for invalidation
|
|
660
|
+
*/
|
|
661
|
+
export const Cache = AttributeFactory.createAttribute(
|
|
662
|
+
Symbol('Cache'),
|
|
663
|
+
CacheSchema,
|
|
664
|
+
)
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### 4. Keep Attributes Focused
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
// ✅ Good - Single responsibility
|
|
671
|
+
export const RateLimit = AttributeFactory.createAttribute(
|
|
672
|
+
Symbol('RateLimit'),
|
|
673
|
+
RateLimitSchema,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
export const Cache = AttributeFactory.createAttribute(
|
|
677
|
+
Symbol('Cache'),
|
|
678
|
+
CacheSchema,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
// ❌ Avoid - Multiple responsibilities
|
|
682
|
+
export const RateLimitAndCache = AttributeFactory.createAttribute(
|
|
683
|
+
Symbol('RateLimitAndCache'),
|
|
684
|
+
z.object({
|
|
685
|
+
rateLimit: RateLimitSchema,
|
|
686
|
+
cache: CacheSchema,
|
|
687
|
+
}),
|
|
688
|
+
)
|
|
689
|
+
```
|