@navios/core 0.4.0 → 0.5.1

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 (92) hide show
  1. package/README.md +95 -2
  2. package/docs/README.md +310 -3
  3. package/docs/adapters.md +308 -0
  4. package/docs/application-setup.md +524 -0
  5. package/docs/attributes.md +689 -0
  6. package/docs/controllers.md +373 -0
  7. package/docs/endpoints.md +444 -0
  8. package/docs/exceptions.md +316 -0
  9. package/docs/guards.md +550 -0
  10. package/docs/modules.md +377 -0
  11. package/docs/quick-start.md +295 -0
  12. package/docs/services.md +427 -0
  13. package/docs/testing.md +704 -0
  14. package/lib/_tsup-dts-rollup.d.mts +310 -239
  15. package/lib/_tsup-dts-rollup.d.ts +310 -239
  16. package/lib/index.d.mts +51 -28
  17. package/lib/index.d.ts +51 -28
  18. package/lib/index.js +633 -1072
  19. package/lib/index.js.map +1 -1
  20. package/lib/index.mjs +631 -1064
  21. package/lib/index.mjs.map +1 -1
  22. package/package.json +5 -9
  23. package/project.json +9 -1
  24. package/src/__tests__/config.service.spec.mts +11 -9
  25. package/src/__tests__/controller.spec.mts +0 -1
  26. package/src/config/config.service.mts +2 -2
  27. package/src/decorators/controller.decorator.mts +1 -1
  28. package/src/decorators/endpoint.decorator.mts +2 -2
  29. package/src/decorators/header.decorator.mts +1 -1
  30. package/src/decorators/multipart.decorator.mts +1 -2
  31. package/src/decorators/stream.decorator.mts +2 -3
  32. package/src/factories/endpoint-adapter.factory.mts +21 -0
  33. package/src/factories/http-adapter.factory.mts +20 -0
  34. package/src/factories/index.mts +6 -0
  35. package/src/factories/multipart-adapter.factory.mts +21 -0
  36. package/src/factories/reply.factory.mts +21 -0
  37. package/src/factories/request.factory.mts +21 -0
  38. package/src/factories/stream-adapter.factory.mts +20 -0
  39. package/src/index.mts +1 -1
  40. package/src/interfaces/abstract-execution-context.inteface.mts +13 -0
  41. package/src/interfaces/abstract-http-adapter.interface.mts +20 -0
  42. package/src/interfaces/abstract-http-cors-options.interface.mts +59 -0
  43. package/src/interfaces/abstract-http-handler-adapter.interface.mts +13 -0
  44. package/src/interfaces/abstract-http-listen-options.interface.mts +4 -0
  45. package/src/interfaces/can-activate.mts +4 -2
  46. package/src/interfaces/http-header.mts +18 -0
  47. package/src/interfaces/index.mts +6 -0
  48. package/src/logger/console-logger.service.mts +28 -44
  49. package/src/logger/index.mts +1 -2
  50. package/src/logger/logger.service.mts +9 -128
  51. package/src/logger/logger.tokens.mts +21 -0
  52. package/src/metadata/handler.metadata.mts +7 -5
  53. package/src/navios.application.mts +65 -172
  54. package/src/navios.environment.mts +30 -0
  55. package/src/navios.factory.mts +53 -12
  56. package/src/services/guard-runner.service.mts +19 -9
  57. package/src/services/index.mts +0 -2
  58. package/src/services/module-loader.service.mts +4 -3
  59. package/src/tokens/endpoint-adapter.token.mts +8 -0
  60. package/src/tokens/execution-context.token.mts +2 -2
  61. package/src/tokens/http-adapter.token.mts +8 -0
  62. package/src/tokens/index.mts +4 -1
  63. package/src/tokens/multipart-adapter.token.mts +8 -0
  64. package/src/tokens/reply.token.mts +1 -5
  65. package/src/tokens/request.token.mts +1 -7
  66. package/src/tokens/stream-adapter.token.mts +8 -0
  67. package/docs/recipes/prisma.md +0 -60
  68. package/e2e/endpoints/get.spec.mts +0 -97
  69. package/e2e/endpoints/post.spec.mts +0 -113
  70. package/examples/simple-test/api/index.mts +0 -64
  71. package/examples/simple-test/config/config.service.mts +0 -14
  72. package/examples/simple-test/config/configuration.mts +0 -7
  73. package/examples/simple-test/index.mts +0 -16
  74. package/examples/simple-test/src/acl/acl-modern.guard.mts +0 -15
  75. package/examples/simple-test/src/acl/acl.guard.mts +0 -14
  76. package/examples/simple-test/src/acl/app.guard.mts +0 -27
  77. package/examples/simple-test/src/acl/one-more.guard.mts +0 -15
  78. package/examples/simple-test/src/acl/public.attribute.mts +0 -21
  79. package/examples/simple-test/src/app.module.mts +0 -9
  80. package/examples/simple-test/src/user/user.controller.mts +0 -72
  81. package/examples/simple-test/src/user/user.module.mts +0 -14
  82. package/examples/simple-test/src/user/user.service.mts +0 -14
  83. package/src/adapters/endpoint-adapter.service.mts +0 -72
  84. package/src/adapters/handler-adapter.interface.mts +0 -21
  85. package/src/adapters/index.mts +0 -4
  86. package/src/adapters/multipart-adapter.service.mts +0 -135
  87. package/src/adapters/stream-adapter.service.mts +0 -91
  88. package/src/logger/logger.factory.mts +0 -36
  89. package/src/logger/pino-wrapper.mts +0 -64
  90. package/src/services/controller-adapter.service.mts +0 -124
  91. package/src/services/execution-context.mts +0 -54
  92. 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
+ ```