@onebun/core 0.2.6 → 0.2.8
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 +6 -6
- package/src/application/application.test.ts +350 -7
- package/src/application/application.ts +537 -254
- package/src/application/multi-service-application.test.ts +15 -0
- package/src/application/multi-service-application.ts +2 -0
- package/src/application/multi-service.types.ts +7 -1
- package/src/decorators/decorators.ts +213 -0
- package/src/docs-examples.test.ts +386 -3
- package/src/exception-filters/exception-filters.test.ts +172 -0
- package/src/exception-filters/exception-filters.ts +129 -0
- package/src/exception-filters/http-exception.ts +22 -0
- package/src/exception-filters/index.ts +2 -0
- package/src/file/onebun-file.ts +8 -2
- package/src/http-guards/http-guards.test.ts +230 -0
- package/src/http-guards/http-guards.ts +173 -0
- package/src/http-guards/index.ts +1 -0
- package/src/index.ts +10 -0
- package/src/module/module.test.ts +78 -0
- package/src/module/module.ts +55 -7
- package/src/queue/docs-examples.test.ts +72 -12
- package/src/queue/index.ts +4 -0
- package/src/queue/queue-service-proxy.test.ts +82 -0
- package/src/queue/queue-service-proxy.ts +114 -0
- package/src/queue/types.ts +2 -2
- package/src/security/cors-middleware.ts +212 -0
- package/src/security/index.ts +19 -0
- package/src/security/rate-limit-middleware.ts +276 -0
- package/src/security/security-headers-middleware.ts +188 -0
- package/src/security/security.test.ts +285 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/testing-module.test.ts +199 -0
- package/src/testing/testing-module.ts +252 -0
- package/src/types.ts +153 -3
package/src/module/module.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
isGlobalModule,
|
|
23
23
|
registerControllerDependencies,
|
|
24
24
|
} from '../decorators/decorators';
|
|
25
|
+
import { QueueService, QueueServiceTag } from '../queue';
|
|
25
26
|
import { BaseWebSocketGateway } from '../websocket/ws-base-gateway';
|
|
26
27
|
import { isWebSocketGateway } from '../websocket/ws-decorators';
|
|
27
28
|
|
|
@@ -616,7 +617,31 @@ export class OneBunModule implements ModuleInstance {
|
|
|
616
617
|
* Resolve dependency by type (constructor function)
|
|
617
618
|
*/
|
|
618
619
|
private resolveDependencyByType(type: Function): unknown {
|
|
619
|
-
//
|
|
620
|
+
// QueueService is registered by tag (QueueServiceTag) before setup(); resolve by tag
|
|
621
|
+
if (type === QueueService) {
|
|
622
|
+
const byTag = this.serviceInstances.get(
|
|
623
|
+
QueueServiceTag as Context.Tag<unknown, unknown>,
|
|
624
|
+
);
|
|
625
|
+
if (byTag !== undefined) {
|
|
626
|
+
return byTag;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Try to find by Effect Context.Tag first.
|
|
631
|
+
// This is the primary mechanism and also makes test overrides work:
|
|
632
|
+
// TestingModule.overrideProvider(MyService).useValue(mock) registers the mock
|
|
633
|
+
// under MyService's tag, so it is found here even if mock is not instanceof MyService.
|
|
634
|
+
try {
|
|
635
|
+
const tag = getServiceTag(type as new (...args: unknown[]) => unknown);
|
|
636
|
+
const byTag = this.serviceInstances.get(tag as Context.Tag<unknown, unknown>);
|
|
637
|
+
if (byTag !== undefined) {
|
|
638
|
+
return byTag;
|
|
639
|
+
}
|
|
640
|
+
} catch {
|
|
641
|
+
// Not a @Service()-decorated class — fall through to instanceof check below
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Fallback: find service instance that matches the type by reference equality or inheritance
|
|
620
645
|
const serviceInstance = Array.from(this.serviceInstances.values()).find((instance) => {
|
|
621
646
|
if (!instance) {
|
|
622
647
|
return false;
|
|
@@ -678,11 +703,27 @@ export class OneBunModule implements ModuleInstance {
|
|
|
678
703
|
/**
|
|
679
704
|
* Setup the module and its dependencies
|
|
680
705
|
*/
|
|
706
|
+
/**
|
|
707
|
+
* Collect all descendant modules in depth-first order (leaves first).
|
|
708
|
+
* This ensures that deeply nested modules are initialized before their parents.
|
|
709
|
+
*/
|
|
710
|
+
private collectDescendantModules(): OneBunModule[] {
|
|
711
|
+
const result: OneBunModule[] = [];
|
|
712
|
+
for (const child of this.childModules) {
|
|
713
|
+
result.push(...child.collectDescendantModules());
|
|
714
|
+
result.push(child);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
|
|
681
720
|
setup(): Effect.Effect<unknown, never, void> {
|
|
721
|
+
const allDescendants = this.collectDescendantModules();
|
|
722
|
+
|
|
682
723
|
return this.callServicesOnModuleInit().pipe(
|
|
683
|
-
//
|
|
724
|
+
// Run onModuleInit for all descendant modules' services (depth-first)
|
|
684
725
|
Effect.flatMap(() =>
|
|
685
|
-
Effect.forEach(
|
|
726
|
+
Effect.forEach(allDescendants, (mod) => mod.callServicesOnModuleInit(), {
|
|
686
727
|
discard: true,
|
|
687
728
|
}),
|
|
688
729
|
),
|
|
@@ -693,18 +734,18 @@ export class OneBunModule implements ModuleInstance {
|
|
|
693
734
|
this.resolveModuleMiddleware();
|
|
694
735
|
}),
|
|
695
736
|
),
|
|
696
|
-
// Create controller instances in
|
|
737
|
+
// Create controller instances in all descendant modules first, then this module
|
|
697
738
|
Effect.flatMap(() =>
|
|
698
|
-
Effect.forEach(
|
|
739
|
+
Effect.forEach(allDescendants, (mod) => mod.createControllerInstances(), {
|
|
699
740
|
discard: true,
|
|
700
741
|
}),
|
|
701
742
|
),
|
|
702
743
|
Effect.flatMap(() => this.createControllerInstances()),
|
|
703
744
|
// Then call onModuleInit for controllers
|
|
704
745
|
Effect.flatMap(() => this.callControllersOnModuleInit()),
|
|
705
|
-
//
|
|
746
|
+
// Run onModuleInit for all descendant modules' controllers
|
|
706
747
|
Effect.flatMap(() =>
|
|
707
|
-
Effect.forEach(
|
|
748
|
+
Effect.forEach(allDescendants, (mod) => mod.callControllersOnModuleInit(), {
|
|
708
749
|
discard: true,
|
|
709
750
|
}),
|
|
710
751
|
),
|
|
@@ -1041,6 +1082,13 @@ export class OneBunModule implements ModuleInstance {
|
|
|
1041
1082
|
return this.rootLayer;
|
|
1042
1083
|
}
|
|
1043
1084
|
|
|
1085
|
+
/**
|
|
1086
|
+
* Register a service instance by tag (e.g. before setup() for application-provided services like QueueService proxy).
|
|
1087
|
+
*/
|
|
1088
|
+
registerService<T>(tag: Context.Tag<unknown, T>, instance: T): void {
|
|
1089
|
+
this.serviceInstances.set(tag as Context.Tag<unknown, unknown>, instance);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1044
1092
|
/**
|
|
1045
1093
|
* Create a module from class
|
|
1046
1094
|
* @param moduleClass - The module class
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
MessageAnyGuard,
|
|
36
36
|
createMessageGuard,
|
|
37
37
|
type Message,
|
|
38
|
+
type QueueAdapter,
|
|
38
39
|
CronExpression,
|
|
39
40
|
parseCronExpression,
|
|
40
41
|
getNextRun,
|
|
@@ -113,9 +114,9 @@ describe('Setup Section Examples (docs/api/queue.md)', () => {
|
|
|
113
114
|
* @source docs/api/queue.md#quick-start
|
|
114
115
|
*/
|
|
115
116
|
describe('Quick Start Example (docs/api/queue.md)', () => {
|
|
116
|
-
it('should define
|
|
117
|
-
// From docs/api/queue.md: Quick Start
|
|
118
|
-
class
|
|
117
|
+
it('should define controller with queue decorators', () => {
|
|
118
|
+
// From docs/api/queue.md: Quick Start (queue handlers must be in controllers)
|
|
119
|
+
class EventProcessor {
|
|
119
120
|
@OnQueueReady()
|
|
120
121
|
onReady() {
|
|
121
122
|
// console.log('Queue connected');
|
|
@@ -134,19 +135,19 @@ describe('Quick Start Example (docs/api/queue.md)', () => {
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
// Verify decorators are registered
|
|
137
|
-
expect(hasQueueDecorators(
|
|
138
|
+
expect(hasQueueDecorators(EventProcessor)).toBe(true);
|
|
138
139
|
|
|
139
|
-
const subscriptions = getSubscribeMetadata(
|
|
140
|
+
const subscriptions = getSubscribeMetadata(EventProcessor);
|
|
140
141
|
expect(subscriptions.length).toBe(1);
|
|
141
142
|
expect(subscriptions[0].pattern).toBe('orders.created');
|
|
142
143
|
|
|
143
|
-
const cronJobs = getCronMetadata(
|
|
144
|
+
const cronJobs = getCronMetadata(EventProcessor);
|
|
144
145
|
expect(cronJobs.length).toBe(1);
|
|
145
146
|
expect(cronJobs[0].expression).toBe(CronExpression.EVERY_HOUR);
|
|
146
147
|
expect(cronJobs[0].options.pattern).toBe('cleanup.expired');
|
|
147
148
|
});
|
|
148
149
|
|
|
149
|
-
it('should define
|
|
150
|
+
it('should define controller with interval decorator', () => {
|
|
150
151
|
// From docs/api/queue.md: Quick Start - Interval example
|
|
151
152
|
class EventProcessor {
|
|
152
153
|
@Subscribe('orders.created')
|
|
@@ -389,9 +390,9 @@ describe('Message Guards Examples (docs/api/queue.md)', () => {
|
|
|
389
390
|
* @source docs/api/queue.md#lifecycle-decorators
|
|
390
391
|
*/
|
|
391
392
|
describe('Lifecycle Decorators Examples (docs/api/queue.md)', () => {
|
|
392
|
-
it('should define lifecycle handlers', () => {
|
|
393
|
-
// From docs/api/queue.md: Lifecycle Decorators
|
|
394
|
-
class
|
|
393
|
+
it('should define lifecycle handlers on controller', () => {
|
|
394
|
+
// From docs/api/queue.md: Lifecycle Decorators (handlers must be in controllers)
|
|
395
|
+
class EventProcessor {
|
|
395
396
|
@OnQueueReady()
|
|
396
397
|
handleReady() {
|
|
397
398
|
// console.log('Queue connected');
|
|
@@ -418,8 +419,67 @@ describe('Lifecycle Decorators Examples (docs/api/queue.md)', () => {
|
|
|
418
419
|
}
|
|
419
420
|
}
|
|
420
421
|
|
|
421
|
-
// Class
|
|
422
|
-
expect(
|
|
422
|
+
// Class with lifecycle handlers only; hasQueueDecorators checks Subscribe/Cron/Interval/Timeout only
|
|
423
|
+
expect(EventProcessor).toBeDefined();
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* @source docs/api/queue.md#custom-adapter-nats-jetstream
|
|
429
|
+
*/
|
|
430
|
+
describe('Custom adapter NATS JetStream (docs/api/queue.md)', () => {
|
|
431
|
+
it('should use custom adapter constructor with options', async () => {
|
|
432
|
+
// From docs/api/queue.md: Custom adapter: NATS JetStream
|
|
433
|
+
// Minimal adapter class that implements QueueAdapter for use with queue: { adapter, options }
|
|
434
|
+
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
435
|
+
class NatsJetStreamAdapter implements QueueAdapter {
|
|
436
|
+
readonly name = 'nats-jetstream';
|
|
437
|
+
readonly type = 'jetstream';
|
|
438
|
+
constructor(private opts: { servers: string; stream?: string }) {}
|
|
439
|
+
async connect(): Promise<void> {}
|
|
440
|
+
async disconnect(): Promise<void> {}
|
|
441
|
+
isConnected(): boolean {
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
async publish(): Promise<string> {
|
|
445
|
+
return '';
|
|
446
|
+
}
|
|
447
|
+
async publishBatch(): Promise<string[]> {
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
450
|
+
async subscribe(): Promise<import('./types').Subscription> {
|
|
451
|
+
return {
|
|
452
|
+
async unsubscribe() {},
|
|
453
|
+
pause() {},
|
|
454
|
+
resume() {},
|
|
455
|
+
pattern: '',
|
|
456
|
+
isActive: true,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
async addScheduledJob(): Promise<void> {}
|
|
460
|
+
async removeScheduledJob(): Promise<boolean> {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
async getScheduledJobs(): Promise<import('./types').ScheduledJobInfo[]> {
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
supports(): boolean {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
on(): void {}
|
|
470
|
+
off(): void {}
|
|
471
|
+
}
|
|
472
|
+
/* eslint-enable @typescript-eslint/no-empty-function */
|
|
473
|
+
|
|
474
|
+
const adapter = new NatsJetStreamAdapter({
|
|
475
|
+
servers: 'nats://localhost:4222',
|
|
476
|
+
stream: 'EVENTS',
|
|
477
|
+
});
|
|
478
|
+
await adapter.connect();
|
|
479
|
+
expect(adapter.name).toBe('nats-jetstream');
|
|
480
|
+
expect(adapter.type).toBe('jetstream');
|
|
481
|
+
expect(adapter.isConnected()).toBe(true);
|
|
482
|
+
await adapter.disconnect();
|
|
423
483
|
});
|
|
424
484
|
});
|
|
425
485
|
|
package/src/queue/index.ts
CHANGED
|
@@ -113,6 +113,10 @@ export {
|
|
|
113
113
|
createQueueService,
|
|
114
114
|
resolveAdapterType,
|
|
115
115
|
} from './queue.service';
|
|
116
|
+
export {
|
|
117
|
+
QueueServiceProxy,
|
|
118
|
+
QUEUE_NOT_ENABLED_ERROR_MESSAGE,
|
|
119
|
+
} from './queue-service-proxy';
|
|
116
120
|
|
|
117
121
|
// Adapters (re-export from adapters folder)
|
|
118
122
|
export * from './adapters';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for QueueServiceProxy
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
describe,
|
|
7
|
+
test,
|
|
8
|
+
expect,
|
|
9
|
+
} from 'bun:test';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import { InMemoryQueueAdapter } from './adapters/memory.adapter';
|
|
13
|
+
import { QueueServiceProxy, QUEUE_NOT_ENABLED_ERROR_MESSAGE } from './queue-service-proxy';
|
|
14
|
+
import { QueueService } from './queue.service';
|
|
15
|
+
|
|
16
|
+
describe('QueueServiceProxy', () => {
|
|
17
|
+
test('throws with expected message when delegate is null', async () => {
|
|
18
|
+
const proxy = new QueueServiceProxy();
|
|
19
|
+
expect(() => proxy.getAdapter()).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
20
|
+
expect(() => proxy.getScheduler()).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
21
|
+
await expect(proxy.publish('e', {})).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
22
|
+
await expect(proxy.publishBatch([{ pattern: 'e', data: {} }])).rejects.toThrow(
|
|
23
|
+
QUEUE_NOT_ENABLED_ERROR_MESSAGE,
|
|
24
|
+
);
|
|
25
|
+
await expect(
|
|
26
|
+
proxy.subscribe('e', async () => {
|
|
27
|
+
/* no-op for throw test */
|
|
28
|
+
}),
|
|
29
|
+
).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
30
|
+
await expect(proxy.addScheduledJob('j', { pattern: 'e', schedule: { every: 1000 } })).rejects.toThrow(
|
|
31
|
+
QUEUE_NOT_ENABLED_ERROR_MESSAGE,
|
|
32
|
+
);
|
|
33
|
+
await expect(proxy.removeScheduledJob('j')).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
34
|
+
await expect(proxy.getScheduledJobs()).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
35
|
+
expect(() => proxy.supports('pattern-subscriptions')).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
36
|
+
expect(() =>
|
|
37
|
+
proxy.on('onReady', () => {
|
|
38
|
+
/* no-op */
|
|
39
|
+
}),
|
|
40
|
+
).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
41
|
+
expect(() =>
|
|
42
|
+
proxy.off('onReady', () => {
|
|
43
|
+
/* no-op */
|
|
44
|
+
}),
|
|
45
|
+
).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('delegates to real QueueService after setDelegate', async () => {
|
|
49
|
+
const adapter = new InMemoryQueueAdapter();
|
|
50
|
+
const real = new QueueService({ adapter: 'memory' as const });
|
|
51
|
+
await real.initialize(adapter);
|
|
52
|
+
await real.start();
|
|
53
|
+
|
|
54
|
+
const proxy = new QueueServiceProxy();
|
|
55
|
+
proxy.setDelegate(real);
|
|
56
|
+
|
|
57
|
+
expect(proxy.getAdapter()).toBe(adapter);
|
|
58
|
+
expect(proxy.getScheduler()).toBe(real.getScheduler());
|
|
59
|
+
expect(proxy.supports('pattern-subscriptions')).toBe(true);
|
|
60
|
+
|
|
61
|
+
const ids = await proxy.publish('test.event', { x: 1 });
|
|
62
|
+
expect(ids).toBeDefined();
|
|
63
|
+
expect(typeof ids).toBe('string');
|
|
64
|
+
|
|
65
|
+
const received: unknown[] = [];
|
|
66
|
+
const sub = await proxy.subscribe('test.event', async (msg) => {
|
|
67
|
+
received.push(msg.data);
|
|
68
|
+
});
|
|
69
|
+
expect(sub.isActive).toBe(true);
|
|
70
|
+
|
|
71
|
+
await proxy.publish('test.event', { y: 2 });
|
|
72
|
+
// Allow microtask for handler
|
|
73
|
+
await Promise.resolve();
|
|
74
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
75
|
+
expect(received.length).toBe(1);
|
|
76
|
+
expect(received[0]).toEqual({ y: 2 });
|
|
77
|
+
|
|
78
|
+
await real.stop();
|
|
79
|
+
proxy.setDelegate(null);
|
|
80
|
+
expect(() => proxy.getAdapter()).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueueServiceProxy — placeholder for DI when the real QueueService is created after setup().
|
|
3
|
+
* When the queue is enabled, the application sets the real QueueService via setDelegate().
|
|
4
|
+
* When the queue is not enabled, any method call throws a clear error.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { QueueService } from './queue.service';
|
|
8
|
+
import type { QueueScheduler } from './scheduler';
|
|
9
|
+
import type { QueueAdapter } from './types';
|
|
10
|
+
import type {
|
|
11
|
+
MessageHandler,
|
|
12
|
+
PublishOptions,
|
|
13
|
+
QueueEvents,
|
|
14
|
+
ScheduledJobInfo,
|
|
15
|
+
ScheduledJobOptions,
|
|
16
|
+
SubscribeOptions,
|
|
17
|
+
Subscription,
|
|
18
|
+
QueueFeature,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
const QUEUE_NOT_ENABLED_MESSAGE =
|
|
22
|
+
'Queue is not enabled. Enable it via `queue.enabled: true` in application options or register at least one controller with queue decorators (@Subscribe, @Cron, @Interval, @Timeout).';
|
|
23
|
+
|
|
24
|
+
function throwIfNoDelegate(delegate: QueueService | null): asserts delegate is QueueService {
|
|
25
|
+
if (delegate === null) {
|
|
26
|
+
throw new Error(QUEUE_NOT_ENABLED_MESSAGE);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Proxy for QueueService used in DI before the real service is created.
|
|
32
|
+
* After initializeQueue(), the application calls setDelegate(realQueueService) when the queue is enabled.
|
|
33
|
+
*/
|
|
34
|
+
export class QueueServiceProxy {
|
|
35
|
+
private delegate: QueueService | null = null;
|
|
36
|
+
|
|
37
|
+
setDelegate(service: QueueService | null): void {
|
|
38
|
+
this.delegate = service;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getAdapter(): QueueAdapter {
|
|
42
|
+
throwIfNoDelegate(this.delegate);
|
|
43
|
+
|
|
44
|
+
return this.delegate.getAdapter();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getScheduler(): QueueScheduler {
|
|
48
|
+
throwIfNoDelegate(this.delegate);
|
|
49
|
+
|
|
50
|
+
return this.delegate.getScheduler();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async publish<T>(pattern: string, data: T, options?: PublishOptions): Promise<string> {
|
|
54
|
+
throwIfNoDelegate(this.delegate);
|
|
55
|
+
|
|
56
|
+
return await this.delegate.publish(pattern, data, options);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async publishBatch<T>(
|
|
60
|
+
messages: Array<{ pattern: string; data: T; options?: PublishOptions }>,
|
|
61
|
+
): Promise<string[]> {
|
|
62
|
+
throwIfNoDelegate(this.delegate);
|
|
63
|
+
|
|
64
|
+
return await this.delegate.publishBatch(messages);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async subscribe<T>(
|
|
68
|
+
pattern: string,
|
|
69
|
+
handler: MessageHandler<T>,
|
|
70
|
+
options?: SubscribeOptions,
|
|
71
|
+
): Promise<Subscription> {
|
|
72
|
+
throwIfNoDelegate(this.delegate);
|
|
73
|
+
|
|
74
|
+
return await this.delegate.subscribe(pattern, handler, options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async addScheduledJob(name: string, options: ScheduledJobOptions): Promise<void> {
|
|
78
|
+
throwIfNoDelegate(this.delegate);
|
|
79
|
+
|
|
80
|
+
return await this.delegate.addScheduledJob(name, options);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async removeScheduledJob(name: string): Promise<boolean> {
|
|
84
|
+
throwIfNoDelegate(this.delegate);
|
|
85
|
+
|
|
86
|
+
return await this.delegate.removeScheduledJob(name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async getScheduledJobs(): Promise<ScheduledJobInfo[]> {
|
|
90
|
+
throwIfNoDelegate(this.delegate);
|
|
91
|
+
|
|
92
|
+
return await this.delegate.getScheduledJobs();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
supports(feature: QueueFeature): boolean {
|
|
96
|
+
throwIfNoDelegate(this.delegate);
|
|
97
|
+
|
|
98
|
+
return this.delegate.supports(feature);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
on<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
|
|
102
|
+
throwIfNoDelegate(this.delegate);
|
|
103
|
+
|
|
104
|
+
return this.delegate.on(event, handler);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
off<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
|
|
108
|
+
throwIfNoDelegate(this.delegate);
|
|
109
|
+
|
|
110
|
+
return this.delegate.off(event, handler);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const QUEUE_NOT_ENABLED_ERROR_MESSAGE = QUEUE_NOT_ENABLED_MESSAGE;
|
package/src/queue/types.ts
CHANGED
|
@@ -395,8 +395,8 @@ export interface QueueAdapterConstructor {
|
|
|
395
395
|
* Queue configuration options
|
|
396
396
|
*/
|
|
397
397
|
export interface QueueConfig {
|
|
398
|
-
/** Adapter type or custom adapter class */
|
|
399
|
-
adapter:
|
|
398
|
+
/** Adapter type (built-in or custom, e.g. 'jetstream') or custom adapter class */
|
|
399
|
+
adapter: QueueAdapterType | QueueAdapterConstructor;
|
|
400
400
|
|
|
401
401
|
/** Adapter-specific options */
|
|
402
402
|
options?: Record<string, unknown>;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { OneBunRequest, OneBunResponse } from '../types';
|
|
2
|
+
|
|
3
|
+
import { BaseMiddleware } from '../module/middleware';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CORS (Cross-Origin Resource Sharing) configuration options.
|
|
7
|
+
* Passed via `ApplicationOptions.cors`.
|
|
8
|
+
*/
|
|
9
|
+
export interface CorsOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Allowed origin(s).
|
|
12
|
+
* - `'*'` — allow any origin
|
|
13
|
+
* - `string` — exact match
|
|
14
|
+
* - `RegExp` — regex match
|
|
15
|
+
* - `string[]` / `RegExp[]` — list of allowed origins
|
|
16
|
+
* - `(origin: string) => boolean` — custom predicate
|
|
17
|
+
* @defaultValue '*'
|
|
18
|
+
*/
|
|
19
|
+
origin?: string | RegExp | Array<string | RegExp> | ((origin: string) => boolean);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Allowed HTTP methods.
|
|
23
|
+
* @defaultValue ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS']
|
|
24
|
+
*/
|
|
25
|
+
methods?: string[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Allowed request headers.
|
|
29
|
+
* @defaultValue ['Content-Type', 'Authorization']
|
|
30
|
+
*/
|
|
31
|
+
allowedHeaders?: string[];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Headers to expose to the browser.
|
|
35
|
+
*/
|
|
36
|
+
exposedHeaders?: string[];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Allow credentials (cookies, Authorization header).
|
|
40
|
+
* @defaultValue false
|
|
41
|
+
*/
|
|
42
|
+
credentials?: boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Preflight cache duration in seconds.
|
|
46
|
+
* @defaultValue 86400 (24 hours)
|
|
47
|
+
*/
|
|
48
|
+
maxAge?: number;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether to pass the CORS preflight request to the next handler.
|
|
52
|
+
* When `false` (default) preflight requests are handled by the middleware
|
|
53
|
+
* and never reach route handlers.
|
|
54
|
+
* @defaultValue false
|
|
55
|
+
*/
|
|
56
|
+
preflightContinue?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const DEFAULT_METHODS = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE', 'OPTIONS'];
|
|
60
|
+
const DEFAULT_ALLOWED_HEADERS = ['Content-Type', 'Authorization'];
|
|
61
|
+
const DEFAULT_MAX_AGE = 86400;
|
|
62
|
+
const NO_CONTENT = 204;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolves whether the given `origin` is allowed by the CORS configuration.
|
|
66
|
+
*/
|
|
67
|
+
function isOriginAllowed(
|
|
68
|
+
origin: string,
|
|
69
|
+
allowed: NonNullable<CorsOptions['origin']>,
|
|
70
|
+
): boolean {
|
|
71
|
+
if (allowed === '*') {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof allowed === 'string') {
|
|
76
|
+
return origin === allowed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (allowed instanceof RegExp) {
|
|
80
|
+
return allowed.test(origin);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof allowed === 'function') {
|
|
84
|
+
return allowed(origin);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (allowed as Array<string | RegExp>).some((item) =>
|
|
88
|
+
typeof item === 'string' ? origin === item : item.test(origin),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Built-in CORS middleware.
|
|
94
|
+
*
|
|
95
|
+
* Handles preflight `OPTIONS` requests and sets appropriate CORS response
|
|
96
|
+
* headers for all other requests. Configure once via `ApplicationOptions.cors`
|
|
97
|
+
* and the framework will instantiate this middleware automatically.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const app = new OneBunApplication(AppModule, {
|
|
102
|
+
* cors: {
|
|
103
|
+
* origin: 'https://my-frontend.example.com',
|
|
104
|
+
* credentials: true,
|
|
105
|
+
* },
|
|
106
|
+
* });
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* When using manually (e.g. from `ApplicationOptions.middleware`):
|
|
110
|
+
* ```typescript
|
|
111
|
+
* const app = new OneBunApplication(AppModule, {
|
|
112
|
+
* middleware: [CorsMiddleware.configure({ origin: /example\.com$/ })],
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export class CorsMiddleware extends BaseMiddleware {
|
|
117
|
+
private readonly options: Required<
|
|
118
|
+
Pick<CorsOptions, 'methods' | 'allowedHeaders' | 'maxAge' | 'credentials' | 'preflightContinue'>
|
|
119
|
+
> &
|
|
120
|
+
Pick<CorsOptions, 'origin' | 'exposedHeaders'>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Create a pre-configured CorsMiddleware class with the given options.
|
|
124
|
+
* Returns a constructor — pass the result directly to `ApplicationOptions.middleware`.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* const app = new OneBunApplication(AppModule, {
|
|
129
|
+
* middleware: [CorsMiddleware.configure({ origin: 'https://example.com' })],
|
|
130
|
+
* });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
static configure(options: CorsOptions = {}): typeof CorsMiddleware {
|
|
134
|
+
class ConfiguredCorsMiddleware extends CorsMiddleware {
|
|
135
|
+
constructor() {
|
|
136
|
+
super(options);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return ConfiguredCorsMiddleware;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
constructor(options: CorsOptions = {}) {
|
|
144
|
+
super();
|
|
145
|
+
|
|
146
|
+
this.options = {
|
|
147
|
+
origin: options.origin ?? '*',
|
|
148
|
+
methods: options.methods ?? DEFAULT_METHODS,
|
|
149
|
+
allowedHeaders: options.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS,
|
|
150
|
+
exposedHeaders: options.exposedHeaders,
|
|
151
|
+
credentials: options.credentials ?? false,
|
|
152
|
+
maxAge: options.maxAge ?? DEFAULT_MAX_AGE,
|
|
153
|
+
preflightContinue: options.preflightContinue ?? false,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async use(req: OneBunRequest, next: () => Promise<OneBunResponse>): Promise<OneBunResponse> {
|
|
158
|
+
const origin = req.headers.get('origin') ?? '';
|
|
159
|
+
const headers = new Headers();
|
|
160
|
+
|
|
161
|
+
// Determine the effective origin for the Access-Control-Allow-Origin header
|
|
162
|
+
const { origin: allowedOrigin } = this.options;
|
|
163
|
+
const allowAll = allowedOrigin === '*' && !this.options.credentials;
|
|
164
|
+
|
|
165
|
+
if (allowAll) {
|
|
166
|
+
headers.set('Access-Control-Allow-Origin', '*');
|
|
167
|
+
} else if (origin && isOriginAllowed(origin, allowedOrigin ?? '*')) {
|
|
168
|
+
headers.set('Access-Control-Allow-Origin', origin);
|
|
169
|
+
headers.append('Vary', 'Origin');
|
|
170
|
+
} else if (!origin) {
|
|
171
|
+
// Non-browser request (no Origin header) — set wildcard if configured
|
|
172
|
+
if (allowedOrigin === '*') {
|
|
173
|
+
headers.set('Access-Control-Allow-Origin', '*');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (this.options.credentials) {
|
|
178
|
+
headers.set('Access-Control-Allow-Credentials', 'true');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (this.options.exposedHeaders && this.options.exposedHeaders.length > 0) {
|
|
182
|
+
headers.set('Access-Control-Expose-Headers', this.options.exposedHeaders.join(', '));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Handle preflight OPTIONS request
|
|
186
|
+
if (req.method === 'OPTIONS') {
|
|
187
|
+
headers.set('Access-Control-Allow-Methods', this.options.methods.join(', '));
|
|
188
|
+
headers.set('Access-Control-Allow-Headers', this.options.allowedHeaders.join(', '));
|
|
189
|
+
headers.set('Access-Control-Max-Age', String(this.options.maxAge));
|
|
190
|
+
|
|
191
|
+
if (this.options.preflightContinue) {
|
|
192
|
+
const response = await next();
|
|
193
|
+
// Copy CORS headers onto the response from next()
|
|
194
|
+
for (const [key, value] of headers.entries()) {
|
|
195
|
+
response.headers.set(key, value);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return response;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return new Response(null, { status: NO_CONTENT, headers });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// For non-OPTIONS requests — let the handler run, then attach CORS headers
|
|
205
|
+
const response = await next();
|
|
206
|
+
for (const [key, value] of headers.entries()) {
|
|
207
|
+
response.headers.set(key, value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return response;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Middleware
|
|
3
|
+
*
|
|
4
|
+
* Built-in middleware for common security concerns: CORS, rate limiting,
|
|
5
|
+
* and HTTP security headers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { CorsMiddleware, type CorsOptions } from './cors-middleware';
|
|
9
|
+
export {
|
|
10
|
+
RateLimitMiddleware,
|
|
11
|
+
MemoryRateLimitStore,
|
|
12
|
+
RedisRateLimitStore,
|
|
13
|
+
type RateLimitOptions,
|
|
14
|
+
type RateLimitStore,
|
|
15
|
+
} from './rate-limit-middleware';
|
|
16
|
+
export {
|
|
17
|
+
SecurityHeadersMiddleware,
|
|
18
|
+
type SecurityHeadersOptions,
|
|
19
|
+
} from './security-headers-middleware';
|