@onebun/core 0.1.24 → 0.2.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/package.json +8 -8
- package/src/application/application.test.ts +492 -19
- package/src/application/application.ts +490 -358
- package/src/decorators/decorators.test.ts +139 -0
- package/src/decorators/decorators.ts +127 -0
- package/src/docs-examples.test.ts +670 -71
- package/src/file/index.ts +8 -0
- package/src/file/onebun-file.test.ts +315 -0
- package/src/file/onebun-file.ts +304 -0
- package/src/index.ts +13 -0
- package/src/module/controller.ts +7 -3
- package/src/queue/docs-examples.test.ts +86 -0
- package/src/service-client/service-client.test.ts +1 -1
- package/src/types.ts +45 -2
- package/src/validation/schemas.test.ts +0 -2
- package/src/websocket/ws-base-gateway.ts +2 -2
- package/src/websocket/ws-handler.ts +4 -3
- package/src/websocket/ws.types.ts +1 -1
|
@@ -50,6 +50,65 @@ import {
|
|
|
50
50
|
hasQueueDecorators,
|
|
51
51
|
} from './index';
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* @source docs/api/queue.md#setup
|
|
55
|
+
*/
|
|
56
|
+
describe('Setup Section Examples (docs/api/queue.md)', () => {
|
|
57
|
+
it('should register controller with queue decorators in module controllers', () => {
|
|
58
|
+
// From docs/api/queue.md: Registering Controllers with Queue Decorators
|
|
59
|
+
class OrderProcessor {
|
|
60
|
+
@Subscribe('orders.created')
|
|
61
|
+
async handleOrderCreated(message: Message<{ orderId: string }>) {
|
|
62
|
+
expect(message.data.orderId).toBeDefined();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Cron(CronExpression.EVERY_HOUR, { pattern: 'cleanup.expired' })
|
|
66
|
+
getCleanupData() {
|
|
67
|
+
return { timestamp: Date.now() };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Verify decorators are registered and auto-discoverable
|
|
72
|
+
expect(hasQueueDecorators(OrderProcessor)).toBe(true);
|
|
73
|
+
|
|
74
|
+
const subscriptions = getSubscribeMetadata(OrderProcessor);
|
|
75
|
+
expect(subscriptions.length).toBe(1);
|
|
76
|
+
expect(subscriptions[0].pattern).toBe('orders.created');
|
|
77
|
+
|
|
78
|
+
const cronJobs = getCronMetadata(OrderProcessor);
|
|
79
|
+
expect(cronJobs.length).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should support error handling with manual ack mode', () => {
|
|
83
|
+
// From docs/api/queue.md: Error Handling in Handlers
|
|
84
|
+
class ErrorHandlingProcessor {
|
|
85
|
+
@Subscribe('orders.created', {
|
|
86
|
+
ackMode: 'manual',
|
|
87
|
+
retry: { attempts: 3, backoff: 'exponential', delay: 1000 },
|
|
88
|
+
})
|
|
89
|
+
async handleOrder(message: Message<{ orderId: string }>) {
|
|
90
|
+
try {
|
|
91
|
+
// process order
|
|
92
|
+
await message.ack();
|
|
93
|
+
} catch {
|
|
94
|
+
if (message.attempt && message.attempt >= (message.maxAttempts || 3)) {
|
|
95
|
+
await message.ack();
|
|
96
|
+
} else {
|
|
97
|
+
await message.nack(true);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const subscriptions = getSubscribeMetadata(ErrorHandlingProcessor);
|
|
104
|
+
expect(subscriptions.length).toBe(1);
|
|
105
|
+
expect(subscriptions[0].options?.ackMode).toBe('manual');
|
|
106
|
+
expect(subscriptions[0].options?.retry?.attempts).toBe(3);
|
|
107
|
+
expect(subscriptions[0].options?.retry?.backoff).toBe('exponential');
|
|
108
|
+
expect(subscriptions[0].options?.retry?.delay).toBe(1000);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
53
112
|
/**
|
|
54
113
|
* @source docs/api/queue.md#quick-start
|
|
55
114
|
*/
|
|
@@ -86,6 +145,33 @@ describe('Quick Start Example (docs/api/queue.md)', () => {
|
|
|
86
145
|
expect(cronJobs[0].expression).toBe(CronExpression.EVERY_HOUR);
|
|
87
146
|
expect(cronJobs[0].options.pattern).toBe('cleanup.expired');
|
|
88
147
|
});
|
|
148
|
+
|
|
149
|
+
it('should define service with interval decorator', () => {
|
|
150
|
+
// From docs/api/queue.md: Quick Start - Interval example
|
|
151
|
+
class EventProcessor {
|
|
152
|
+
@Subscribe('orders.created')
|
|
153
|
+
async handleOrderCreated(message: Message<{ orderId: number }>) {
|
|
154
|
+
expect(message.data.orderId).toBeDefined();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@Cron(CronExpression.EVERY_HOUR, { pattern: 'cleanup.expired' })
|
|
158
|
+
getCleanupData() {
|
|
159
|
+
return { timestamp: Date.now() };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@Interval(30000, { pattern: 'metrics.collect' })
|
|
163
|
+
getMetricsData() {
|
|
164
|
+
return { cpu: process.cpuUsage() };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
expect(hasQueueDecorators(EventProcessor)).toBe(true);
|
|
169
|
+
|
|
170
|
+
const intervals = getIntervalMetadata(EventProcessor);
|
|
171
|
+
expect(intervals.length).toBe(1);
|
|
172
|
+
expect(intervals[0].milliseconds).toBe(30000);
|
|
173
|
+
expect(intervals[0].options.pattern).toBe('metrics.collect');
|
|
174
|
+
});
|
|
89
175
|
});
|
|
90
176
|
|
|
91
177
|
/**
|
package/src/types.ts
CHANGED
|
@@ -3,6 +3,21 @@ import type { Effect, Layer } from 'effect';
|
|
|
3
3
|
|
|
4
4
|
import type { Logger, LoggerOptions } from '@onebun/logger';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* HTTP Request type used in OneBun controllers.
|
|
8
|
+
* Extends standard Web API Request with:
|
|
9
|
+
* - `.cookies` (CookieMap) for reading/setting cookies
|
|
10
|
+
* - `.params` for accessing route parameters
|
|
11
|
+
* @see https://bun.sh/docs/api/http#bunsrequest
|
|
12
|
+
*/
|
|
13
|
+
export type OneBunRequest = import('bun').BunRequest;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* HTTP Response type used in OneBun controllers.
|
|
17
|
+
* Standard Web API Response.
|
|
18
|
+
*/
|
|
19
|
+
export type OneBunResponse = Response;
|
|
20
|
+
|
|
6
21
|
/**
|
|
7
22
|
* Base interface for all OneBun services
|
|
8
23
|
*/
|
|
@@ -505,23 +520,47 @@ export enum ParamType {
|
|
|
505
520
|
QUERY = 'query',
|
|
506
521
|
BODY = 'body',
|
|
507
522
|
HEADER = 'header',
|
|
523
|
+
COOKIE = 'cookie',
|
|
508
524
|
REQUEST = 'request',
|
|
509
525
|
RESPONSE = 'response',
|
|
526
|
+
FILE = 'file',
|
|
527
|
+
FILES = 'files',
|
|
528
|
+
FORM_FIELD = 'formField',
|
|
510
529
|
}
|
|
511
530
|
|
|
512
531
|
/**
|
|
513
|
-
* Options for parameter decorators (@Query, @Header, @Body, etc.)
|
|
532
|
+
* Options for parameter decorators (@Query, @Header, @Cookie, @Body, etc.)
|
|
514
533
|
*/
|
|
515
534
|
export interface ParamDecoratorOptions {
|
|
516
535
|
/**
|
|
517
536
|
* Whether the parameter is required
|
|
518
537
|
* - @Param: always true (OpenAPI spec requirement)
|
|
519
|
-
* - @Query, @Header: false by default
|
|
538
|
+
* - @Query, @Header, @Cookie: false by default
|
|
520
539
|
* - @Body: determined from schema (accepts undefined = optional)
|
|
521
540
|
*/
|
|
522
541
|
required?: boolean;
|
|
523
542
|
}
|
|
524
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Options for file upload decorators (@UploadedFile, @UploadedFiles)
|
|
546
|
+
*/
|
|
547
|
+
export interface FileUploadOptions {
|
|
548
|
+
/** Maximum file size in bytes */
|
|
549
|
+
maxSize?: number;
|
|
550
|
+
/** Allowed MIME types, supports wildcards like 'image/*'. Use MimeType enum for convenience. */
|
|
551
|
+
mimeTypes?: string[];
|
|
552
|
+
/** Whether the file is required (default: true for @UploadedFile/@UploadedFiles) */
|
|
553
|
+
required?: boolean;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Options for multiple file upload decorator (@UploadedFiles)
|
|
558
|
+
*/
|
|
559
|
+
export interface FilesUploadOptions extends FileUploadOptions {
|
|
560
|
+
/** Maximum number of files allowed */
|
|
561
|
+
maxCount?: number;
|
|
562
|
+
}
|
|
563
|
+
|
|
525
564
|
/**
|
|
526
565
|
* Parameter metadata
|
|
527
566
|
*/
|
|
@@ -534,6 +573,10 @@ export interface ParamMetadata {
|
|
|
534
573
|
* ArkType schema for validation
|
|
535
574
|
*/
|
|
536
575
|
schema?: Type<unknown>;
|
|
576
|
+
/**
|
|
577
|
+
* File upload options (only for FILE/FILES param types)
|
|
578
|
+
*/
|
|
579
|
+
fileOptions?: FileUploadOptions & { maxCount?: number };
|
|
537
580
|
}
|
|
538
581
|
|
|
539
582
|
/**
|
|
@@ -47,7 +47,6 @@ describe('Validation Schemas', () => {
|
|
|
47
47
|
test('should create an optional schema that accepts undefined', () => {
|
|
48
48
|
const schema = optionalSchema(stringSchema());
|
|
49
49
|
expect(schema('test')).toBe('test');
|
|
50
|
-
// @ts-expect-error - Testing that optional schema accepts undefined
|
|
51
50
|
expect(schema(undefined)).toBe(undefined);
|
|
52
51
|
const invalidResult = schema(123);
|
|
53
52
|
expect(invalidResult instanceof type.errors).toBe(true);
|
|
@@ -56,7 +55,6 @@ describe('Validation Schemas', () => {
|
|
|
56
55
|
test('should work with number schema', () => {
|
|
57
56
|
const schema = optionalSchema(numberSchema());
|
|
58
57
|
expect(schema(42)).toBe(42);
|
|
59
|
-
// @ts-expect-error - Testing that optional schema accepts undefined
|
|
60
58
|
expect(schema(undefined)).toBe(undefined);
|
|
61
59
|
const invalidResult = schema('test');
|
|
62
60
|
expect(invalidResult instanceof type.errors).toBe(true);
|
|
@@ -49,7 +49,7 @@ export abstract class BaseWebSocketGateway {
|
|
|
49
49
|
protected storage: WsStorageAdapter | null = null;
|
|
50
50
|
|
|
51
51
|
/** Bun server reference */
|
|
52
|
-
protected server: Server | null = null;
|
|
52
|
+
protected server: Server<WsClientData> | null = null;
|
|
53
53
|
|
|
54
54
|
/** Unique instance ID (for multi-instance setups) */
|
|
55
55
|
protected instanceId: string = crypto.randomUUID();
|
|
@@ -63,7 +63,7 @@ export abstract class BaseWebSocketGateway {
|
|
|
63
63
|
* Called internally by the framework
|
|
64
64
|
* @internal
|
|
65
65
|
*/
|
|
66
|
-
_initialize(storage: WsStorageAdapter, server: Server): void {
|
|
66
|
+
_initialize(storage: WsStorageAdapter, server: Server<WsClientData>): void {
|
|
67
67
|
this.storage = storage;
|
|
68
68
|
this.server = server;
|
|
69
69
|
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
WebSocketApplicationOptions,
|
|
15
15
|
} from './ws.types';
|
|
16
16
|
import type { WsHandlerResponse } from './ws.types';
|
|
17
|
+
import type { OneBunRequest } from '../types';
|
|
17
18
|
import type { Server, ServerWebSocket } from 'bun';
|
|
18
19
|
|
|
19
20
|
import type { SyncLogger } from '@onebun/logger';
|
|
@@ -121,7 +122,7 @@ export class WsHandler {
|
|
|
121
122
|
/**
|
|
122
123
|
* Initialize gateways with server
|
|
123
124
|
*/
|
|
124
|
-
initializeGateways(server: Server): void {
|
|
125
|
+
initializeGateways(server: Server<WsClientData>): void {
|
|
125
126
|
for (const [_, gateway] of this.gateways) {
|
|
126
127
|
gateway.instance._initialize(this.storage, server);
|
|
127
128
|
}
|
|
@@ -182,8 +183,8 @@ export class WsHandler {
|
|
|
182
183
|
* Handle WebSocket upgrade request
|
|
183
184
|
*/
|
|
184
185
|
async handleUpgrade(
|
|
185
|
-
req: Request,
|
|
186
|
-
server: Server
|
|
186
|
+
req: OneBunRequest | Request,
|
|
187
|
+
server: Server<WsClientData>,
|
|
187
188
|
): Promise<Response | undefined> {
|
|
188
189
|
const url = new URL(req.url);
|
|
189
190
|
const path = url.pathname;
|
|
@@ -294,7 +294,7 @@ export interface WsGuard {
|
|
|
294
294
|
*/
|
|
295
295
|
export interface WsServer {
|
|
296
296
|
/** Bun server instance */
|
|
297
|
-
server: Server
|
|
297
|
+
server: Server<WsClientData>;
|
|
298
298
|
/** Publish message to a topic */
|
|
299
299
|
publish(topic: string, message: string | Buffer): void;
|
|
300
300
|
/** Get subscriber count for a topic */
|