@navios/core 0.6.0 → 0.7.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.
- package/CHANGELOG.md +115 -0
- package/README.md +18 -1
- package/docs/README.md +1 -0
- package/docs/legacy-compat.md +320 -0
- package/docs/testing.md +140 -17
- package/lib/index-DW9EPAE6.d.mts +2156 -0
- package/lib/index-DW9EPAE6.d.mts.map +1 -0
- package/lib/index-pHp-dIGt.d.cts +2156 -0
- package/lib/index-pHp-dIGt.d.cts.map +1 -0
- package/lib/index.cjs +157 -0
- package/lib/index.d.cts +3 -0
- package/lib/index.d.mts +3 -190
- package/lib/index.mjs +4 -1459
- package/lib/legacy-compat/index.cjs +315 -0
- package/lib/legacy-compat/index.cjs.map +1 -0
- package/lib/legacy-compat/index.d.cts +219 -0
- package/lib/legacy-compat/index.d.cts.map +1 -0
- package/lib/legacy-compat/index.d.mts +219 -0
- package/lib/legacy-compat/index.d.mts.map +1 -0
- package/lib/legacy-compat/index.mjs +308 -0
- package/lib/legacy-compat/index.mjs.map +1 -0
- package/lib/src-DyvCDuKO.mjs +5443 -0
- package/lib/src-DyvCDuKO.mjs.map +1 -0
- package/lib/src-QnxR5b7c.cjs +5800 -0
- package/lib/src-QnxR5b7c.cjs.map +1 -0
- package/lib/testing/index.cjs +106 -0
- package/lib/testing/index.cjs.map +1 -0
- package/lib/testing/index.d.cts +156 -0
- package/lib/testing/index.d.cts.map +1 -0
- package/lib/testing/index.d.mts +156 -0
- package/lib/testing/index.d.mts.map +1 -0
- package/lib/testing/index.mjs +100 -0
- package/lib/testing/index.mjs.map +1 -0
- package/lib/use-guards.decorator-B6q_N0sf.cjs +622 -0
- package/lib/use-guards.decorator-B6q_N0sf.cjs.map +1 -0
- package/lib/use-guards.decorator-kZ3lNK8v.mjs +454 -0
- package/lib/use-guards.decorator-kZ3lNK8v.mjs.map +1 -0
- package/package.json +28 -8
- package/project.json +2 -2
- package/src/attribute.factory.mts +154 -0
- package/src/config/config-service.interface.mts +31 -0
- package/src/config/config.provider.mts +36 -0
- package/src/config/config.service.mts +94 -4
- package/src/decorators/controller.decorator.mts +28 -0
- package/src/decorators/endpoint.decorator.mts +76 -0
- package/src/decorators/header.decorator.mts +19 -0
- package/src/decorators/http-code.decorator.mts +20 -0
- package/src/decorators/module.decorator.mts +34 -0
- package/src/decorators/multipart.decorator.mts +41 -0
- package/src/decorators/stream.decorator.mts +33 -0
- package/src/decorators/use-guards.decorator.mts +29 -0
- package/src/exceptions/bad-request.exception.mts +21 -0
- package/src/exceptions/conflict.exception.mts +24 -0
- package/src/exceptions/forbidden.exception.mts +23 -0
- package/src/exceptions/http.exception.mts +26 -0
- package/src/exceptions/internal-server-error.exception.mts +26 -0
- package/src/exceptions/not-found.exception.mts +23 -0
- package/src/exceptions/unauthorized.exception.mts +23 -0
- package/src/index.mts +1 -0
- package/src/interfaces/abstract-execution-context.inteface.mts +35 -0
- package/src/interfaces/abstract-http-adapter.interface.mts +52 -0
- package/src/interfaces/abstract-http-handler-adapter.interface.mts +2 -2
- package/src/interfaces/can-activate.mts +31 -0
- package/src/interfaces/index.mts +1 -0
- package/src/interfaces/navios-module.mts +25 -0
- package/src/interfaces/plugin.interface.mts +105 -0
- package/src/legacy-compat/__type-tests__/legacy-decorators.spec-d.mts +420 -0
- package/src/legacy-compat/__type-tests__/tsconfig.json +15 -0
- package/src/legacy-compat/context-compat.mts +93 -0
- package/src/legacy-compat/decorators/controller.decorator.mts +31 -0
- package/src/legacy-compat/decorators/endpoint.decorator.mts +99 -0
- package/src/legacy-compat/decorators/header.decorator.mts +42 -0
- package/src/legacy-compat/decorators/http-code.decorator.mts +38 -0
- package/src/legacy-compat/decorators/index.mts +9 -0
- package/src/legacy-compat/decorators/module.decorator.mts +37 -0
- package/src/legacy-compat/decorators/multipart.decorator.mts +93 -0
- package/src/legacy-compat/decorators/stream.decorator.mts +76 -0
- package/src/legacy-compat/decorators/use-guards.decorator.mts +80 -0
- package/src/legacy-compat/index.mts +40 -0
- package/src/logger/console-logger.service.mts +15 -2
- package/src/logger/log-levels.mts +9 -0
- package/src/logger/logger.service.mts +21 -0
- package/src/logger/logger.tokens.mts +23 -0
- package/src/navios.application.mts +228 -4
- package/src/navios.factory.mts +60 -1
- package/src/services/guard-runner.service.mts +12 -11
- package/src/services/module-loader.service.mts +118 -12
- package/src/stores/index.mts +1 -0
- package/src/stores/request-id.store.mts +43 -0
- package/src/testing/index.mts +2 -0
- package/src/testing/testing-module.mts +231 -0
- package/tsconfig.lib.json +1 -1
- package/tsconfig.spec.json +3 -0
- package/tsdown.config.mts +35 -0
- package/vitest.config.mts +6 -0
- package/lib/_tsup-dts-rollup.d.mts +0 -1365
- package/lib/_tsup-dts-rollup.d.ts +0 -1365
- package/lib/index.d.ts +0 -190
- package/lib/index.js +0 -1540
- package/lib/index.js.map +0 -1
- package/lib/index.mjs.map +0 -1
- package/tsup.config.mts +0 -13
|
@@ -2,19 +2,71 @@ import type { ModuleMetadata } from '../metadata/index.mjs'
|
|
|
2
2
|
import type { AbstractHttpCorsOptions } from './abstract-http-cors-options.interface.mjs'
|
|
3
3
|
import type { AbstractHttpListenOptions } from './abstract-http-listen-options.interface.mjs'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Abstract interface for HTTP adapters.
|
|
7
|
+
*
|
|
8
|
+
* Adapters implement this interface to provide runtime-specific HTTP server
|
|
9
|
+
* functionality (Fastify, Bun, etc.).
|
|
10
|
+
*
|
|
11
|
+
* @typeParam ServerInstance - The underlying server type (e.g., FastifyInstance)
|
|
12
|
+
* @typeParam CorsOptions - CORS configuration options type
|
|
13
|
+
* @typeParam Options - Server setup options type
|
|
14
|
+
* @typeParam MultipartOptions - Multipart form handling options type
|
|
15
|
+
*/
|
|
5
16
|
export interface AbstractHttpAdapterInterface<
|
|
6
17
|
ServerInstance,
|
|
7
18
|
CorsOptions = AbstractHttpCorsOptions,
|
|
8
19
|
Options = {},
|
|
9
20
|
MultipartOptions = {},
|
|
10
21
|
> {
|
|
22
|
+
/**
|
|
23
|
+
* Sets up the HTTP server with the provided options.
|
|
24
|
+
*/
|
|
11
25
|
setupHttpServer(options: Options): Promise<void>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Called after all modules are loaded to register routes.
|
|
29
|
+
*/
|
|
12
30
|
onModulesInit(modules: Map<string, ModuleMetadata>): Promise<void>
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Signals that the server is ready to accept requests.
|
|
34
|
+
*/
|
|
13
35
|
ready(): Promise<void>
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the underlying HTTP server instance.
|
|
39
|
+
*/
|
|
14
40
|
getServer(): ServerInstance
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sets a global prefix for all routes.
|
|
44
|
+
*/
|
|
15
45
|
setGlobalPrefix(prefix: string): void
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Gets the current global prefix.
|
|
49
|
+
* Returns empty string if no prefix is set.
|
|
50
|
+
*/
|
|
51
|
+
getGlobalPrefix(): string
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Enables CORS with the specified options.
|
|
55
|
+
*/
|
|
16
56
|
enableCors(options: CorsOptions): void
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Enables multipart form data handling.
|
|
60
|
+
*/
|
|
17
61
|
enableMultipart(options: MultipartOptions): void
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Starts the server and listens for incoming requests.
|
|
65
|
+
*/
|
|
18
66
|
listen(options: AbstractHttpListenOptions): Promise<string>
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Disposes of the server and cleans up resources.
|
|
70
|
+
*/
|
|
19
71
|
dispose(): Promise<void>
|
|
20
72
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ClassType,
|
|
1
|
+
import type { ClassType, ScopedContainer } from '@navios/di'
|
|
2
2
|
|
|
3
3
|
import type { HandlerMetadata } from '../metadata/index.mjs'
|
|
4
4
|
|
|
@@ -9,5 +9,5 @@ export interface AbstractHttpHandlerAdapterInterface {
|
|
|
9
9
|
provideHandler: (
|
|
10
10
|
controller: ClassType,
|
|
11
11
|
handlerMetadata: HandlerMetadata<any>,
|
|
12
|
-
) => (context:
|
|
12
|
+
) => (context: ScopedContainer, request: any, reply: any) => Promise<any>
|
|
13
13
|
}
|
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
import type { AbstractExecutionContext } from '../interfaces/index.mjs'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Interface that guards must implement to control access to endpoints.
|
|
5
|
+
*
|
|
6
|
+
* Guards are used for authentication, authorization, and request validation.
|
|
7
|
+
* They are executed before the endpoint handler and can prevent the request
|
|
8
|
+
* from proceeding by returning `false` or throwing an exception.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* @Injectable()
|
|
13
|
+
* export class AuthGuard implements CanActivate {
|
|
14
|
+
* async canActivate(context: AbstractExecutionContext): Promise<boolean> {
|
|
15
|
+
* const request = context.getRequest()
|
|
16
|
+
* const token = request.headers.authorization
|
|
17
|
+
*
|
|
18
|
+
* if (!token) {
|
|
19
|
+
* throw new UnauthorizedException('Authentication required')
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* // Validate token
|
|
23
|
+
* return true
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
3
28
|
export interface CanActivate {
|
|
29
|
+
/**
|
|
30
|
+
* Determines if the current request can proceed to the endpoint handler.
|
|
31
|
+
*
|
|
32
|
+
* @param executionContext - The execution context containing request, reply, and metadata
|
|
33
|
+
* @returns `true` if the request can proceed, `false` otherwise. Can also throw an exception.
|
|
34
|
+
*/
|
|
4
35
|
canActivate(
|
|
5
36
|
executionContext: AbstractExecutionContext,
|
|
6
37
|
): Promise<boolean> | boolean
|
package/src/interfaces/index.mts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface that all Navios modules must implement.
|
|
3
|
+
*
|
|
4
|
+
* Modules decorated with @Module() should implement this interface to receive
|
|
5
|
+
* lifecycle hooks.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* @Module({
|
|
10
|
+
* controllers: [UserController],
|
|
11
|
+
* })
|
|
12
|
+
* export class AppModule implements NaviosModule {
|
|
13
|
+
* async onModuleInit() {
|
|
14
|
+
* console.log('AppModule initialized')
|
|
15
|
+
* // Perform initialization logic
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
1
20
|
export interface NaviosModule {
|
|
21
|
+
/**
|
|
22
|
+
* Optional lifecycle hook called after the module is initialized.
|
|
23
|
+
*
|
|
24
|
+
* This is called after all modules are loaded and the HTTP server is set up
|
|
25
|
+
* (if an adapter is configured), but before the server starts listening.
|
|
26
|
+
*/
|
|
2
27
|
onModuleInit?: () => Promise<void> | void
|
|
3
28
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Container } from '@navios/di'
|
|
2
|
+
|
|
3
|
+
import type { ModuleMetadata } from '../metadata/index.mjs'
|
|
4
|
+
import type { ModuleLoaderService } from '../services/module-loader.service.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Context provided to plugins during registration.
|
|
8
|
+
*
|
|
9
|
+
* This context gives plugins access to the application's modules,
|
|
10
|
+
* server instance, DI container, and configuration.
|
|
11
|
+
*/
|
|
12
|
+
export interface PluginContext {
|
|
13
|
+
/**
|
|
14
|
+
* All loaded modules with their metadata.
|
|
15
|
+
* Keys are module class names, values are their metadata.
|
|
16
|
+
*/
|
|
17
|
+
modules: Map<string, ModuleMetadata>
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The underlying HTTP server instance.
|
|
21
|
+
* Type depends on the adapter used (Fastify, Bun, etc.)
|
|
22
|
+
*/
|
|
23
|
+
server: any
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The dependency injection container.
|
|
27
|
+
*/
|
|
28
|
+
container: Container
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Global route prefix (e.g., '/api/v1').
|
|
32
|
+
* Empty string if no prefix is set.
|
|
33
|
+
*/
|
|
34
|
+
globalPrefix: string
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Module loader service for extending the module tree.
|
|
38
|
+
* Use `moduleLoader.extendModules()` to add controllers dynamically.
|
|
39
|
+
*/
|
|
40
|
+
moduleLoader: ModuleLoaderService
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Base interface for Navios plugins.
|
|
45
|
+
*
|
|
46
|
+
* Plugins are registered using `app.usePlugin()` and are initialized
|
|
47
|
+
* after all modules are loaded but before the server starts listening.
|
|
48
|
+
*
|
|
49
|
+
* @typeParam TOptions - The type of options the plugin accepts
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const myPlugin: NaviosPlugin<{ enabled: boolean }> = {
|
|
54
|
+
* name: 'my-plugin',
|
|
55
|
+
* register: async (context, options) => {
|
|
56
|
+
* if (options.enabled) {
|
|
57
|
+
* // Register routes, services, etc.
|
|
58
|
+
* }
|
|
59
|
+
* },
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export interface NaviosPlugin<TOptions = unknown> {
|
|
64
|
+
/**
|
|
65
|
+
* Plugin name for identification and logging.
|
|
66
|
+
*/
|
|
67
|
+
name: string
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Called after modules are loaded but before the server starts listening.
|
|
71
|
+
*
|
|
72
|
+
* @param context - The plugin context with access to modules and server
|
|
73
|
+
* @param options - Plugin-specific configuration options
|
|
74
|
+
*/
|
|
75
|
+
register(context: PluginContext, options: TOptions): Promise<void> | void
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Plugin definition combining a plugin with its options.
|
|
80
|
+
*
|
|
81
|
+
* This is the type returned by plugin factory functions like `defineOpenApiPlugin()`.
|
|
82
|
+
*
|
|
83
|
+
* @typeParam TOptions - The type of options the plugin accepts
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* function defineMyPlugin(options: MyPluginOptions): PluginDefinition<MyPluginOptions> {
|
|
88
|
+
* return {
|
|
89
|
+
* plugin: myPlugin,
|
|
90
|
+
* options,
|
|
91
|
+
* }
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export interface PluginDefinition<TOptions = unknown> {
|
|
96
|
+
/**
|
|
97
|
+
* The plugin instance.
|
|
98
|
+
*/
|
|
99
|
+
plugin: NaviosPlugin<TOptions>
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Options to pass to the plugin's register function.
|
|
103
|
+
*/
|
|
104
|
+
options: TOptions
|
|
105
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
// oxlint-disable no-unused-vars
|
|
2
|
+
import { builder } from '@navios/builder'
|
|
3
|
+
|
|
4
|
+
import { describe, expectTypeOf, test } from 'vitest'
|
|
5
|
+
import { z } from 'zod/v4'
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
EndpointParams,
|
|
9
|
+
EndpointResult,
|
|
10
|
+
MultipartParams,
|
|
11
|
+
MultipartResult,
|
|
12
|
+
StreamParams,
|
|
13
|
+
} from '../index.mjs'
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
Controller,
|
|
17
|
+
Endpoint,
|
|
18
|
+
Header,
|
|
19
|
+
HttpCode,
|
|
20
|
+
Module,
|
|
21
|
+
Multipart,
|
|
22
|
+
Stream,
|
|
23
|
+
UseGuards,
|
|
24
|
+
} from '../index.mjs'
|
|
25
|
+
|
|
26
|
+
// Create a test API builder
|
|
27
|
+
const api = builder()
|
|
28
|
+
|
|
29
|
+
// Test schemas
|
|
30
|
+
const responseSchema = z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
name: z.string(),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const querySchema = z.object({
|
|
36
|
+
page: z.number(),
|
|
37
|
+
limit: z.number(),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const requestSchema = z.object({
|
|
41
|
+
name: z.string(),
|
|
42
|
+
email: z.string(),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const multipartRequestSchema = z.object({
|
|
46
|
+
file: z.instanceof(File),
|
|
47
|
+
description: z.string(),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Test endpoint declarations
|
|
51
|
+
const getUserEndpoint = api.declareEndpoint({
|
|
52
|
+
method: 'GET',
|
|
53
|
+
url: '/users/$userId',
|
|
54
|
+
querySchema,
|
|
55
|
+
responseSchema,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const createUserEndpoint = api.declareEndpoint({
|
|
59
|
+
method: 'POST',
|
|
60
|
+
url: '/users',
|
|
61
|
+
requestSchema,
|
|
62
|
+
responseSchema,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const uploadFileEndpoint = api.declareMultipart({
|
|
66
|
+
method: 'POST',
|
|
67
|
+
url: '/upload',
|
|
68
|
+
requestSchema: multipartRequestSchema,
|
|
69
|
+
responseSchema,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const downloadFileEndpoint = api.declareStream({
|
|
73
|
+
method: 'GET',
|
|
74
|
+
url: '/files/$fileId',
|
|
75
|
+
querySchema,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Mock guard for testing
|
|
79
|
+
class AuthGuard {
|
|
80
|
+
canActivate() {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('Legacy Decorators Type Safety', () => {
|
|
86
|
+
describe('Module decorator', () => {
|
|
87
|
+
test('should accept valid module options', () => {
|
|
88
|
+
@Module({
|
|
89
|
+
controllers: [],
|
|
90
|
+
imports: [],
|
|
91
|
+
guards: [],
|
|
92
|
+
})
|
|
93
|
+
class TestModule {}
|
|
94
|
+
|
|
95
|
+
expectTypeOf(TestModule).toBeConstructibleWith()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('should accept controllers in module', () => {
|
|
99
|
+
@Controller()
|
|
100
|
+
class TestController {}
|
|
101
|
+
|
|
102
|
+
@Module({
|
|
103
|
+
controllers: [TestController],
|
|
104
|
+
})
|
|
105
|
+
class TestModule {}
|
|
106
|
+
|
|
107
|
+
expectTypeOf(TestModule).toBeConstructibleWith()
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('Controller decorator', () => {
|
|
112
|
+
test('should accept valid controller options', () => {
|
|
113
|
+
@Controller()
|
|
114
|
+
class TestController {}
|
|
115
|
+
|
|
116
|
+
expectTypeOf(TestController).toBeConstructibleWith()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('should accept guards in controller', () => {
|
|
120
|
+
@Controller({
|
|
121
|
+
guards: [AuthGuard],
|
|
122
|
+
})
|
|
123
|
+
class TestController {}
|
|
124
|
+
|
|
125
|
+
expectTypeOf(TestController).toBeConstructibleWith()
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('Endpoint decorator', () => {
|
|
130
|
+
test('should enforce correct parameter type', () => {
|
|
131
|
+
@Controller()
|
|
132
|
+
class UserController {
|
|
133
|
+
@Endpoint(getUserEndpoint)
|
|
134
|
+
async getUser(
|
|
135
|
+
request: EndpointParams<typeof getUserEndpoint>,
|
|
136
|
+
): EndpointResult<typeof getUserEndpoint> {
|
|
137
|
+
// TypeScript should infer:
|
|
138
|
+
// - request.urlParams.userId: string | number
|
|
139
|
+
// - request.params.page: number
|
|
140
|
+
// - request.params.limit: number
|
|
141
|
+
expectTypeOf(request.urlParams.userId).toEqualTypeOf<
|
|
142
|
+
string | number
|
|
143
|
+
>()
|
|
144
|
+
expectTypeOf(request.params.page).toEqualTypeOf<number>()
|
|
145
|
+
expectTypeOf(request.params.limit).toEqualTypeOf<number>()
|
|
146
|
+
return {
|
|
147
|
+
id: request.urlParams.userId.toString(),
|
|
148
|
+
name: 'Test',
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
expectTypeOf(UserController).toBeConstructibleWith()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('should enforce correct return type', () => {
|
|
157
|
+
@Controller()
|
|
158
|
+
class UserController {
|
|
159
|
+
@Endpoint(getUserEndpoint)
|
|
160
|
+
async getUser(
|
|
161
|
+
request: EndpointParams<typeof getUserEndpoint>,
|
|
162
|
+
): EndpointResult<typeof getUserEndpoint> {
|
|
163
|
+
// Return type should match responseSchema
|
|
164
|
+
return {
|
|
165
|
+
id: '1',
|
|
166
|
+
name: 'John',
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
expectTypeOf(UserController).toBeConstructibleWith()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('should reject incorrect parameter type', () => {
|
|
175
|
+
@Controller()
|
|
176
|
+
class UserController {
|
|
177
|
+
// @ts-expect-error - wrong parameter type
|
|
178
|
+
@Endpoint(getUserEndpoint)
|
|
179
|
+
async getUser(request: {
|
|
180
|
+
wrong: string
|
|
181
|
+
}): EndpointResult<typeof getUserEndpoint> {
|
|
182
|
+
return { id: '1', name: 'John' }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('should reject incorrect return type', () => {
|
|
188
|
+
@Controller()
|
|
189
|
+
class UserController {
|
|
190
|
+
@Endpoint(getUserEndpoint)
|
|
191
|
+
async getUser(
|
|
192
|
+
request: EndpointParams<typeof getUserEndpoint>,
|
|
193
|
+
): EndpointResult<typeof getUserEndpoint> {
|
|
194
|
+
// @ts-expect-error - wrong return type
|
|
195
|
+
return { wrong: 'value' }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('should work with POST endpoint with request body', () => {
|
|
201
|
+
@Controller()
|
|
202
|
+
class UserController {
|
|
203
|
+
@Endpoint(createUserEndpoint)
|
|
204
|
+
async createUser(
|
|
205
|
+
request: EndpointParams<typeof createUserEndpoint>,
|
|
206
|
+
): EndpointResult<typeof createUserEndpoint> {
|
|
207
|
+
// TypeScript should infer:
|
|
208
|
+
// - request.data.name: string
|
|
209
|
+
// - request.data.email: string
|
|
210
|
+
expectTypeOf(request.data.name).toEqualTypeOf<string>()
|
|
211
|
+
expectTypeOf(request.data.email).toEqualTypeOf<string>()
|
|
212
|
+
return {
|
|
213
|
+
id: '1',
|
|
214
|
+
name: request.data.name,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
expectTypeOf(UserController).toBeConstructibleWith()
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('Multipart decorator', () => {
|
|
224
|
+
test('should enforce correct parameter type', () => {
|
|
225
|
+
@Controller()
|
|
226
|
+
class FileController {
|
|
227
|
+
@Multipart(uploadFileEndpoint)
|
|
228
|
+
async uploadFile(
|
|
229
|
+
request: MultipartParams<typeof uploadFileEndpoint>,
|
|
230
|
+
): MultipartResult<typeof uploadFileEndpoint> {
|
|
231
|
+
// TypeScript should infer:
|
|
232
|
+
// - request.data.file: File
|
|
233
|
+
// - request.data.description: string
|
|
234
|
+
expectTypeOf(request.data.file).toEqualTypeOf<File>()
|
|
235
|
+
expectTypeOf(request.data.description).toEqualTypeOf<string>()
|
|
236
|
+
return {
|
|
237
|
+
id: '1',
|
|
238
|
+
name: 'uploaded.jpg',
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
expectTypeOf(FileController).toBeConstructibleWith()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('should reject incorrect parameter type', () => {
|
|
247
|
+
@Controller()
|
|
248
|
+
class FileController {
|
|
249
|
+
// @ts-expect-error - wrong parameter type
|
|
250
|
+
@Multipart(uploadFileEndpoint)
|
|
251
|
+
async uploadFile(request: {
|
|
252
|
+
wrong: string
|
|
253
|
+
}): MultipartResult<typeof uploadFileEndpoint> {
|
|
254
|
+
return { id: '1', name: 'test.jpg' }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
describe('Stream decorator', () => {
|
|
261
|
+
test('should enforce correct parameter type', () => {
|
|
262
|
+
@Controller()
|
|
263
|
+
class FileController {
|
|
264
|
+
@Stream(downloadFileEndpoint)
|
|
265
|
+
async downloadFile(
|
|
266
|
+
request: StreamParams<typeof downloadFileEndpoint>,
|
|
267
|
+
reply: any,
|
|
268
|
+
): Promise<void> {
|
|
269
|
+
// TypeScript should infer:
|
|
270
|
+
// - request.urlParams.fileId: string | number
|
|
271
|
+
// - request.params.page: number
|
|
272
|
+
// - request.params.limit: number
|
|
273
|
+
expectTypeOf(request.urlParams.fileId).toEqualTypeOf<
|
|
274
|
+
string | number
|
|
275
|
+
>()
|
|
276
|
+
expectTypeOf(request.params.page).toEqualTypeOf<number>()
|
|
277
|
+
expectTypeOf(request.params.limit).toEqualTypeOf<number>()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
expectTypeOf(FileController).toBeConstructibleWith()
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('should require reply parameter', () => {
|
|
285
|
+
@Controller()
|
|
286
|
+
class FileController {
|
|
287
|
+
// @ts-expect-error - missing reply parameter
|
|
288
|
+
@Stream(downloadFileEndpoint)
|
|
289
|
+
async downloadFile(
|
|
290
|
+
request: StreamParams<typeof downloadFileEndpoint>,
|
|
291
|
+
): Promise<void> {
|
|
292
|
+
// Stream methods must have reply parameter
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
describe('UseGuards decorator', () => {
|
|
299
|
+
test('should work on class level', () => {
|
|
300
|
+
@Controller()
|
|
301
|
+
@UseGuards(AuthGuard)
|
|
302
|
+
class ProtectedController {
|
|
303
|
+
@Endpoint(getUserEndpoint)
|
|
304
|
+
async getUser(
|
|
305
|
+
request: EndpointParams<typeof getUserEndpoint>,
|
|
306
|
+
): EndpointResult<typeof getUserEndpoint> {
|
|
307
|
+
return { id: '1', name: 'John' }
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
expectTypeOf(ProtectedController).toBeConstructibleWith()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('should work on method level', () => {
|
|
315
|
+
@Controller()
|
|
316
|
+
class UserController {
|
|
317
|
+
@Endpoint(getUserEndpoint)
|
|
318
|
+
@UseGuards(AuthGuard)
|
|
319
|
+
async getUser(
|
|
320
|
+
request: EndpointParams<typeof getUserEndpoint>,
|
|
321
|
+
): EndpointResult<typeof getUserEndpoint> {
|
|
322
|
+
return { id: '1', name: 'John' }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
expectTypeOf(UserController).toBeConstructibleWith()
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
describe('Header decorator', () => {
|
|
331
|
+
test('should work with string value', () => {
|
|
332
|
+
@Controller()
|
|
333
|
+
class UserController {
|
|
334
|
+
@Endpoint(getUserEndpoint)
|
|
335
|
+
@Header('Cache-Control', 'max-age=3600')
|
|
336
|
+
async getUser(
|
|
337
|
+
request: EndpointParams<typeof getUserEndpoint>,
|
|
338
|
+
): EndpointResult<typeof getUserEndpoint> {
|
|
339
|
+
return { id: '1', name: 'John' }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
expectTypeOf(UserController).toBeConstructibleWith()
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('should work with number value', () => {
|
|
347
|
+
@Controller()
|
|
348
|
+
class UserController {
|
|
349
|
+
@Endpoint(getUserEndpoint)
|
|
350
|
+
@Header('Content-Length', 100)
|
|
351
|
+
async getUser(
|
|
352
|
+
request: EndpointParams<typeof getUserEndpoint>,
|
|
353
|
+
): EndpointResult<typeof getUserEndpoint> {
|
|
354
|
+
return { id: '1', name: 'John' }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
expectTypeOf(UserController).toBeConstructibleWith()
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
describe('HttpCode decorator', () => {
|
|
363
|
+
test('should accept valid status codes', () => {
|
|
364
|
+
@Controller()
|
|
365
|
+
class UserController {
|
|
366
|
+
@Endpoint(createUserEndpoint)
|
|
367
|
+
@HttpCode(201)
|
|
368
|
+
async createUser(
|
|
369
|
+
request: EndpointParams<typeof createUserEndpoint>,
|
|
370
|
+
): EndpointResult<typeof createUserEndpoint> {
|
|
371
|
+
return { id: '1', name: request.data.name }
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
expectTypeOf(UserController).toBeConstructibleWith()
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
describe('Integration test - full controller', () => {
|
|
380
|
+
test('should work with all decorators together', () => {
|
|
381
|
+
@Controller({
|
|
382
|
+
guards: [AuthGuard],
|
|
383
|
+
})
|
|
384
|
+
@UseGuards(AuthGuard)
|
|
385
|
+
class UserController {
|
|
386
|
+
@Endpoint(getUserEndpoint)
|
|
387
|
+
@UseGuards(AuthGuard)
|
|
388
|
+
@Header('Cache-Control', 'max-age=3600')
|
|
389
|
+
@HttpCode(200)
|
|
390
|
+
async getUser(
|
|
391
|
+
request: EndpointParams<typeof getUserEndpoint>,
|
|
392
|
+
): EndpointResult<typeof getUserEndpoint> {
|
|
393
|
+
return {
|
|
394
|
+
id: request.urlParams.userId.toString(),
|
|
395
|
+
name: 'John',
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
@Endpoint(createUserEndpoint)
|
|
400
|
+
@HttpCode(201)
|
|
401
|
+
async createUser(
|
|
402
|
+
request: EndpointParams<typeof createUserEndpoint>,
|
|
403
|
+
): EndpointResult<typeof createUserEndpoint> {
|
|
404
|
+
return {
|
|
405
|
+
id: '1',
|
|
406
|
+
name: request.data.name,
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
@Module({
|
|
412
|
+
controllers: [UserController],
|
|
413
|
+
})
|
|
414
|
+
class AppModule {}
|
|
415
|
+
|
|
416
|
+
expectTypeOf(UserController).toBeConstructibleWith()
|
|
417
|
+
expectTypeOf(AppModule).toBeConstructibleWith()
|
|
418
|
+
})
|
|
419
|
+
})
|
|
420
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"experimentalDecorators": true,
|
|
5
|
+
"emitDecoratorMetadata": false,
|
|
6
|
+
"outDir": "../../../dist/legacy-compat/__type-tests__"
|
|
7
|
+
},
|
|
8
|
+
"include": ["**/*.spec-d.mts"],
|
|
9
|
+
"exclude": [],
|
|
10
|
+
"references": [
|
|
11
|
+
{
|
|
12
|
+
"path": "../../../tsconfig.lib.json"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|