@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.
@@ -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
  /**
@@ -138,7 +138,7 @@ describe('createServiceClient', () => {
138
138
  }),
139
139
  ),
140
140
  );
141
- globalThis.fetch = mockFetch;
141
+ globalThis.fetch = mockFetch as unknown as typeof fetch;
142
142
  });
143
143
 
144
144
  afterEach(() => {
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 */