@buenojs/bueno 0.8.4 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +264 -17
- package/dist/cli/{index.js → bin.js} +413 -332
- package/dist/container/index.js +273 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/graphql/index.js +2156 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +9694 -5047
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3411 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +795 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/llms.txt +231 -0
- package/package.json +125 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/ARCHITECTURE.md +3 -3
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +294 -232
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +37 -18
- package/src/cli/templates/database/mysql.ts +3 -3
- package/src/cli/templates/database/none.ts +2 -2
- package/src/cli/templates/database/postgresql.ts +3 -3
- package/src/cli/templates/database/sqlite.ts +3 -3
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +33 -15
- package/src/cli/templates/frontend/none.ts +2 -2
- package/src/cli/templates/frontend/react.ts +18 -18
- package/src/cli/templates/frontend/solid.ts +15 -15
- package/src/cli/templates/frontend/svelte.ts +17 -17
- package/src/cli/templates/frontend/vue.ts +15 -15
- package/src/cli/templates/generators/index.ts +29 -29
- package/src/cli/templates/generators/types.ts +21 -21
- package/src/cli/templates/index.ts +6 -6
- package/src/cli/templates/project/api.ts +37 -36
- package/src/cli/templates/project/default.ts +25 -25
- package/src/cli/templates/project/fullstack.ts +28 -26
- package/src/cli/templates/project/index.ts +55 -16
- package/src/cli/templates/project/minimal.ts +17 -12
- package/src/cli/templates/project/types.ts +10 -5
- package/src/cli/templates/project/website.ts +15 -15
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -3
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +14 -8
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +566 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/graphql/built-in-engine.ts +598 -0
- package/src/graphql/context-builder.ts +110 -0
- package/src/graphql/decorators.ts +358 -0
- package/src/graphql/execution-pipeline.ts +227 -0
- package/src/graphql/graphql-module.ts +563 -0
- package/src/graphql/index.ts +101 -0
- package/src/graphql/metadata.ts +237 -0
- package/src/graphql/schema-builder.ts +319 -0
- package/src/graphql/subscription-handler.ts +283 -0
- package/src/graphql/types.ts +324 -0
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +182 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +457 -299
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/cli.test.ts +19 -19
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/cli.test.ts +1 -1
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/graphql.test.ts +991 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability Types
|
|
3
|
+
*
|
|
4
|
+
* Core interfaces for the error tracking and observability integration layer.
|
|
5
|
+
* Implement ErrorReporter to integrate with Sentry, Bugsnag, Datadog, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============= Breadcrumbs =============
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A single breadcrumb entry recording an event that occurred before an error
|
|
12
|
+
*/
|
|
13
|
+
export interface BreadcrumbEntry {
|
|
14
|
+
/** When this breadcrumb was recorded */
|
|
15
|
+
timestamp: Date;
|
|
16
|
+
/** Category of event */
|
|
17
|
+
type: "http" | "log" | "navigation" | "custom";
|
|
18
|
+
/** Severity level */
|
|
19
|
+
level: "debug" | "info" | "warning" | "error";
|
|
20
|
+
/** Human-readable description of the event */
|
|
21
|
+
message: string;
|
|
22
|
+
/** Optional structured data for context */
|
|
23
|
+
data?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============= Error Event =============
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Request metadata captured at error time
|
|
30
|
+
*/
|
|
31
|
+
export interface ErrorRequestContext {
|
|
32
|
+
/** HTTP method */
|
|
33
|
+
method: string;
|
|
34
|
+
/** Request path */
|
|
35
|
+
path: string;
|
|
36
|
+
/** Relevant request headers (sanitized) */
|
|
37
|
+
headers: Record<string, string>;
|
|
38
|
+
/** Client IP address */
|
|
39
|
+
ip: string;
|
|
40
|
+
/** User-Agent header if present */
|
|
41
|
+
userAgent?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* User identity snapshot (sourced from context.get('user'))
|
|
46
|
+
*/
|
|
47
|
+
export interface ErrorUserContext {
|
|
48
|
+
id?: string | number;
|
|
49
|
+
email?: string;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A captured error event with full context, ready for the reporter
|
|
55
|
+
*/
|
|
56
|
+
export interface ErrorEvent {
|
|
57
|
+
/** Unique ID for this error event (for deduplication) */
|
|
58
|
+
id: string;
|
|
59
|
+
/** When the error occurred */
|
|
60
|
+
timestamp: Date;
|
|
61
|
+
/** The original Error object */
|
|
62
|
+
error: Error;
|
|
63
|
+
/** Severity level */
|
|
64
|
+
level: "fatal" | "error" | "warning";
|
|
65
|
+
/** HTTP request context (undefined for non-HTTP errors) */
|
|
66
|
+
request?: ErrorRequestContext;
|
|
67
|
+
/** W3C trace ID (from traceparent header or generated) */
|
|
68
|
+
traceId?: string;
|
|
69
|
+
/** W3C span ID */
|
|
70
|
+
spanId?: string;
|
|
71
|
+
/** Authenticated user at time of error */
|
|
72
|
+
user?: ErrorUserContext;
|
|
73
|
+
/** Recent events leading up to this error */
|
|
74
|
+
breadcrumbs: BreadcrumbEntry[];
|
|
75
|
+
/** Custom tags for categorization */
|
|
76
|
+
tags?: Record<string, string>;
|
|
77
|
+
/** Any additional context */
|
|
78
|
+
extra?: Record<string, unknown>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A captured message event (non-error) for the reporter
|
|
83
|
+
*/
|
|
84
|
+
export interface MessageEvent {
|
|
85
|
+
/** Unique ID */
|
|
86
|
+
id: string;
|
|
87
|
+
/** When the message was captured */
|
|
88
|
+
timestamp: Date;
|
|
89
|
+
/** The message text */
|
|
90
|
+
message: string;
|
|
91
|
+
/** Severity */
|
|
92
|
+
level: "debug" | "info" | "warning" | "error";
|
|
93
|
+
/** Optional extra context */
|
|
94
|
+
extra?: Record<string, unknown>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============= ErrorReporter Interface =============
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Implement this interface to report errors to your platform (Sentry, Bugsnag, etc.).
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* class SentryReporter implements ErrorReporter {
|
|
105
|
+
* captureError(event: ErrorEvent) {
|
|
106
|
+
* Sentry.withScope(scope => {
|
|
107
|
+
* if (event.user) scope.setUser(event.user);
|
|
108
|
+
* if (event.traceId) scope.setTag('traceId', event.traceId);
|
|
109
|
+
* Sentry.captureException(event.error);
|
|
110
|
+
* });
|
|
111
|
+
* }
|
|
112
|
+
* async flush() { await Sentry.flush(2000); }
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export interface ErrorReporter {
|
|
117
|
+
/**
|
|
118
|
+
* Report an error event. Called asynchronously (non-blocking).
|
|
119
|
+
* Throw errors only if truly unrecoverable — they are caught and logged.
|
|
120
|
+
*/
|
|
121
|
+
captureError(event: ErrorEvent): void | Promise<void>;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Report a non-error message. Optional.
|
|
125
|
+
*/
|
|
126
|
+
captureMessage?(event: MessageEvent): void | Promise<void>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Flush pending events. Called on application shutdown.
|
|
130
|
+
* Use this to ensure all buffered events are sent before process exit.
|
|
131
|
+
*/
|
|
132
|
+
flush?(): Promise<void>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============= Module Options =============
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Options for ObservabilityModule.forRoot()
|
|
139
|
+
*/
|
|
140
|
+
export interface ObservabilityOptions {
|
|
141
|
+
/**
|
|
142
|
+
* The reporter that receives all captured error events.
|
|
143
|
+
* Required — without a reporter, no events are sent anywhere.
|
|
144
|
+
*/
|
|
145
|
+
reporter: ErrorReporter;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Maximum number of breadcrumbs to retain in the ring buffer.
|
|
149
|
+
* Older breadcrumbs are evicted when the buffer is full.
|
|
150
|
+
* @default 20
|
|
151
|
+
*/
|
|
152
|
+
breadcrumbsSize?: number;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Error classes to suppress — events with these types are not reported.
|
|
156
|
+
* Useful for suppressing expected errors like 404s or 401s.
|
|
157
|
+
*
|
|
158
|
+
* @example ignoreErrors: [NotFoundError, UnauthorizedError]
|
|
159
|
+
*/
|
|
160
|
+
ignoreErrors?: Array<new (...args: unknown[]) => Error>;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* HTTP status codes to suppress.
|
|
164
|
+
* Errors whose statusCode property matches are not reported.
|
|
165
|
+
* @example ignoreStatusCodes: [404, 401]
|
|
166
|
+
*/
|
|
167
|
+
ignoreStatusCodes?: number[];
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Custom tags attached to every error event.
|
|
171
|
+
* @example tags: { environment: 'production', version: '1.2.3' }
|
|
172
|
+
*/
|
|
173
|
+
tags?: Record<string, string>;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Also capture unhandled promise rejections and uncaught exceptions
|
|
177
|
+
* at the process level (outside the HTTP request pipeline).
|
|
178
|
+
* @default false
|
|
179
|
+
*/
|
|
180
|
+
captureUnhandled?: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============= Config =============
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Configuration values that can be set via environment variables.
|
|
187
|
+
* These are applied as defaults; ObservabilityOptions take precedence.
|
|
188
|
+
*/
|
|
189
|
+
export interface ObservabilityConfig {
|
|
190
|
+
/** Enable/disable the observability system */
|
|
191
|
+
enabled?: boolean;
|
|
192
|
+
/** Maximum breadcrumb ring buffer size */
|
|
193
|
+
breadcrumbsSize?: number;
|
|
194
|
+
/** HTTP status codes to ignore (comma-separated in env) */
|
|
195
|
+
ignoreStatusCodes?: number[];
|
|
196
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
ApiTags,
|
|
4
|
+
ApiBearerAuth,
|
|
5
|
+
ApiBasicAuth,
|
|
6
|
+
ApiApiKey,
|
|
7
|
+
ApiExcludeController,
|
|
8
|
+
ApiOperation,
|
|
9
|
+
ApiResponse,
|
|
10
|
+
ApiParam,
|
|
11
|
+
ApiQuery,
|
|
12
|
+
ApiHeader,
|
|
13
|
+
ApiBody,
|
|
14
|
+
ApiExcludeEndpoint,
|
|
15
|
+
ApiProperty,
|
|
16
|
+
ApiPropertyOptional,
|
|
17
|
+
} from '../decorators';
|
|
18
|
+
import { getApiMetadata, getApiMethodMetadata, getApiPropertyMetadata } from '../metadata';
|
|
19
|
+
|
|
20
|
+
// ============= Fixtures =============
|
|
21
|
+
|
|
22
|
+
class TestDto {}
|
|
23
|
+
|
|
24
|
+
// ============= Class-Level Decorators =============
|
|
25
|
+
|
|
26
|
+
describe('Class-Level Decorators', () => {
|
|
27
|
+
describe('@ApiTags', () => {
|
|
28
|
+
test('should store tags on class constructor', () => {
|
|
29
|
+
@ApiTags('users', 'admin')
|
|
30
|
+
class UserController {}
|
|
31
|
+
|
|
32
|
+
const tags = getApiMetadata<string[]>(UserController, 'api:tags');
|
|
33
|
+
expect(tags).toEqual(['users', 'admin']);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should merge multiple @ApiTags decorators', () => {
|
|
37
|
+
@ApiTags('users')
|
|
38
|
+
@ApiTags('admin')
|
|
39
|
+
class UserController {}
|
|
40
|
+
|
|
41
|
+
const tags = getApiMetadata<string[]>(UserController, 'api:tags');
|
|
42
|
+
expect(tags?.includes('users')).toBe(true);
|
|
43
|
+
expect(tags?.includes('admin')).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('@ApiBearerAuth', () => {
|
|
48
|
+
test('should store bearer auth security on class', () => {
|
|
49
|
+
@ApiBearerAuth()
|
|
50
|
+
class ApiController {}
|
|
51
|
+
|
|
52
|
+
const security = getApiMetadata<Record<string, string[]>[]>(ApiController, 'api:security');
|
|
53
|
+
expect(security).toBeDefined();
|
|
54
|
+
expect(security?.length).toBeGreaterThan(0);
|
|
55
|
+
expect(security?.[0]).toHaveProperty('bearer');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('should use custom bearer name', () => {
|
|
59
|
+
@ApiBearerAuth('jwt')
|
|
60
|
+
class ApiController {}
|
|
61
|
+
|
|
62
|
+
const security = getApiMetadata<Record<string, string[]>[]>(ApiController, 'api:security');
|
|
63
|
+
expect(security?.[0]).toHaveProperty('jwt');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('@ApiBasicAuth', () => {
|
|
68
|
+
test('should store basic auth security on class', () => {
|
|
69
|
+
@ApiBasicAuth()
|
|
70
|
+
class ApiController {}
|
|
71
|
+
|
|
72
|
+
const security = getApiMetadata<Record<string, string[]>[]>(ApiController, 'api:security');
|
|
73
|
+
expect(security).toBeDefined();
|
|
74
|
+
expect(security?.[0]).toHaveProperty('basic');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('@ApiApiKey', () => {
|
|
79
|
+
test('should store API key security on class', () => {
|
|
80
|
+
@ApiApiKey({ in: 'header', name: 'X-API-Key' })
|
|
81
|
+
class ApiController {}
|
|
82
|
+
|
|
83
|
+
const security = getApiMetadata<Record<string, string[]>[]>(ApiController, 'api:security');
|
|
84
|
+
expect(security).toBeDefined();
|
|
85
|
+
expect(security?.[0]).toHaveProperty('api_key');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('@ApiExcludeController', () => {
|
|
90
|
+
test('should mark controller as excluded', () => {
|
|
91
|
+
@ApiExcludeController()
|
|
92
|
+
class HiddenController {}
|
|
93
|
+
|
|
94
|
+
const excluded = getApiMetadata<boolean>(HiddenController, 'api:exclude');
|
|
95
|
+
expect(excluded).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ============= Method-Level Decorators =============
|
|
101
|
+
// Note: metadata is stored as 'api:<key>:<methodName>' on the prototype
|
|
102
|
+
|
|
103
|
+
describe('Method-Level Decorators', () => {
|
|
104
|
+
describe('@ApiOperation', () => {
|
|
105
|
+
test('should store operation metadata keyed by method name', () => {
|
|
106
|
+
class UserController {
|
|
107
|
+
@ApiOperation({ summary: 'Get all users', description: 'Retrieve a list of users' })
|
|
108
|
+
getAll() {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const meta = getApiMethodMetadata<any>(UserController.prototype, 'api:operation:getAll');
|
|
112
|
+
expect(meta).toBeDefined();
|
|
113
|
+
expect(meta?.summary).toBe('Get all users');
|
|
114
|
+
expect(meta?.description).toBe('Retrieve a list of users');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('should store deprecated flag', () => {
|
|
118
|
+
class UserController {
|
|
119
|
+
@ApiOperation({ summary: 'Old endpoint', deprecated: true })
|
|
120
|
+
oldMethod() {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const meta = getApiMethodMetadata<any>(UserController.prototype, 'api:operation:oldMethod');
|
|
124
|
+
expect(meta?.deprecated).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('@ApiResponse', () => {
|
|
129
|
+
test('should store single response keyed by method name', () => {
|
|
130
|
+
class UserController {
|
|
131
|
+
@ApiResponse({ status: 200, description: 'Success' })
|
|
132
|
+
getUser() {}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const responses = getApiMethodMetadata<any[]>(UserController.prototype, 'api:responses:getUser');
|
|
136
|
+
expect(responses).toBeDefined();
|
|
137
|
+
expect(responses?.length).toBe(1);
|
|
138
|
+
expect(responses?.[0].status).toBe(200);
|
|
139
|
+
expect(responses?.[0].description).toBe('Success');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('should accumulate multiple @ApiResponse decorators', () => {
|
|
143
|
+
class UserController {
|
|
144
|
+
@ApiResponse({ status: 200, description: 'Success' })
|
|
145
|
+
@ApiResponse({ status: 404, description: 'Not found' })
|
|
146
|
+
getUser() {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const responses = getApiMethodMetadata<any[]>(UserController.prototype, 'api:responses:getUser');
|
|
150
|
+
expect(responses?.length).toBe(2);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('@ApiParam', () => {
|
|
155
|
+
test('should store path parameter keyed by method name', () => {
|
|
156
|
+
class UserController {
|
|
157
|
+
@ApiParam({ name: 'id', type: 'string', description: 'User ID' })
|
|
158
|
+
getUser() {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const params = getApiMethodMetadata<any[]>(UserController.prototype, 'api:params:getUser');
|
|
162
|
+
expect(params).toBeDefined();
|
|
163
|
+
expect(params?.length).toBe(1);
|
|
164
|
+
expect(params?.[0].name).toBe('id');
|
|
165
|
+
expect(params?.[0].type).toBe('string');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('should accumulate multiple @ApiParam decorators', () => {
|
|
169
|
+
class UserController {
|
|
170
|
+
@ApiParam({ name: 'userId', type: 'string' })
|
|
171
|
+
@ApiParam({ name: 'postId', type: 'string' })
|
|
172
|
+
getPost() {}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const params = getApiMethodMetadata<any[]>(UserController.prototype, 'api:params:getPost');
|
|
176
|
+
expect(params?.length).toBe(2);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('@ApiQuery', () => {
|
|
181
|
+
test('should store query parameter keyed by method name', () => {
|
|
182
|
+
class UserController {
|
|
183
|
+
@ApiQuery({ name: 'page', type: 'number', required: false })
|
|
184
|
+
getUsers() {}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const query = getApiMethodMetadata<any[]>(UserController.prototype, 'api:query:getUsers');
|
|
188
|
+
expect(query).toBeDefined();
|
|
189
|
+
expect(query?.length).toBe(1);
|
|
190
|
+
expect(query?.[0].name).toBe('page');
|
|
191
|
+
expect(query?.[0].required).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('@ApiHeader', () => {
|
|
196
|
+
test('should store header keyed by method name', () => {
|
|
197
|
+
class UserController {
|
|
198
|
+
@ApiHeader({ name: 'X-Request-ID', description: 'Request ID', required: true })
|
|
199
|
+
create() {}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const headers = getApiMethodMetadata<any[]>(UserController.prototype, 'api:headers:create');
|
|
203
|
+
expect(headers).toBeDefined();
|
|
204
|
+
expect(headers?.length).toBe(1);
|
|
205
|
+
expect(headers?.[0].name).toBe('X-Request-ID');
|
|
206
|
+
expect(headers?.[0].required).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('@ApiBody', () => {
|
|
211
|
+
test('should store request body keyed by method name', () => {
|
|
212
|
+
class UserController {
|
|
213
|
+
@ApiBody({ type: TestDto, description: 'User data' })
|
|
214
|
+
create() {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const body = getApiMethodMetadata<any>(UserController.prototype, 'api:body:create');
|
|
218
|
+
expect(body).toBeDefined();
|
|
219
|
+
expect(body?.type).toBe(TestDto);
|
|
220
|
+
expect(body?.description).toBe('User data');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('@ApiExcludeEndpoint', () => {
|
|
225
|
+
test('should mark endpoint as excluded keyed by method name', () => {
|
|
226
|
+
class UserController {
|
|
227
|
+
@ApiExcludeEndpoint()
|
|
228
|
+
internalMethod() {}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const excluded = getApiMethodMetadata<boolean>(
|
|
232
|
+
UserController.prototype,
|
|
233
|
+
'api:exclude:internalMethod',
|
|
234
|
+
);
|
|
235
|
+
expect(excluded).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ============= Property-Level Decorators (for DTOs) =============
|
|
241
|
+
|
|
242
|
+
describe('Property-Level Decorators (DTOs)', () => {
|
|
243
|
+
describe('@ApiProperty', () => {
|
|
244
|
+
test('should store property with required=true by default', () => {
|
|
245
|
+
class UserDto {
|
|
246
|
+
@ApiProperty({ description: 'User email' })
|
|
247
|
+
email!: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const meta = getApiPropertyMetadata<any>(UserDto.prototype, 'email');
|
|
251
|
+
expect(meta?.description).toBe('User email');
|
|
252
|
+
expect(meta?.required).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('should store type information', () => {
|
|
256
|
+
class UserDto {
|
|
257
|
+
@ApiProperty({ type: 'string' })
|
|
258
|
+
name!: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const meta = getApiPropertyMetadata<any>(UserDto.prototype, 'name');
|
|
262
|
+
expect(meta?.type).toBe('string');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('should store validation constraints', () => {
|
|
266
|
+
class UserDto {
|
|
267
|
+
@ApiProperty({ minLength: 2, maxLength: 100, pattern: '^[a-zA-Z]+$' })
|
|
268
|
+
name!: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const meta = getApiPropertyMetadata<any>(UserDto.prototype, 'name');
|
|
272
|
+
expect(meta?.minLength).toBe(2);
|
|
273
|
+
expect(meta?.maxLength).toBe(100);
|
|
274
|
+
expect(meta?.pattern).toBe('^[a-zA-Z]+$');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('should store numeric constraints', () => {
|
|
278
|
+
class UserDto {
|
|
279
|
+
@ApiProperty({ minimum: 0, maximum: 150 })
|
|
280
|
+
age!: number;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const meta = getApiPropertyMetadata<any>(UserDto.prototype, 'age');
|
|
284
|
+
expect(meta?.minimum).toBe(0);
|
|
285
|
+
expect(meta?.maximum).toBe(150);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('should store enum values', () => {
|
|
289
|
+
class UserDto {
|
|
290
|
+
@ApiProperty({ enum: ['active', 'inactive', 'pending'] })
|
|
291
|
+
status!: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const meta = getApiPropertyMetadata<any>(UserDto.prototype, 'status');
|
|
295
|
+
expect(meta?.enum).toEqual(['active', 'inactive', 'pending']);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('should store examples and defaults', () => {
|
|
299
|
+
class UserDto {
|
|
300
|
+
@ApiProperty({ example: 'john@example.com', default: 'user@example.com' })
|
|
301
|
+
email!: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const meta = getApiPropertyMetadata<any>(UserDto.prototype, 'email');
|
|
305
|
+
expect(meta?.example).toBe('john@example.com');
|
|
306
|
+
expect(meta?.default).toBe('user@example.com');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('@ApiPropertyOptional', () => {
|
|
311
|
+
test('should store property with required=false', () => {
|
|
312
|
+
class UserDto {
|
|
313
|
+
@ApiPropertyOptional({ description: 'Optional middle name' })
|
|
314
|
+
middleName?: string;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const meta = getApiPropertyMetadata<any>(UserDto.prototype, 'middleName');
|
|
318
|
+
expect(meta?.description).toBe('Optional middle name');
|
|
319
|
+
expect(meta?.required).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('should accept same options as @ApiProperty except required', () => {
|
|
323
|
+
class UserDto {
|
|
324
|
+
@ApiPropertyOptional({ minLength: 1, maxLength: 50, example: 'Smith' })
|
|
325
|
+
suffix?: string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const meta = getApiPropertyMetadata<any>(UserDto.prototype, 'suffix');
|
|
329
|
+
expect(meta?.minLength).toBe(1);
|
|
330
|
+
expect(meta?.maxLength).toBe(50);
|
|
331
|
+
expect(meta?.example).toBe('Smith');
|
|
332
|
+
expect(meta?.required).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|