@navios/core 0.8.0 → 0.9.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 (64) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/lib/{index-BDNl7j1G.d.cts → index-B2ulzZIr.d.cts} +65 -13
  3. package/lib/index-B2ulzZIr.d.cts.map +1 -0
  4. package/lib/{index-BoP0cWT6.d.mts → index-C8lUQePd.d.mts} +65 -13
  5. package/lib/index-C8lUQePd.d.mts.map +1 -0
  6. package/lib/index.cjs +5 -2
  7. package/lib/index.d.cts +2 -2
  8. package/lib/index.d.mts +2 -2
  9. package/lib/index.mjs +3 -3
  10. package/lib/legacy-compat/index.cjs +133 -1
  11. package/lib/legacy-compat/index.cjs.map +1 -1
  12. package/lib/legacy-compat/index.d.cts +219 -7
  13. package/lib/legacy-compat/index.d.cts.map +1 -1
  14. package/lib/legacy-compat/index.d.mts +219 -7
  15. package/lib/legacy-compat/index.d.mts.map +1 -1
  16. package/lib/legacy-compat/index.mjs +128 -2
  17. package/lib/legacy-compat/index.mjs.map +1 -1
  18. package/lib/{src-gBAChVRL.mjs → src-Baabu2R8.mjs} +17 -12
  19. package/lib/src-Baabu2R8.mjs.map +1 -0
  20. package/lib/{src-B6eISODM.cjs → src-Cu9OAnfp.cjs} +16 -11
  21. package/lib/src-Cu9OAnfp.cjs.map +1 -0
  22. package/lib/testing/index.cjs +346 -29
  23. package/lib/testing/index.cjs.map +1 -1
  24. package/lib/testing/index.d.cts +299 -63
  25. package/lib/testing/index.d.cts.map +1 -1
  26. package/lib/testing/index.d.mts +299 -63
  27. package/lib/testing/index.d.mts.map +1 -1
  28. package/lib/testing/index.mjs +347 -31
  29. package/lib/testing/index.mjs.map +1 -1
  30. package/lib/{use-guards.decorator-CUww54Nt.mjs → use-guards.decorator-ChJVzYLW.mjs} +38 -9
  31. package/lib/use-guards.decorator-ChJVzYLW.mjs.map +1 -0
  32. package/lib/{use-guards.decorator-COR-9mZY.cjs → use-guards.decorator-EvI2m2DK.cjs} +56 -9
  33. package/lib/use-guards.decorator-EvI2m2DK.cjs.map +1 -0
  34. package/package.json +4 -4
  35. package/src/__tests__/controller-resolver.spec.mts +19 -13
  36. package/src/__tests__/testing-module.spec.mts +459 -0
  37. package/src/__tests__/unit-testing-module.spec.mts +424 -0
  38. package/src/attribute.factory.mts +19 -3
  39. package/src/decorators/controller.decorator.mts +19 -2
  40. package/src/decorators/module.decorator.mts +23 -5
  41. package/src/legacy-compat/__type-tests__/legacy-decorators.spec-d.mts +114 -10
  42. package/src/legacy-compat/attribute.factory.mts +365 -0
  43. package/src/legacy-compat/context-compat.mts +2 -0
  44. package/src/legacy-compat/decorators/index.mts +1 -0
  45. package/src/legacy-compat/decorators/injectable.decorator.mts +41 -0
  46. package/src/legacy-compat/decorators/multipart.decorator.mts +4 -4
  47. package/src/legacy-compat/decorators/stream.decorator.mts +21 -14
  48. package/src/legacy-compat/index.mts +14 -3
  49. package/src/metadata/index.mts +1 -0
  50. package/src/metadata/navios-managed.metadata.mts +42 -0
  51. package/src/navios.application.mts +9 -9
  52. package/src/navios.environment.mts +3 -1
  53. package/src/navios.factory.mts +9 -27
  54. package/src/services/instance-resolver.service.mts +8 -7
  55. package/src/services/module-loader.service.mts +3 -2
  56. package/src/testing/index.mts +1 -0
  57. package/src/testing/testing-module.mts +255 -93
  58. package/src/testing/unit-testing-module.mts +298 -0
  59. package/lib/index-BDNl7j1G.d.cts.map +0 -1
  60. package/lib/index-BoP0cWT6.d.mts.map +0 -1
  61. package/lib/src-B6eISODM.cjs.map +0 -1
  62. package/lib/src-gBAChVRL.mjs.map +0 -1
  63. package/lib/use-guards.decorator-COR-9mZY.cjs.map +0 -1
  64. package/lib/use-guards.decorator-CUww54Nt.mjs.map +0 -1
