@navios/core 1.0.0-alpha.2 → 1.0.0-alpha.3
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/CHANGELOG.md +36 -0
- package/lib/{index-BJjk2X1S.d.mts → index-BISYCYEG.d.mts} +435 -180
- package/lib/index-BISYCYEG.d.mts.map +1 -0
- package/lib/{index-DZ6NU03y.d.cts → index-CP80H6Dh.d.cts} +435 -180
- package/lib/index-CP80H6Dh.d.cts.map +1 -0
- package/lib/index.cjs +3 -2
- package/lib/index.d.cts +2 -2
- package/lib/index.d.mts +2 -2
- package/lib/index.mjs +2 -2
- package/lib/legacy-compat/index.cjs +38 -124
- package/lib/legacy-compat/index.cjs.map +1 -1
- package/lib/legacy-compat/index.d.cts +4 -60
- package/lib/legacy-compat/index.d.cts.map +1 -1
- package/lib/legacy-compat/index.d.mts +4 -60
- package/lib/legacy-compat/index.d.mts.map +1 -1
- package/lib/legacy-compat/index.mjs +12 -118
- package/lib/legacy-compat/index.mjs.map +1 -1
- package/lib/{src-C46ePe3d.cjs → src-CC5lmk_Q.cjs} +167 -2
- package/lib/src-CC5lmk_Q.cjs.map +1 -0
- package/lib/{src-K2k0riYJ.mjs → src-j1cBuAgy.mjs} +162 -3
- package/lib/src-j1cBuAgy.mjs.map +1 -0
- package/lib/testing/index.cjs +2 -2
- package/lib/testing/index.d.cts +1 -1
- package/lib/testing/index.d.mts +1 -1
- package/lib/testing/index.mjs +1 -1
- package/lib/{use-guards.decorator-B6tghdxM.cjs → use-guards.decorator-DtCGXcWZ.cjs} +2 -2
- package/lib/{use-guards.decorator-B6tghdxM.cjs.map → use-guards.decorator-DtCGXcWZ.cjs.map} +1 -1
- package/package.json +2 -2
- package/src/__tests__/attribute.factory.spec.mts +300 -0
- package/src/__tests__/guard-runner.service.spec.mts +399 -0
- package/src/__tests__/logger.service.spec.mts +147 -0
- package/src/__tests__/responders.spec.mts +6 -5
- package/src/interfaces/abstract-http-handler-adapter.interface.mts +86 -2
- package/src/legacy-compat/attribute.factory.mts +2 -2
- package/src/legacy-compat/decorators/controller.decorator.mts +1 -1
- package/src/legacy-compat/decorators/endpoint.decorator.mts +1 -1
- package/src/legacy-compat/decorators/header.decorator.mts +2 -1
- package/src/legacy-compat/decorators/http-code.decorator.mts +2 -1
- package/src/legacy-compat/decorators/index.mts +2 -2
- package/src/legacy-compat/decorators/module.decorator.mts +1 -1
- package/src/legacy-compat/decorators/multipart.decorator.mts +1 -1
- package/src/legacy-compat/decorators/stream.decorator.mts +1 -1
- package/src/legacy-compat/decorators/use-guards.decorator.mts +1 -1
- package/src/legacy-compat/index.mts +10 -5
- package/src/services/abstract-handler-adapter.service.mts +366 -0
- package/src/services/index.mts +1 -0
- package/lib/index-BJjk2X1S.d.mts.map +0 -1
- package/lib/index-DZ6NU03y.d.cts.map +0 -1
- package/lib/src-C46ePe3d.cjs.map +0 -1
- package/lib/src-K2k0riYJ.mjs.map +0 -1
- package/src/legacy-compat/context-compat.mts +0 -95
- package/src/legacy-compat/decorators/factory.decorator.mts +0 -37
- package/src/legacy-compat/decorators/injectable.decorator.mts +0 -41
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import type { ScopedContainer } from '@navios/di'
|
|
2
|
+
|
|
3
|
+
import { Container, Injectable, InjectionToken } from '@navios/di'
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
AbstractExecutionContext,
|
|
9
|
+
CanActivate,
|
|
10
|
+
} from '../interfaces/index.mjs'
|
|
11
|
+
import type {
|
|
12
|
+
ControllerMetadata,
|
|
13
|
+
HandlerMetadata,
|
|
14
|
+
ModuleMetadata,
|
|
15
|
+
} from '../metadata/index.mjs'
|
|
16
|
+
|
|
17
|
+
import { HttpException } from '../exceptions/index.mjs'
|
|
18
|
+
import { LoggerOutput } from '../logger/logger.tokens.mjs'
|
|
19
|
+
import {
|
|
20
|
+
ForbiddenResponderToken,
|
|
21
|
+
InternalServerErrorResponderToken,
|
|
22
|
+
NotFoundResponderToken,
|
|
23
|
+
ValidationErrorResponderToken,
|
|
24
|
+
} from '../responders/tokens/responder.tokens.mjs'
|
|
25
|
+
import { GuardRunnerService } from '../services/guard-runner.service.mjs'
|
|
26
|
+
|
|
27
|
+
// Mock responders
|
|
28
|
+
const createMockResponder = (statusCode: number, message: string) => ({
|
|
29
|
+
getResponse: vi.fn().mockReturnValue({
|
|
30
|
+
statusCode,
|
|
31
|
+
payload: { message },
|
|
32
|
+
headers: { 'content-type': 'application/problem+json' },
|
|
33
|
+
}),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Mock logger output
|
|
37
|
+
const mockLoggerOutput = {
|
|
38
|
+
log: vi.fn(),
|
|
39
|
+
error: vi.fn(),
|
|
40
|
+
warn: vi.fn(),
|
|
41
|
+
debug: vi.fn(),
|
|
42
|
+
verbose: vi.fn(),
|
|
43
|
+
fatal: vi.fn(),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Mock execution context
|
|
47
|
+
const createMockExecutionContext = (): AbstractExecutionContext => {
|
|
48
|
+
const mockReply = {
|
|
49
|
+
status: vi.fn().mockReturnThis(),
|
|
50
|
+
send: vi.fn().mockReturnThis(),
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
getRequest: vi.fn().mockReturnValue({}),
|
|
54
|
+
getReply: vi.fn().mockReturnValue(mockReply),
|
|
55
|
+
getHandler: vi.fn(),
|
|
56
|
+
getModule: vi.fn(),
|
|
57
|
+
getController: vi.fn(),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Mock guard
|
|
62
|
+
const createMockGuard = (
|
|
63
|
+
canActivateResult: boolean | Promise<boolean>,
|
|
64
|
+
): CanActivate => ({
|
|
65
|
+
canActivate: vi.fn().mockImplementation(() => canActivateResult),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('GuardRunnerService', () => {
|
|
69
|
+
let container: Container
|
|
70
|
+
let mockForbiddenResponder: ReturnType<typeof createMockResponder>
|
|
71
|
+
let mockInternalErrorResponder: ReturnType<typeof createMockResponder>
|
|
72
|
+
let mockNotFoundResponder: ReturnType<typeof createMockResponder>
|
|
73
|
+
let mockValidationErrorResponder: ReturnType<typeof createMockResponder>
|
|
74
|
+
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
container = new Container()
|
|
77
|
+
vi.clearAllMocks()
|
|
78
|
+
|
|
79
|
+
// Create mock responders
|
|
80
|
+
mockForbiddenResponder = createMockResponder(403, 'Forbidden')
|
|
81
|
+
mockInternalErrorResponder = createMockResponder(
|
|
82
|
+
500,
|
|
83
|
+
'Internal Server Error',
|
|
84
|
+
)
|
|
85
|
+
mockNotFoundResponder = createMockResponder(404, 'Not Found')
|
|
86
|
+
mockValidationErrorResponder = createMockResponder(400, 'Validation Error')
|
|
87
|
+
|
|
88
|
+
// Register all required dependencies
|
|
89
|
+
container.addInstance(ForbiddenResponderToken, mockForbiddenResponder)
|
|
90
|
+
container.addInstance(
|
|
91
|
+
InternalServerErrorResponderToken,
|
|
92
|
+
mockInternalErrorResponder,
|
|
93
|
+
)
|
|
94
|
+
container.addInstance(NotFoundResponderToken, mockNotFoundResponder)
|
|
95
|
+
container.addInstance(
|
|
96
|
+
ValidationErrorResponderToken,
|
|
97
|
+
mockValidationErrorResponder,
|
|
98
|
+
)
|
|
99
|
+
container.addInstance(LoggerOutput, mockLoggerOutput)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
afterEach(async () => {
|
|
103
|
+
await container.dispose()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('runGuardsStatic', () => {
|
|
107
|
+
it('should return true when all guards pass', async () => {
|
|
108
|
+
const service = await container.get(GuardRunnerService)
|
|
109
|
+
const guards = [createMockGuard(true), createMockGuard(true)]
|
|
110
|
+
const context = createMockExecutionContext()
|
|
111
|
+
|
|
112
|
+
const result = await service.runGuardsStatic(guards, context)
|
|
113
|
+
|
|
114
|
+
expect(result).toBe(true)
|
|
115
|
+
expect(guards[0].canActivate).toHaveBeenCalledWith(context)
|
|
116
|
+
expect(guards[1].canActivate).toHaveBeenCalledWith(context)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should return false when a guard returns false', async () => {
|
|
120
|
+
const service = await container.get(GuardRunnerService)
|
|
121
|
+
const guards = [createMockGuard(true), createMockGuard(false)]
|
|
122
|
+
const context = createMockExecutionContext()
|
|
123
|
+
|
|
124
|
+
const result = await service.runGuardsStatic(guards, context)
|
|
125
|
+
|
|
126
|
+
expect(result).toBe(false)
|
|
127
|
+
expect(context.getReply().status).toHaveBeenCalledWith(403)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should stop execution when a guard fails', async () => {
|
|
131
|
+
const service = await container.get(GuardRunnerService)
|
|
132
|
+
const firstGuard = createMockGuard(false)
|
|
133
|
+
const secondGuard = createMockGuard(true)
|
|
134
|
+
const guards = [firstGuard, secondGuard]
|
|
135
|
+
const context = createMockExecutionContext()
|
|
136
|
+
|
|
137
|
+
await service.runGuardsStatic(guards, context)
|
|
138
|
+
|
|
139
|
+
expect(firstGuard.canActivate).toHaveBeenCalled()
|
|
140
|
+
expect(secondGuard.canActivate).not.toHaveBeenCalled()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should handle HttpException from guard', async () => {
|
|
144
|
+
const service = await container.get(GuardRunnerService)
|
|
145
|
+
const httpException = new HttpException(401, 'Unauthorized')
|
|
146
|
+
const guard = {
|
|
147
|
+
canActivate: vi.fn().mockRejectedValue(httpException),
|
|
148
|
+
}
|
|
149
|
+
const context = createMockExecutionContext()
|
|
150
|
+
|
|
151
|
+
const result = await service.runGuardsStatic([guard], context)
|
|
152
|
+
|
|
153
|
+
expect(result).toBe(false)
|
|
154
|
+
expect(context.getReply().status).toHaveBeenCalledWith(401)
|
|
155
|
+
expect(context.getReply().send).toHaveBeenCalledWith('Unauthorized')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should handle unknown errors from guard', async () => {
|
|
159
|
+
const service = await container.get(GuardRunnerService)
|
|
160
|
+
const guard = {
|
|
161
|
+
canActivate: vi.fn().mockRejectedValue(new Error('Unknown error')),
|
|
162
|
+
}
|
|
163
|
+
const context = createMockExecutionContext()
|
|
164
|
+
|
|
165
|
+
const result = await service.runGuardsStatic([guard], context)
|
|
166
|
+
|
|
167
|
+
expect(result).toBe(false)
|
|
168
|
+
expect(mockLoggerOutput.error).toHaveBeenCalled()
|
|
169
|
+
expect(context.getReply().status).toHaveBeenCalledWith(500)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should handle async guards', async () => {
|
|
173
|
+
const service = await container.get(GuardRunnerService)
|
|
174
|
+
const asyncGuard = createMockGuard(Promise.resolve(true))
|
|
175
|
+
const context = createMockExecutionContext()
|
|
176
|
+
|
|
177
|
+
const result = await service.runGuardsStatic([asyncGuard], context)
|
|
178
|
+
|
|
179
|
+
expect(result).toBe(true)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should work with empty guard array', async () => {
|
|
183
|
+
const service = await container.get(GuardRunnerService)
|
|
184
|
+
const context = createMockExecutionContext()
|
|
185
|
+
|
|
186
|
+
const result = await service.runGuardsStatic([], context)
|
|
187
|
+
|
|
188
|
+
expect(result).toBe(true)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('runGuards', () => {
|
|
193
|
+
it('should resolve guards from scoped container', async () => {
|
|
194
|
+
@Injectable()
|
|
195
|
+
class TestGuard implements CanActivate {
|
|
196
|
+
canActivate = vi.fn().mockReturnValue(true)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const service = await container.get(GuardRunnerService)
|
|
200
|
+
const testGuardInstance = new TestGuard()
|
|
201
|
+
|
|
202
|
+
const mockScopedContainer = {
|
|
203
|
+
get: vi.fn().mockResolvedValue(testGuardInstance),
|
|
204
|
+
} as unknown as ScopedContainer
|
|
205
|
+
|
|
206
|
+
const guards = new Set([TestGuard])
|
|
207
|
+
const context = createMockExecutionContext()
|
|
208
|
+
|
|
209
|
+
const result = await service.runGuards(
|
|
210
|
+
guards,
|
|
211
|
+
context,
|
|
212
|
+
mockScopedContainer,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
expect(result).toBe(true)
|
|
216
|
+
expect(mockScopedContainer.get).toHaveBeenCalled()
|
|
217
|
+
expect(testGuardInstance.canActivate).toHaveBeenCalledWith(context)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should throw error for guard without canActivate', async () => {
|
|
221
|
+
@Injectable()
|
|
222
|
+
class InvalidGuard {
|
|
223
|
+
// Missing canActivate
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const service = await container.get(GuardRunnerService)
|
|
227
|
+
const mockScopedContainer = {
|
|
228
|
+
get: vi.fn().mockResolvedValue(new InvalidGuard()),
|
|
229
|
+
} as unknown as ScopedContainer
|
|
230
|
+
|
|
231
|
+
const guards = new Set([InvalidGuard as any])
|
|
232
|
+
const context = createMockExecutionContext()
|
|
233
|
+
|
|
234
|
+
await expect(
|
|
235
|
+
service.runGuards(guards, context, mockScopedContainer),
|
|
236
|
+
).rejects.toThrow('does not implement canActivate')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should reverse guard order (module -> controller -> endpoint)', async () => {
|
|
240
|
+
const callOrder: string[] = []
|
|
241
|
+
|
|
242
|
+
@Injectable()
|
|
243
|
+
class Guard1 implements CanActivate {
|
|
244
|
+
canActivate() {
|
|
245
|
+
callOrder.push('guard1')
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@Injectable()
|
|
251
|
+
class Guard2 implements CanActivate {
|
|
252
|
+
canActivate() {
|
|
253
|
+
callOrder.push('guard2')
|
|
254
|
+
return true
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@Injectable()
|
|
259
|
+
class Guard3 implements CanActivate {
|
|
260
|
+
canActivate() {
|
|
261
|
+
callOrder.push('guard3')
|
|
262
|
+
return true
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const service = await container.get(GuardRunnerService)
|
|
267
|
+
|
|
268
|
+
const mockScopedContainer = {
|
|
269
|
+
get: vi.fn().mockImplementation((token) => {
|
|
270
|
+
if (token === Guard1) return new Guard1()
|
|
271
|
+
if (token === Guard2) return new Guard2()
|
|
272
|
+
if (token === Guard3) return new Guard3()
|
|
273
|
+
}),
|
|
274
|
+
} as unknown as ScopedContainer
|
|
275
|
+
|
|
276
|
+
// Order in Set: Guard1, Guard2, Guard3
|
|
277
|
+
// Should execute in reverse: Guard3, Guard2, Guard1
|
|
278
|
+
const guards = new Set([Guard1, Guard2, Guard3])
|
|
279
|
+
const context = createMockExecutionContext()
|
|
280
|
+
|
|
281
|
+
await service.runGuards(guards, context, mockScopedContainer)
|
|
282
|
+
|
|
283
|
+
expect(callOrder).toEqual(['guard3', 'guard2', 'guard1'])
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
describe('makeContext', () => {
|
|
288
|
+
it('should merge guards from module, controller, and endpoint', async () => {
|
|
289
|
+
const service = await container.get(GuardRunnerService)
|
|
290
|
+
|
|
291
|
+
class ModuleGuard {}
|
|
292
|
+
class ControllerGuard {}
|
|
293
|
+
class EndpointGuard {}
|
|
294
|
+
|
|
295
|
+
const moduleMetadata = {
|
|
296
|
+
guards: new Set([ModuleGuard]),
|
|
297
|
+
} as unknown as ModuleMetadata
|
|
298
|
+
|
|
299
|
+
const controllerMetadata = {
|
|
300
|
+
guards: new Set([ControllerGuard]),
|
|
301
|
+
} as unknown as ControllerMetadata
|
|
302
|
+
|
|
303
|
+
const endpointMetadata = {
|
|
304
|
+
guards: new Set([EndpointGuard]),
|
|
305
|
+
} as unknown as HandlerMetadata<any>
|
|
306
|
+
|
|
307
|
+
const result = service.makeContext(
|
|
308
|
+
moduleMetadata,
|
|
309
|
+
controllerMetadata,
|
|
310
|
+
endpointMetadata,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
expect(result.size).toBe(3)
|
|
314
|
+
expect(result.has(ModuleGuard as any)).toBe(true)
|
|
315
|
+
expect(result.has(ControllerGuard as any)).toBe(true)
|
|
316
|
+
expect(result.has(EndpointGuard as any)).toBe(true)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('should handle empty guards at each level', async () => {
|
|
320
|
+
const service = await container.get(GuardRunnerService)
|
|
321
|
+
|
|
322
|
+
const moduleMetadata = {
|
|
323
|
+
guards: new Set(),
|
|
324
|
+
} as unknown as ModuleMetadata
|
|
325
|
+
|
|
326
|
+
const controllerMetadata = {
|
|
327
|
+
guards: new Set(),
|
|
328
|
+
} as unknown as ControllerMetadata
|
|
329
|
+
|
|
330
|
+
const endpointMetadata = {
|
|
331
|
+
guards: new Set(),
|
|
332
|
+
} as unknown as HandlerMetadata<any>
|
|
333
|
+
|
|
334
|
+
const result = service.makeContext(
|
|
335
|
+
moduleMetadata,
|
|
336
|
+
controllerMetadata,
|
|
337
|
+
endpointMetadata,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
expect(result.size).toBe(0)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('should deduplicate guards', async () => {
|
|
344
|
+
const service = await container.get(GuardRunnerService)
|
|
345
|
+
|
|
346
|
+
class SharedGuard {}
|
|
347
|
+
|
|
348
|
+
const moduleMetadata = {
|
|
349
|
+
guards: new Set([SharedGuard]),
|
|
350
|
+
} as unknown as ModuleMetadata
|
|
351
|
+
|
|
352
|
+
const controllerMetadata = {
|
|
353
|
+
guards: new Set([SharedGuard]), // Same guard
|
|
354
|
+
} as unknown as ControllerMetadata
|
|
355
|
+
|
|
356
|
+
const endpointMetadata = {
|
|
357
|
+
guards: new Set([SharedGuard]), // Same guard
|
|
358
|
+
} as unknown as HandlerMetadata<any>
|
|
359
|
+
|
|
360
|
+
const result = service.makeContext(
|
|
361
|
+
moduleMetadata,
|
|
362
|
+
controllerMetadata,
|
|
363
|
+
endpointMetadata,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
// Set deduplicates automatically
|
|
367
|
+
expect(result.size).toBe(1)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('should handle injection tokens as guards', async () => {
|
|
371
|
+
const service = await container.get(GuardRunnerService)
|
|
372
|
+
|
|
373
|
+
const GuardToken = InjectionToken.create<CanActivate>(
|
|
374
|
+
Symbol.for('GuardToken'),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
const moduleMetadata = {
|
|
378
|
+
guards: new Set([GuardToken]),
|
|
379
|
+
} as unknown as ModuleMetadata
|
|
380
|
+
|
|
381
|
+
const controllerMetadata = {
|
|
382
|
+
guards: new Set(),
|
|
383
|
+
} as unknown as ControllerMetadata
|
|
384
|
+
|
|
385
|
+
const endpointMetadata = {
|
|
386
|
+
guards: new Set(),
|
|
387
|
+
} as unknown as HandlerMetadata<any>
|
|
388
|
+
|
|
389
|
+
const result = service.makeContext(
|
|
390
|
+
moduleMetadata,
|
|
391
|
+
controllerMetadata,
|
|
392
|
+
endpointMetadata,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
expect(result.size).toBe(1)
|
|
396
|
+
expect(result.has(GuardToken)).toBe(true)
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Container } from '@navios/di'
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import type { LoggerService } from '../index.mjs'
|
|
6
|
+
|
|
7
|
+
import { LoggerInstance } from '../logger/logger.service.mjs'
|
|
8
|
+
import { LoggerOutput } from '../logger/logger.tokens.mjs'
|
|
9
|
+
|
|
10
|
+
describe('LoggerInstance', () => {
|
|
11
|
+
let container: Container
|
|
12
|
+
let mockLoggerOutput: {
|
|
13
|
+
log: ReturnType<typeof vi.fn>
|
|
14
|
+
error: ReturnType<typeof vi.fn>
|
|
15
|
+
warn: ReturnType<typeof vi.fn>
|
|
16
|
+
debug: ReturnType<typeof vi.fn>
|
|
17
|
+
verbose: ReturnType<typeof vi.fn>
|
|
18
|
+
fatal: ReturnType<typeof vi.fn>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
container = new Container()
|
|
23
|
+
mockLoggerOutput = {
|
|
24
|
+
log: vi.fn(),
|
|
25
|
+
error: vi.fn(),
|
|
26
|
+
warn: vi.fn(),
|
|
27
|
+
debug: vi.fn(),
|
|
28
|
+
verbose: vi.fn(),
|
|
29
|
+
fatal: vi.fn(),
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await container.dispose()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('log', () => {
|
|
38
|
+
it('should call localInstance.log with message', async () => {
|
|
39
|
+
container.addInstance(LoggerOutput, mockLoggerOutput as LoggerService)
|
|
40
|
+
const logger = await container.get(LoggerInstance)
|
|
41
|
+
|
|
42
|
+
logger.log('Test message')
|
|
43
|
+
|
|
44
|
+
expect(mockLoggerOutput.log).toHaveBeenCalledWith('Test message')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('error', () => {
|
|
49
|
+
it('should call localInstance.error with message', async () => {
|
|
50
|
+
container.addInstance(LoggerOutput, mockLoggerOutput as LoggerService)
|
|
51
|
+
const logger = await container.get(LoggerInstance)
|
|
52
|
+
|
|
53
|
+
logger.error('Error message')
|
|
54
|
+
|
|
55
|
+
expect(mockLoggerOutput.error).toHaveBeenCalledWith('Error message')
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('warn', () => {
|
|
60
|
+
it('should call localInstance.warn with message', async () => {
|
|
61
|
+
container.addInstance(LoggerOutput, mockLoggerOutput as LoggerService)
|
|
62
|
+
const logger = await container.get(LoggerInstance)
|
|
63
|
+
|
|
64
|
+
logger.warn('Warning message')
|
|
65
|
+
|
|
66
|
+
expect(mockLoggerOutput.warn).toHaveBeenCalledWith('Warning message')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('debug', () => {
|
|
71
|
+
it('should call localInstance.debug with message', async () => {
|
|
72
|
+
container.addInstance(LoggerOutput, mockLoggerOutput as LoggerService)
|
|
73
|
+
const logger = await container.get(LoggerInstance)
|
|
74
|
+
|
|
75
|
+
logger.debug('Debug message')
|
|
76
|
+
|
|
77
|
+
expect(mockLoggerOutput.debug).toHaveBeenCalledWith('Debug message')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should handle missing debug method gracefully', async () => {
|
|
81
|
+
const loggerWithoutDebug = {
|
|
82
|
+
log: vi.fn(),
|
|
83
|
+
error: vi.fn(),
|
|
84
|
+
warn: vi.fn(),
|
|
85
|
+
// No debug method
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
container.addInstance(LoggerOutput, loggerWithoutDebug as LoggerService)
|
|
89
|
+
const logger = await container.get(LoggerInstance)
|
|
90
|
+
|
|
91
|
+
// Should not throw
|
|
92
|
+
expect(() => logger.debug('Debug message')).not.toThrow()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('verbose', () => {
|
|
97
|
+
it('should call localInstance.verbose with message', async () => {
|
|
98
|
+
container.addInstance(LoggerOutput, mockLoggerOutput as LoggerService)
|
|
99
|
+
const logger = await container.get(LoggerInstance)
|
|
100
|
+
|
|
101
|
+
logger.verbose('Verbose message')
|
|
102
|
+
|
|
103
|
+
expect(mockLoggerOutput.verbose).toHaveBeenCalledWith('Verbose message')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should handle missing verbose method gracefully', async () => {
|
|
107
|
+
const loggerWithoutVerbose = {
|
|
108
|
+
log: vi.fn(),
|
|
109
|
+
error: vi.fn(),
|
|
110
|
+
warn: vi.fn(),
|
|
111
|
+
// No verbose method
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
container.addInstance(LoggerOutput, loggerWithoutVerbose as LoggerService)
|
|
115
|
+
const logger = await container.get(LoggerInstance)
|
|
116
|
+
|
|
117
|
+
// Should not throw
|
|
118
|
+
expect(() => logger.verbose('Verbose message')).not.toThrow()
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('fatal', () => {
|
|
123
|
+
it('should call localInstance.fatal with message', async () => {
|
|
124
|
+
container.addInstance(LoggerOutput, mockLoggerOutput as LoggerService)
|
|
125
|
+
const logger = await container.get(LoggerInstance)
|
|
126
|
+
|
|
127
|
+
logger.fatal('Fatal message')
|
|
128
|
+
|
|
129
|
+
expect(mockLoggerOutput.fatal).toHaveBeenCalledWith('Fatal message')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should handle missing fatal method gracefully', async () => {
|
|
133
|
+
const loggerWithoutFatal = {
|
|
134
|
+
log: vi.fn(),
|
|
135
|
+
error: vi.fn(),
|
|
136
|
+
warn: vi.fn(),
|
|
137
|
+
// No fatal method
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
container.addInstance(LoggerOutput, loggerWithoutFatal as LoggerService)
|
|
141
|
+
const logger = await container.get(LoggerInstance)
|
|
142
|
+
|
|
143
|
+
// Should not throw
|
|
144
|
+
expect(() => logger.fatal('Fatal message')).not.toThrow()
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -9,10 +9,11 @@ import type { ErrorResponse } from '../responders/interfaces/error-response.inte
|
|
|
9
9
|
import { NotFoundException } from '../exceptions/not-found.exception.mjs'
|
|
10
10
|
import { FrameworkError } from '../responders/enums/framework-error.enum.mjs'
|
|
11
11
|
import { ErrorResponseProducerService } from '../responders/services/error-response-producer.service.mjs'
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
12
|
+
// Import services for side-effects (registers @Injectable decorators)
|
|
13
|
+
import '../responders/services/forbidden-responder.service.mjs'
|
|
14
|
+
import '../responders/services/internal-server-error-responder.service.mjs'
|
|
15
|
+
import '../responders/services/not-found-responder.service.mjs'
|
|
16
|
+
import '../responders/services/validation-error-responder.service.mjs'
|
|
16
17
|
import {
|
|
17
18
|
ForbiddenResponderToken,
|
|
18
19
|
InternalServerErrorResponderToken,
|
|
@@ -309,7 +310,7 @@ describe('Responders', () => {
|
|
|
309
310
|
token: NotFoundResponderToken,
|
|
310
311
|
priority: 0, // Higher than default -10
|
|
311
312
|
})
|
|
312
|
-
class
|
|
313
|
+
class _CustomNotFoundResponder implements ErrorResponder {
|
|
313
314
|
getResponse(_error: unknown, description?: string): ErrorResponse {
|
|
314
315
|
return {
|
|
315
316
|
statusCode: 404,
|
|
@@ -32,10 +32,94 @@ export type HandlerResult<TRequest = any, TReply = any> =
|
|
|
32
32
|
| StaticHandler<TRequest, TReply>
|
|
33
33
|
| DynamicHandler<TRequest, TReply>
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Function type for argument getters that extract data from requests.
|
|
37
|
+
* Each getter populates a target object with data from the request.
|
|
38
|
+
*/
|
|
39
|
+
export type ArgumentGetterFn<TRequest = any> = (
|
|
40
|
+
target: Record<string, any>,
|
|
41
|
+
request: TRequest,
|
|
42
|
+
) => void | Promise<void>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Function type for formatting arguments from a request.
|
|
46
|
+
* Built from argument getters, optimized for sync/async handling.
|
|
47
|
+
*/
|
|
48
|
+
export type FormatArgumentsFn<TRequest = any> = (
|
|
49
|
+
request: TRequest,
|
|
50
|
+
) => Record<string, any> | Promise<Record<string, any>>
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Interface for HTTP handler adapter services.
|
|
54
|
+
*
|
|
55
|
+
* Adapters handle different types of HTTP requests (REST, streaming, multipart)
|
|
56
|
+
* and are responsible for:
|
|
57
|
+
* - Parsing and validating request data
|
|
58
|
+
* - Creating handler functions
|
|
59
|
+
* - Formatting responses
|
|
60
|
+
* - Providing schema information (for frameworks like Fastify)
|
|
61
|
+
*/
|
|
62
|
+
export interface AbstractHttpHandlerAdapterInterface<TRequest = any> {
|
|
63
|
+
/**
|
|
64
|
+
* Prepares argument getters for parsing request data.
|
|
65
|
+
*
|
|
66
|
+
* Creates functions that extract and validate data from the request,
|
|
67
|
+
* populating a target object with validated arguments.
|
|
68
|
+
*
|
|
69
|
+
* @param handlerMetadata - The handler metadata with schemas and configuration.
|
|
70
|
+
* @returns An array of getter functions that populate request arguments.
|
|
71
|
+
*/
|
|
36
72
|
prepareArguments?: (
|
|
37
73
|
handlerMetadata: HandlerMetadata<any>,
|
|
38
|
-
) =>
|
|
74
|
+
) => ArgumentGetterFn<TRequest>[]
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Builds a formatArguments function from argument getters.
|
|
78
|
+
*
|
|
79
|
+
* Automatically detects sync vs async getters and optimizes accordingly:
|
|
80
|
+
* - If all getters are sync: returns sync function (no Promise overhead)
|
|
81
|
+
* - If any getter is async: returns async function with Promise.all
|
|
82
|
+
* - If no getters: returns frozen empty object (zero allocation)
|
|
83
|
+
*
|
|
84
|
+
* This method is useful for composition-based adapters that need to
|
|
85
|
+
* build formatArguments without duplicating the optimization logic.
|
|
86
|
+
*
|
|
87
|
+
* @param getters - Array of argument getter functions
|
|
88
|
+
* @returns Function to format arguments from request
|
|
89
|
+
*/
|
|
90
|
+
buildFormatArguments?: (
|
|
91
|
+
getters: ArgumentGetterFn<TRequest>[],
|
|
92
|
+
) => FormatArgumentsFn<TRequest>
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Checks if the handler has any validation schemas defined.
|
|
96
|
+
*
|
|
97
|
+
* @param handlerMetadata - The handler metadata containing configuration.
|
|
98
|
+
* @returns `true` if the handler has any schemas (request, query, response).
|
|
99
|
+
*/
|
|
100
|
+
hasSchema?: (handlerMetadata: HandlerMetadata<any>) => boolean
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Provides schema information for the framework's validation system.
|
|
104
|
+
*
|
|
105
|
+
* For frameworks like Fastify, this returns route schema objects.
|
|
106
|
+
* For frameworks like Bun, this typically returns an empty object.
|
|
107
|
+
*
|
|
108
|
+
* @param handlerMetadata - The handler metadata containing configuration.
|
|
109
|
+
* @returns Schema information for framework registration.
|
|
110
|
+
*/
|
|
111
|
+
provideSchema?: (handlerMetadata: HandlerMetadata<any>) => Record<string, any>
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates a request handler function for the endpoint.
|
|
115
|
+
*
|
|
116
|
+
* This is the core method that generates the actual handler function
|
|
117
|
+
* that will be called when a request matches the endpoint.
|
|
118
|
+
*
|
|
119
|
+
* @param controller - The controller class containing the handler method.
|
|
120
|
+
* @param handlerMetadata - The handler metadata with configuration and schemas.
|
|
121
|
+
* @returns A handler result that is either static or dynamic.
|
|
122
|
+
*/
|
|
39
123
|
provideHandler: (
|
|
40
124
|
controller: ClassType,
|
|
41
125
|
handlerMetadata: HandlerMetadata<any>,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { z, ZodType } from 'zod/v4'
|
|
2
1
|
import type { ClassType } from '@navios/di'
|
|
2
|
+
import { createClassContext, createMethodContext } from '@navios/di/legacy-compat'
|
|
3
|
+
import type { z, ZodType } from 'zod/v4'
|
|
3
4
|
|
|
4
5
|
import type {
|
|
5
6
|
ControllerMetadata,
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
getManagedMetadata,
|
|
19
20
|
hasManagedMetadata,
|
|
20
21
|
} from '../metadata/navios-managed.metadata.mjs'
|
|
21
|
-
import { createClassContext, createMethodContext } from './context-compat.mjs'
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Type for a legacy class/method attribute decorator without a value.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ClassType } from '@navios/di'
|
|
2
|
+
import { createClassContext } from '@navios/di/legacy-compat'
|
|
2
3
|
|
|
3
4
|
import type { ControllerOptions } from '../../decorators/controller.decorator.mjs'
|
|
4
5
|
|
|
5
6
|
import { Controller as OriginalController } from '../../decorators/controller.decorator.mjs'
|
|
6
|
-
import { createClassContext } from '../context-compat.mjs'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Legacy-compatible Controller decorator.
|