@@ -13,10 +13,12 @@ import type {
13
13
  } from '../index.mjs'
14
14
 
15
15
  import {
16
+ AttributeFactory,
16
17
  Controller,
17
18
  Endpoint,
18
19
  Header,
19
20
  HttpCode,
21
+ Injectable,
20
22
  Module,
21
23
  Multipart,
22
24
  Stream,
@@ -138,9 +140,7 @@ describe('Legacy Decorators Type Safety', () => {
138
140
  // - request.urlParams.userId: string | number
139
141
  // - request.params.page: number
140
142
  // - request.params.limit: number
141
- expectTypeOf(request.urlParams.userId).toEqualTypeOf<
142
- string | number
143
- >()
143
+ expectTypeOf(request.urlParams.userId).toEqualTypeOf<string>()
144
144
  expectTypeOf(request.params.page).toEqualTypeOf<number>()
145
145
  expectTypeOf(request.params.limit).toEqualTypeOf<number>()
146
146
  return {
@@ -261,18 +261,16 @@ describe('Legacy Decorators Type Safety', () => {
261
261
  test('should enforce correct parameter type', () => {
262
262
  @Controller()
263
263
  class FileController {
264
+ // @ts-expect-error - legacy decorator is not typed
264
265
  @Stream(downloadFileEndpoint)
265
266
  async downloadFile(
266
267
  request: StreamParams<typeof downloadFileEndpoint>,
267
- reply: any,
268
268
  ): Promise<void> {
269
269
  // TypeScript should infer:
270
270
  // - request.urlParams.fileId: string | number
271
271
  // - request.params.page: number
272
272
  // - request.params.limit: number
273
- expectTypeOf(request.urlParams.fileId).toEqualTypeOf<
274
- string | number
275
- >()
273
+ expectTypeOf(request.urlParams.fileId).toEqualTypeOf<string>()
276
274
  expectTypeOf(request.params.page).toEqualTypeOf<number>()
277
275
  expectTypeOf(request.params.limit).toEqualTypeOf<number>()
278
276
  }
@@ -281,17 +279,20 @@ describe('Legacy Decorators Type Safety', () => {
281
279
  expectTypeOf(FileController).toBeConstructibleWith()
282
280
  })
283
281
 
284
- test('should require reply parameter', () => {
282
+ test('should work without reply parameter (Bun compatibility)', () => {
285
283
  @Controller()
286
284
  class FileController {
287
- // @ts-expect-error - missing reply parameter
285
+ // @ts-expect-error - legacy decorator is not typed
288
286
  @Stream(downloadFileEndpoint)
289
287
  async downloadFile(
290
288
  request: StreamParams<typeof downloadFileEndpoint>,
291
289
  ): Promise<void> {
292
- // Stream methods must have reply parameter
290
+ // Stream methods can work without reply parameter (for Bun)
291
+ expectTypeOf(request.urlParams.fileId).toEqualTypeOf<string>()
293
292
  }
294
293
  }
294
+
295
+ expectTypeOf(FileController).toBeConstructibleWith()
295
296
  })
296
297
  })
297
298
 
@@ -376,6 +377,109 @@ describe('Legacy Decorators Type Safety', () => {
376
377
  })
377
378
  })
378
379
 
380
+ describe('Injectable decorator', () => {
381
+ test('should work with services', () => {
382
+ @Injectable()
383
+ class UserService {
384
+ getUser(id: string) {
385
+ return { id, name: 'John' }
386
+ }
387
+ }
388
+
389
+ expectTypeOf(UserService).toBeConstructibleWith()
390
+ })
391
+
392
+ test('should work with controller dependencies', () => {
393
+ @Injectable()
394
+ class UserService {
395
+ getUser(id: string) {
396
+ return { id, name: 'John' }
397
+ }
398
+ }
399
+
400
+ @Controller()
401
+ class UserController {
402
+ constructor(private readonly userService: UserService) {}
403
+
404
+ @Endpoint(getUserEndpoint)
405
+ async getUser(
406
+ request: EndpointParams<typeof getUserEndpoint>,
407
+ ): EndpointResult<typeof getUserEndpoint> {
408
+ return this.userService.getUser(request.urlParams.userId.toString())
409
+ }
410
+ }
411
+
412
+ expectTypeOf(UserController).toBeConstructibleWith(
413
+ {} as InstanceType<typeof UserService>,
414
+ )
415
+ })
416
+ })
417
+
418
+ describe('AttributeFactory', () => {
419
+ test('should create simple attribute for controllers', () => {
420
+ const Public = AttributeFactory.createAttribute(Symbol.for('Public'))
421
+
422
+ // Note: In legacy decorators, decorators are applied bottom-to-top
423
+ // So @Controller() must be at the bottom to run first
424
+ @Public()
425
+ @Controller()
426
+ class PublicController {
427
+ @Endpoint(getUserEndpoint)
428
+ async getUser(
429
+ request: EndpointParams<typeof getUserEndpoint>,
430
+ ): EndpointResult<typeof getUserEndpoint> {
431
+ return { id: '1', name: 'John' }
432
+ }
433
+ }
434
+
435
+ expectTypeOf(PublicController).toBeConstructibleWith()
436
+ expectTypeOf(Public.token).toEqualTypeOf<symbol>()
437
+ })
438
+
439
+ test('should create attribute with schema for endpoints', () => {
440
+ const RateLimit = AttributeFactory.createAttribute(
441
+ Symbol.for('RateLimit'),
442
+ z.object({ requests: z.number(), window: z.number() }),
443
+ )
444
+
445
+ @Controller()
446
+ class RateLimitedController {
447
+ // Note: For method decorators, @Endpoint must be at the bottom
448
+ // @ts-expect-error - legacy decorator is not typed correctly
449
+ @RateLimit({ requests: 100, window: 60000 })
450
+ @Endpoint(getUserEndpoint)
451
+ async getUser(
452
+ request: EndpointParams<typeof getUserEndpoint>,
453
+ ): EndpointResult<typeof getUserEndpoint> {
454
+ return { id: '1', name: 'John' }
455
+ }
456
+ }
457
+
458
+ expectTypeOf(RateLimitedController).toBeConstructibleWith()
459
+ expectTypeOf(RateLimit.token).toEqualTypeOf<symbol>()
460
+ expectTypeOf(RateLimit.schema).toBeObject()
461
+ })
462
+
463
+ test('should create attribute for modules', () => {
464
+ const ApiVersion = AttributeFactory.createAttribute(
465
+ Symbol.for('ApiVersion'),
466
+ z.string(),
467
+ )
468
+
469
+ @Controller()
470
+ class VersionedController {}
471
+
472
+ // Note: In legacy decorators, @Module must be at the bottom
473
+ @ApiVersion('v2')
474
+ @Module({
475
+ controllers: [VersionedController],
476
+ })
477
+ class VersionedModule {}
478
+
479
+ expectTypeOf(VersionedModule).toBeConstructibleWith()
480
+ })
481
+ })
482
+
379
483
  describe('Integration test - full controller', () => {
380
484
  test('should work with all decorators together', () => {
381
485
  @Controller({
@@ -0,0 +1,365 @@
1
+ import type { ClassType } from '@navios/di'
2
+ import type { z, ZodType } from 'zod/v4'
3
+
4
+ import type {
5
+ ControllerMetadata,
6
+ HandlerMetadata,
7
+ ModuleMetadata,
8
+ } from '../metadata/index.mjs'
9
+
10
+ import {
11
+ getControllerMetadata,
12
+ getEndpointMetadata,
13
+ getModuleMetadata,
14
+ hasControllerMetadata,
15
+ hasModuleMetadata,
16
+ } from '../metadata/index.mjs'
17
+ import {
18
+ getManagedMetadata,
19
+ hasManagedMetadata,
20
+ } from '../metadata/navios-managed.metadata.mjs'
21
+ import { createClassContext, createMethodContext } from './context-compat.mjs'
22
+
23
+ /**
24
+ * Type for a legacy class attribute decorator without a value.
25
+ *
26
+ * Attributes are custom metadata decorators that can be applied to modules,
27
+ * controllers, and endpoints.
28
+ */
29
+ export type LegacyClassAttribute = (() => <T extends ClassType>(
30
+ target: T,
31
+ ) => T) &
32
+ (() => <T extends object>(
33
+ target: T,
34
+ propertyKey: string | symbol,
35
+ descriptor: PropertyDescriptor,
36
+ ) => PropertyDescriptor) & {
37
+ token: symbol
38
+ }
39
+
40
+ /**
41
+ * Type for a legacy class attribute decorator with a validated value.
42
+ *
43
+ * @typeParam S - The Zod schema type for validation
44
+ */
45
+ export type LegacyClassSchemaAttribute<S extends ZodType> = ((
46
+ value: z.input<S>,
47
+ ) => <T extends ClassType>(target: T) => T) &
48
+ ((
49
+ value: z.input<S>,
50
+ ) => <T extends object>(
51
+ target: T,
52
+ propertyKey: string | symbol,
53
+ descriptor: PropertyDescriptor,
54
+ ) => PropertyDescriptor) & {
55
+ token: symbol
56
+ schema: ZodType
57
+ }
58
+
59
+ /**
60
+ * Legacy-compatible factory for creating custom attribute decorators.
61
+ *
62
+ * Works with TypeScript experimental decorators (legacy API).
63
+ *
64
+ * Attributes allow you to add custom metadata to modules, controllers, and endpoints.
65
+ * This is useful for cross-cutting concerns like rate limiting, caching, API versioning, etc.
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * // Create a simple boolean attribute
70
+ * const Public = LegacyAttributeFactory.createAttribute(Symbol.for('Public'))
71
+ *
72
+ * // Use it as a decorator
73
+ * @Controller()
74
+ * @Public()
75
+ * export class PublicController { }
76
+ *
77
+ * // Check if attribute exists
78
+ * if (LegacyAttributeFactory.has(Public, controllerMetadata)) {
79
+ * // Skip authentication
80
+ * }
81
+ * ```
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * // Create an attribute with a validated value
86
+ * const RateLimit = LegacyAttributeFactory.createAttribute(
87
+ * Symbol.for('RateLimit'),
88
+ * z.object({ requests: z.number(), window: z.number() })
89
+ * )
90
+ *
91
+ * // Use it with a value
92
+ * @Endpoint(apiEndpoint)
93
+ * @RateLimit({ requests: 100, window: 60000 })
94
+ * async handler() { }
95
+ *
96
+ * // Get the value
97
+ * const limit = LegacyAttributeFactory.get(RateLimit, endpointMetadata)
98
+ * // limit is typed as { requests: number, window: number } | null
99
+ * ```
100
+ */
101
+ export class LegacyAttributeFactory {
102
+ /**
103
+ * Creates a simple attribute decorator without a value.
104
+ *
105
+ * @param token - A unique symbol to identify this attribute
106
+ * @returns A decorator function that can be applied to classes or methods
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * const Public = LegacyAttributeFactory.createAttribute(Symbol.for('Public'))
111
+ *
112
+ * @Public()
113
+ * @Controller()
114
+ * export class PublicController { }
115
+ * ```
116
+ */
117
+ static createAttribute(token: symbol): LegacyClassAttribute
118
+ /**
119
+ * Creates an attribute decorator with a validated value.
120
+ *
121
+ * @param token - A unique symbol to identify this attribute
122
+ * @param schema - A Zod schema to validate the attribute value
123
+ * @returns A decorator function that accepts a value and can be applied to classes or methods
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * const RateLimit = LegacyAttributeFactory.createAttribute(
128
+ * Symbol.for('RateLimit'),
129
+ * z.object({ requests: z.number(), window: z.number() })
130
+ * )
131
+ *
132
+ * @RateLimit({ requests: 100, window: 60000 })
133
+ * @Endpoint(apiEndpoint)
134
+ * async handler() { }
135
+ * ```
136
+ */
137
+ static createAttribute<T extends ZodType>(
138
+ token: symbol,
139
+ schema: T,
140
+ ): LegacyClassSchemaAttribute<T>
141
+
142
+ static createAttribute(token: symbol, schema?: ZodType) {
143
+ const res = (value?: unknown) => {
144
+ // Return a decorator that can handle both class and method targets
145
+ function decorator(target: any): any
146
+ function decorator(
147
+ target: any,
148
+ propertyKey: string | symbol,
149
+ descriptor: PropertyDescriptor,
150
+ ): PropertyDescriptor
151
+ function decorator(
152
+ target: any,
153
+ propertyKey?: string | symbol,
154
+ descriptor?: PropertyDescriptor,
155
+ ): any {
156
+ const isMethodDecorator =
157
+ propertyKey !== undefined && descriptor !== undefined
158
+
159
+ if (isMethodDecorator) {
160
+ // Method decorator - apply to endpoint
161
+ const context = createMethodContext(target, propertyKey, descriptor)
162
+ const metadata = getEndpointMetadata(descriptor.value, context)
163
+
164
+ if (schema) {
165
+ const validatedValue = schema.safeParse(value)
166
+ if (!validatedValue.success) {
167
+ throw new Error(
168
+ `[Navios] Invalid value for attribute ${token.toString()}: ${validatedValue.error}`,
169
+ )
170
+ }
171
+ metadata.customAttributes.set(token, validatedValue.data)
172
+ } else {
173
+ metadata.customAttributes.set(token, true)
174
+ }
175
+ return descriptor
176
+ } else {
177
+ // Class decorator
178
+ const isController = hasControllerMetadata(target as ClassType)
179
+ const isModule = hasModuleMetadata(target as ClassType)
180
+ const isManaged = hasManagedMetadata(target as ClassType)
181
+
182
+ if (!isController && !isModule && !isManaged) {
183
+ throw new Error(
184
+ '[Navios] Attribute can only be applied to classes with @Controller, @Module, or other Navios-managed decorators',
185
+ )
186
+ }
187
+
188
+ const context = createClassContext(target)
189
+ const metadata = isController
190
+ ? getControllerMetadata(target, context)
191
+ : isModule
192
+ ? getModuleMetadata(target, context)
193
+ : isManaged
194
+ ? getManagedMetadata(target)!
195
+ : null
196
+
197
+ if (!metadata) {
198
+ throw new Error(
199
+ '[Navios] Could not determine metadata for attribute target',
200
+ )
201
+ }
202
+
203
+ if (schema) {
204
+ const validatedValue = schema.safeParse(value)
205
+ if (!validatedValue.success) {
206
+ throw new Error(
207
+ `[Navios] Invalid value for attribute ${token.toString()}: ${validatedValue.error}`,
208
+ )
209
+ }
210
+ metadata.customAttributes.set(token, validatedValue.data)
211
+ } else {
212
+ metadata.customAttributes.set(token, true)
213
+ }
214
+ return target
215
+ }
216
+ }
217
+ return decorator
218
+ }
219
+ res.token = token
220
+ if (schema) {
221
+ res.schema = schema
222
+ }
223
+ return res
224
+ }
225
+
226
+ /**
227
+ * Gets the value of an attribute from metadata.
228
+ *
229
+ * Returns `null` if the attribute is not present.
230
+ * For simple attributes (without values), returns `true` if present.
231
+ *
232
+ * @param attribute - The attribute decorator
233
+ * @param target - The metadata object (module, controller, or handler)
234
+ * @returns The attribute value, `true` for simple attributes, or `null` if not found
235
+ *
236
+ * @example
237
+ * ```typescript
238
+ * const isPublic = LegacyAttributeFactory.get(Public, controllerMetadata)
239
+ * // isPublic is true | null
240
+ *
241
+ * const rateLimit = LegacyAttributeFactory.get(RateLimit, endpointMetadata)
242
+ * // rateLimit is { requests: number, window: number } | null
243
+ * ```
244
+ */
245
+ static get(
246
+ attribute: LegacyClassAttribute,
247
+ target: ModuleMetadata | ControllerMetadata | HandlerMetadata<any>,
248
+ ): true | null
249
+ static get<T extends ZodType>(
250
+ attribute: LegacyClassSchemaAttribute<T>,
251
+ target: ModuleMetadata | ControllerMetadata | HandlerMetadata<any>,
252
+ ): z.output<T> | null
253
+ static get(
254
+ attribute: LegacyClassAttribute | LegacyClassSchemaAttribute<any>,
255
+ target: ModuleMetadata | ControllerMetadata | HandlerMetadata<any>,
256
+ ) {
257
+ return target.customAttributes.get(attribute.token) ?? null
258
+ }
259
+
260
+ /**
261
+ * Gets all values of an attribute from metadata (useful when an attribute can appear multiple times).
262
+ *
263
+ * Returns `null` if the attribute is not present.
264
+ *
265
+ * @param attribute - The attribute decorator
266
+ * @param target - The metadata object (module, controller, or handler)
267
+ * @returns An array of attribute values, or `null` if not found
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * const tags = LegacyAttributeFactory.getAll(Tag, endpointMetadata)
272
+ * // tags is string[] | null
273
+ * ```
274
+ */
275
+ static getAll(
276
+ attribute: LegacyClassAttribute,
277
+ target: ModuleMetadata | ControllerMetadata | HandlerMetadata<any>,
278
+ ): Array<true> | null
279
+ static getAll<T extends ZodType>(
280
+ attribute: LegacyClassSchemaAttribute<T>,
281
+ target: ModuleMetadata | ControllerMetadata | HandlerMetadata<any>,
282
+ ): Array<z.output<T>> | null
283
+ static getAll(
284
+ attribute: LegacyClassAttribute | LegacyClassSchemaAttribute<any>,
285
+ target: ModuleMetadata | ControllerMetadata | HandlerMetadata<any>,
286
+ ) {
287
+ const values = Array.from(target.customAttributes.entries())
288
+ .filter(([key]) => key === attribute.token)
289
+ .map(([, value]) => value)
290
+ return values.length > 0 ? values : null
291
+ }
292
+
293
+ /**
294
+ * Gets the last value of an attribute from an array of metadata objects.
295
+ *
296
+ * Searches from the end of the array backwards, useful for finding the most
297
+ * specific attribute value (e.g., endpoint-level overrides module-level).
298
+ *
299
+ * @param attribute - The attribute decorator
300
+ * @param target - An array of metadata objects (typically [module, controller, handler])
301
+ * @returns The last attribute value found, or `null` if not found
302
+ *
303
+ * @example
304
+ * ```typescript
305
+ * // Check attribute hierarchy: endpoint -> controller -> module
306
+ * const rateLimit = LegacyAttributeFactory.getLast(RateLimit, [
307
+ * moduleMetadata,
308
+ * controllerMetadata,
309
+ * endpointMetadata
310
+ * ])
311
+ * ```
312
+ */
313
+ static getLast(
314
+ attribute: LegacyClassAttribute,
315
+ target: (ModuleMetadata | ControllerMetadata | HandlerMetadata<any>)[],
316
+ ): true | null
317
+ static getLast<T extends ZodType>(
318
+ attribute: LegacyClassSchemaAttribute<T>,
319
+ target: (ModuleMetadata | ControllerMetadata | HandlerMetadata<any>)[],
320
+ ): z.output<T> | null
321
+ static getLast(
322
+ attribute: LegacyClassAttribute | LegacyClassSchemaAttribute<any>,
323
+ target: (ModuleMetadata | ControllerMetadata | HandlerMetadata<any>)[],
324
+ ) {
325
+ for (let i = target.length - 1; i >= 0; i--) {
326
+ const value = target[i].customAttributes.get(attribute.token)
327
+ if (value) {
328
+ return value
329
+ }
330
+ }
331
+ return null
332
+ }
333
+
334
+ /**
335
+ * Checks if an attribute is present on the metadata object.
336
+ *
337
+ * @param attribute - The attribute decorator
338
+ * @param target - The metadata object (module, controller, or handler)
339
+ * @returns `true` if the attribute is present, `false` otherwise
340
+ *
341
+ * @example
342
+ * ```typescript
343
+ * if (LegacyAttributeFactory.has(Public, controllerMetadata)) {
344
+ * // Skip authentication
345
+ * }
346
+ * ```
347
+ */
348
+ static has(
349
+ attribute: LegacyClassAttribute,
350
+ target: ModuleMetadata | ControllerMetadata | HandlerMetadata<any>,
351
+ ): boolean
352
+ static has<T extends ZodType>(
353
+ attribute: LegacyClassSchemaAttribute<T>,
354
+ target: ModuleMetadata | ControllerMetadata | HandlerMetadata<any>,
355
+ ): boolean
356
+ static has(
357
+ attribute: LegacyClassAttribute | LegacyClassSchemaAttribute<any>,
358
+ target: ModuleMetadata | ControllerMetadata | HandlerMetadata<any>,
359
+ ) {
360
+ return target.customAttributes.has(attribute.token)
361
+ }
362
+ }
363
+
364
+ // Re-export as AttributeFactory for convenience
365
+ export { LegacyAttributeFactory as AttributeFactory }
@@ -30,6 +30,7 @@ function getConstructor(prototype: any): ClassType | null {
30
30
 
31
31
  /**
32
32
  * Creates a mock ClassDecoratorContext for legacy class decorators.
33
+ * @internal
33
34
  */
34
35
  export function createClassContext(target: ClassType): ClassDecoratorContext {
35
36
  // Get or create metadata storage for this class
@@ -53,6 +54,7 @@ export function createClassContext(target: ClassType): ClassDecoratorContext {
53
54
  *
54
55
  * Note: Method decorators need to share metadata with the class context
55
56
  * because endpoint metadata is stored at the class level.
57
+ * @internal
56
58
  */
57
59
  export function createMethodContext(
58
60
  target: any,
@@ -6,4 +6,5 @@ export * from './header.decorator.mjs'
6
6
  export * from './http-code.decorator.mjs'
7
7
  export * from './multipart.decorator.mjs'
8
8
  export * from './stream.decorator.mjs'
9
+ export * from './injectable.decorator.mjs'
9
10
 
@@ -0,0 +1,41 @@
1
+ import type { ClassType } from '@navios/di'
2
+
3
+ import {
4
+ Injectable as OriginalInjectable,
5
+ type InjectableOptions,
6
+ } from '@navios/di'
7
+
8
+ import { createClassContext } from '../context-compat.mjs'
9
+
10
+ export type { InjectableOptions }
11
+
12
+ /**
13
+ * Legacy-compatible Injectable decorator.
14
+ *
15
+ * Works with TypeScript experimental decorators (legacy API).
16
+ *
17
+ * @param options - Injectable configuration options
18
+ * @returns A class decorator compatible with legacy decorator API
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * @Injectable()
23
+ * export class UserService {
24
+ * getUser(id: string) {
25
+ * return { id, name: 'John' }
26
+ * }
27
+ * }
28
+ * ```
29
+ */
30
+ export function Injectable(options: InjectableOptions = {}) {
31
+ return function (target: ClassType) {
32
+ const context = createClassContext(target)
33
+ // Use the no-args overload when options is empty, otherwise pass options
34
+ const originalDecorator =
35
+ Object.keys(options).length === 0
36
+ ? OriginalInjectable()
37
+ : // @ts-expect-error - InjectableOptions is a union type, we let runtime handle it
38
+ OriginalInjectable(options)
39
+ return originalDecorator(target, context)
40
+ }
41
+ }
@@ -22,11 +22,11 @@ type MultipartMethodDescriptor<
22
22
  (
23
23
  params: QuerySchema extends ZodObject
24
24
  ? RequestSchema extends ZodType
25
- ? EndpointFunctionArgs<Url, QuerySchema, RequestSchema>
26
- : EndpointFunctionArgs<Url, QuerySchema, undefined>
25
+ ? EndpointFunctionArgs<Url, QuerySchema, RequestSchema, true>
26
+ : EndpointFunctionArgs<Url, QuerySchema, undefined, true>
27
27
  : RequestSchema extends ZodType
28
- ? EndpointFunctionArgs<Url, undefined, RequestSchema>
29
- : EndpointFunctionArgs<Url, undefined, undefined>,
28
+ ? EndpointFunctionArgs<Url, undefined, RequestSchema, true>
29
+ : EndpointFunctionArgs<Url, undefined, undefined, true>,
30
30
  ) => Promise<z.input<ResponseSchema>>
31
31
  >
32
32
 
@@ -8,27 +8,34 @@ import type { ZodObject, ZodType } from 'zod/v4'
8
8
  import { Stream as OriginalStream } from '../../decorators/stream.decorator.mjs'
9
9
  import { createMethodContext } from '../context-compat.mjs'
10
10
 
11
+ type StreamParams<
12
+ Url extends string,
13
+ QuerySchema,
14
+ RequestSchema,
15
+ > = QuerySchema extends ZodObject
16
+ ? RequestSchema extends ZodType
17
+ ? EndpointFunctionArgs<Url, QuerySchema, RequestSchema, true>
18
+ : EndpointFunctionArgs<Url, QuerySchema, undefined, true>
19
+ : RequestSchema extends ZodType
20
+ ? EndpointFunctionArgs<Url, undefined, RequestSchema, true>
21
+ : EndpointFunctionArgs<Url, undefined, undefined, true>
22
+
11
23
  /**
12
24
  * Type helper to constrain a PropertyDescriptor's value to match a stream endpoint signature.
13
- * Note: In legacy decorators, type constraints are checked when the decorator is applied,
14
- * but may not be preserved perfectly when decorators are stacked.
25
+ * Supports both with and without reply parameter (Bun doesn't use reply parameter).
15
26
  */
16
27
  type StreamMethodDescriptor<
17
28
  Url extends string,
18
29
  QuerySchema,
19
30
  RequestSchema,
20
- > = TypedPropertyDescriptor<
21
- (
22
- params: QuerySchema extends ZodObject
23
- ? RequestSchema extends ZodType
24
- ? EndpointFunctionArgs<Url, QuerySchema, RequestSchema>
25
- : EndpointFunctionArgs<Url, QuerySchema, undefined>
26
- : RequestSchema extends ZodType
27
- ? EndpointFunctionArgs<Url, undefined, RequestSchema>
28
- : EndpointFunctionArgs<Url, undefined, undefined>,
29
- reply: any,
30
- ) => Promise<void>
31
- >
31
+ > =
32
+ | TypedPropertyDescriptor<
33
+ (params: StreamParams<Url, QuerySchema, RequestSchema>, reply: any) => any
34
+ >
35
+ | TypedPropertyDescriptor<
36
+ (params: StreamParams<Url, QuerySchema, RequestSchema>) => any
37
+ >
38
+ | TypedPropertyDescriptor<() => any>
32
39
 
33
40
  /**
34
41
  * Legacy-compatible Stream decorator.