@onebun/core 0.1.23 → 0.2.0
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 +519 -32
- package/src/application/application.ts +342 -377
- package/src/decorators/decorators.test.ts +47 -33
- package/src/decorators/decorators.ts +12 -0
- package/src/docs-examples.test.ts +300 -1
- package/src/index.ts +2 -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 +18 -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
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
expect,
|
|
12
12
|
beforeEach,
|
|
13
13
|
afterEach,
|
|
14
|
+
mock,
|
|
14
15
|
} from 'bun:test';
|
|
15
16
|
|
|
16
17
|
import { OneBunApplication } from '../application';
|
|
@@ -817,50 +818,63 @@ describe('decorators', () => {
|
|
|
817
818
|
});
|
|
818
819
|
|
|
819
820
|
test('should properly inject module service into controller without @Inject', async () => {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
821
|
+
// Mock Bun.serve to avoid starting a real server
|
|
822
|
+
const originalServe = Bun.serve;
|
|
823
|
+
|
|
824
|
+
(Bun as any).serve = mock(() => ({
|
|
825
|
+
stop: mock(),
|
|
826
|
+
port: 3000,
|
|
827
|
+
}));
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
@Service()
|
|
831
|
+
class TestService extends BaseService {
|
|
832
|
+
getValue() {
|
|
833
|
+
return 'injected-value';
|
|
834
|
+
}
|
|
824
835
|
}
|
|
825
|
-
}
|
|
826
836
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
837
|
+
// No @Inject needed - automatic DI via emitDecoratorMetadata
|
|
838
|
+
@Controller('')
|
|
839
|
+
class TestController extends BaseController {
|
|
840
|
+
constructor(private service: TestService) {
|
|
841
|
+
super();
|
|
842
|
+
}
|
|
833
843
|
|
|
834
|
-
|
|
835
|
-
|
|
844
|
+
getServiceValue() {
|
|
845
|
+
return this.service.getValue();
|
|
846
|
+
}
|
|
836
847
|
}
|
|
837
|
-
}
|
|
838
848
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
849
|
+
@Module({
|
|
850
|
+
controllers: [TestController],
|
|
851
|
+
providers: [TestService],
|
|
852
|
+
})
|
|
853
|
+
class TestModule {}
|
|
844
854
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
855
|
+
const app = new OneBunApplication(TestModule, {
|
|
856
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
857
|
+
});
|
|
848
858
|
|
|
849
|
-
|
|
850
|
-
|
|
859
|
+
// start() creates rootModule and calls setup, triggering dependency injection
|
|
860
|
+
await app.start();
|
|
851
861
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
862
|
+
// Access rootModule after start to verify DI
|
|
863
|
+
|
|
864
|
+
const rootModule = (app as any).rootModule;
|
|
855
865
|
|
|
856
|
-
|
|
857
|
-
|
|
866
|
+
// Get controller instance and verify service was injected
|
|
867
|
+
const controllerInstance = rootModule.getControllerInstance(TestController) as TestController;
|
|
858
868
|
|
|
859
|
-
|
|
860
|
-
|
|
869
|
+
expect(controllerInstance).toBeDefined();
|
|
870
|
+
expect(controllerInstance).toBeInstanceOf(TestController);
|
|
861
871
|
|
|
862
|
-
|
|
863
|
-
|
|
872
|
+
// Verify the injected service works correctly
|
|
873
|
+
expect(controllerInstance.getServiceValue()).toBe('injected-value');
|
|
874
|
+
} finally {
|
|
875
|
+
|
|
876
|
+
(Bun as any).serve = originalServe;
|
|
877
|
+
}
|
|
864
878
|
});
|
|
865
879
|
});
|
|
866
880
|
|
|
@@ -493,6 +493,18 @@ export const Body = createParamDecorator(ParamType.BODY);
|
|
|
493
493
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
494
494
|
export const Header = createParamDecorator(ParamType.HEADER);
|
|
495
495
|
|
|
496
|
+
/**
|
|
497
|
+
* Cookie parameter decorator.
|
|
498
|
+
* Extracts a cookie value by name using BunRequest.cookies (CookieMap).
|
|
499
|
+
* Optional by default, use { required: true } for required cookies.
|
|
500
|
+
* @example \@Cookie('session_id') - optional
|
|
501
|
+
* @example \@Cookie('session_id', { required: true }) - required
|
|
502
|
+
* @example \@Cookie('session_id', schema) - optional with validation
|
|
503
|
+
* @example \@Cookie('session_id', schema, { required: true }) - required with validation
|
|
504
|
+
*/
|
|
505
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
506
|
+
export const Cookie = createParamDecorator(ParamType.COOKIE);
|
|
507
|
+
|
|
496
508
|
/**
|
|
497
509
|
* Request object decorator
|
|
498
510
|
* @example \@Req()
|
|
@@ -33,7 +33,12 @@ import type {
|
|
|
33
33
|
BeforeApplicationDestroy,
|
|
34
34
|
OnApplicationDestroy,
|
|
35
35
|
} from './';
|
|
36
|
-
import type {
|
|
36
|
+
import type {
|
|
37
|
+
SseEvent,
|
|
38
|
+
SseGenerator,
|
|
39
|
+
OneBunRequest,
|
|
40
|
+
OneBunResponse,
|
|
41
|
+
} from './types';
|
|
37
42
|
import type { ServerWebSocket } from 'bun';
|
|
38
43
|
|
|
39
44
|
|
|
@@ -3431,13 +3436,16 @@ import {
|
|
|
3431
3436
|
Body,
|
|
3432
3437
|
Header,
|
|
3433
3438
|
Req,
|
|
3439
|
+
Cookie,
|
|
3434
3440
|
Module,
|
|
3435
3441
|
Service,
|
|
3436
3442
|
BaseService,
|
|
3437
3443
|
BaseController,
|
|
3438
3444
|
UseMiddleware,
|
|
3439
3445
|
getServiceTag,
|
|
3446
|
+
getControllerMetadata,
|
|
3440
3447
|
HttpStatusCode,
|
|
3448
|
+
ParamType,
|
|
3441
3449
|
NotFoundError,
|
|
3442
3450
|
InternalServerError,
|
|
3443
3451
|
OneBunBaseError,
|
|
@@ -3829,3 +3837,294 @@ describe('SSE (Server-Sent Events) API Documentation (docs/api/controllers.md)',
|
|
|
3829
3837
|
});
|
|
3830
3838
|
});
|
|
3831
3839
|
});
|
|
3840
|
+
|
|
3841
|
+
// ============================================================================
|
|
3842
|
+
// Cookie, Headers, @Req with OneBunRequest
|
|
3843
|
+
// ============================================================================
|
|
3844
|
+
|
|
3845
|
+
describe('@Cookie Decorator (docs/api/decorators.md)', () => {
|
|
3846
|
+
/**
|
|
3847
|
+
* @source docs/api/decorators.md#cookie
|
|
3848
|
+
*/
|
|
3849
|
+
it('should define @Cookie decorator with optional parameter', () => {
|
|
3850
|
+
// From docs: @Cookie('session_id') - optional
|
|
3851
|
+
@Controller('/api')
|
|
3852
|
+
class ApiController extends BaseController {
|
|
3853
|
+
@Get('/me')
|
|
3854
|
+
async getMe(@Cookie('session_id') sessionId?: string) {
|
|
3855
|
+
return { sessionId };
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
expect(ApiController).toBeDefined();
|
|
3860
|
+
const metadata = getControllerMetadata(ApiController);
|
|
3861
|
+
expect(metadata).toBeDefined();
|
|
3862
|
+
expect(metadata!.routes.length).toBe(1);
|
|
3863
|
+
expect(metadata!.routes[0].params!.length).toBe(1);
|
|
3864
|
+
expect(metadata!.routes[0].params![0].type).toBe(ParamType.COOKIE);
|
|
3865
|
+
expect(metadata!.routes[0].params![0].name).toBe('session_id');
|
|
3866
|
+
expect(metadata!.routes[0].params![0].isRequired).toBe(false);
|
|
3867
|
+
});
|
|
3868
|
+
|
|
3869
|
+
/**
|
|
3870
|
+
* @source docs/api/decorators.md#cookie-required
|
|
3871
|
+
*/
|
|
3872
|
+
it('should define @Cookie decorator with required option', () => {
|
|
3873
|
+
// From docs: @Cookie('session_id', { required: true }) - required
|
|
3874
|
+
@Controller('/api')
|
|
3875
|
+
class AuthController extends BaseController {
|
|
3876
|
+
@Get('/protected')
|
|
3877
|
+
async protectedRoute(@Cookie('session_id', { required: true }) sessionId: string) {
|
|
3878
|
+
return { sessionId };
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
expect(AuthController).toBeDefined();
|
|
3883
|
+
const metadata = getControllerMetadata(AuthController);
|
|
3884
|
+
expect(metadata!.routes[0].params![0].isRequired).toBe(true);
|
|
3885
|
+
});
|
|
3886
|
+
|
|
3887
|
+
/**
|
|
3888
|
+
* @source docs/api/decorators.md#cookie-with-validation
|
|
3889
|
+
*/
|
|
3890
|
+
it('should define @Cookie decorator with validation schema', () => {
|
|
3891
|
+
// From docs: @Cookie('session_id', schema) - optional with validation
|
|
3892
|
+
const uuidSchema = type('string');
|
|
3893
|
+
|
|
3894
|
+
@Controller('/api')
|
|
3895
|
+
class ApiController extends BaseController {
|
|
3896
|
+
@Get('/session')
|
|
3897
|
+
async getSession(@Cookie('session_id', uuidSchema) sessionId?: string) {
|
|
3898
|
+
return { sessionId };
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
expect(ApiController).toBeDefined();
|
|
3903
|
+
const metadata = getControllerMetadata(ApiController);
|
|
3904
|
+
expect(metadata!.routes[0].params![0].schema).toBeDefined();
|
|
3905
|
+
});
|
|
3906
|
+
|
|
3907
|
+
/**
|
|
3908
|
+
* @source docs/api/decorators.md#cookie-combined-example
|
|
3909
|
+
*/
|
|
3910
|
+
it('should combine @Cookie with other parameter decorators', () => {
|
|
3911
|
+
// From docs: combining @Cookie, @Param, @Header, @Query
|
|
3912
|
+
@Controller('/api')
|
|
3913
|
+
class CombinedController extends BaseController {
|
|
3914
|
+
@Get('/users/:id')
|
|
3915
|
+
async getUser(
|
|
3916
|
+
@Param('id') id: string,
|
|
3917
|
+
@Query('fields') fields?: string,
|
|
3918
|
+
@Header('Authorization') auth?: string,
|
|
3919
|
+
@Cookie('session') session?: string,
|
|
3920
|
+
) {
|
|
3921
|
+
return {
|
|
3922
|
+
id, fields, auth, session,
|
|
3923
|
+
};
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
expect(CombinedController).toBeDefined();
|
|
3928
|
+
const metadata = getControllerMetadata(CombinedController);
|
|
3929
|
+
expect(metadata!.routes[0].params!.length).toBe(4);
|
|
3930
|
+
});
|
|
3931
|
+
});
|
|
3932
|
+
|
|
3933
|
+
describe('@Req() with OneBunRequest (docs/api/decorators.md)', () => {
|
|
3934
|
+
/**
|
|
3935
|
+
* @source docs/api/decorators.md#req-onebunrequest
|
|
3936
|
+
*/
|
|
3937
|
+
it('should define @Req() handler with OneBunRequest type', () => {
|
|
3938
|
+
// From docs: @Req() with OneBunRequest type
|
|
3939
|
+
// OneBunRequest extends Request with .cookies (CookieMap) and .params
|
|
3940
|
+
@Controller('/api')
|
|
3941
|
+
class ApiController extends BaseController {
|
|
3942
|
+
@Get('/raw')
|
|
3943
|
+
async handleRaw(@Req() req: OneBunRequest) {
|
|
3944
|
+
const url = new URL(req.url);
|
|
3945
|
+
|
|
3946
|
+
// req.cookies is CookieMap, req.params is available from routes API
|
|
3947
|
+
return { url: url.pathname };
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
expect(ApiController).toBeDefined();
|
|
3952
|
+
const metadata = getControllerMetadata(ApiController);
|
|
3953
|
+
expect(metadata!.routes[0].params![0].type).toBe(ParamType.REQUEST);
|
|
3954
|
+
});
|
|
3955
|
+
|
|
3956
|
+
/**
|
|
3957
|
+
* @source docs/api/decorators.md#req-cookies-access
|
|
3958
|
+
*/
|
|
3959
|
+
it('should define handler accessing cookies via req.cookies', () => {
|
|
3960
|
+
// From docs: reading cookies through @Req()
|
|
3961
|
+
@Controller('/api')
|
|
3962
|
+
class ApiController extends BaseController {
|
|
3963
|
+
@Get('/session')
|
|
3964
|
+
async session(@Req() req: OneBunRequest) {
|
|
3965
|
+
// Access cookies via CookieMap
|
|
3966
|
+
const session = req.cookies.get('session');
|
|
3967
|
+
|
|
3968
|
+
return { session };
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
expect(ApiController).toBeDefined();
|
|
3973
|
+
});
|
|
3974
|
+
});
|
|
3975
|
+
|
|
3976
|
+
describe('OneBunRequest and OneBunResponse Types (docs/api/decorators.md)', () => {
|
|
3977
|
+
/**
|
|
3978
|
+
* @source docs/api/decorators.md#onebunrequest-type
|
|
3979
|
+
*/
|
|
3980
|
+
it('should use OneBunRequest as type alias for BunRequest', () => {
|
|
3981
|
+
// OneBunRequest is an alias for BunRequest
|
|
3982
|
+
// It extends standard Request with .cookies and .params
|
|
3983
|
+
const _check: OneBunRequest extends Request ? true : false = true;
|
|
3984
|
+
expect(_check).toBe(true);
|
|
3985
|
+
});
|
|
3986
|
+
|
|
3987
|
+
/**
|
|
3988
|
+
* @source docs/api/decorators.md#onebunresponse-type
|
|
3989
|
+
*/
|
|
3990
|
+
it('should use OneBunResponse as type alias for Response', () => {
|
|
3991
|
+
// OneBunResponse is an alias for Response
|
|
3992
|
+
const response: OneBunResponse = new Response('ok');
|
|
3993
|
+
expect(response).toBeInstanceOf(Response);
|
|
3994
|
+
});
|
|
3995
|
+
});
|
|
3996
|
+
|
|
3997
|
+
describe('Custom Response Headers (docs/api/controllers.md)', () => {
|
|
3998
|
+
/**
|
|
3999
|
+
* @source docs/api/controllers.md#custom-response-headers
|
|
4000
|
+
*/
|
|
4001
|
+
it('should define handler returning Response with custom headers', () => {
|
|
4002
|
+
// From docs: returning Response with custom headers
|
|
4003
|
+
@Controller('/api')
|
|
4004
|
+
class ApiController extends BaseController {
|
|
4005
|
+
@Get('/download')
|
|
4006
|
+
async download() {
|
|
4007
|
+
return new Response(JSON.stringify({ data: 'file content' }), {
|
|
4008
|
+
status: 200,
|
|
4009
|
+
headers: {
|
|
4010
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
4011
|
+
'Content-Type': 'application/json',
|
|
4012
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
4013
|
+
'X-Custom-Header': 'custom-value',
|
|
4014
|
+
},
|
|
4015
|
+
});
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
expect(ApiController).toBeDefined();
|
|
4020
|
+
});
|
|
4021
|
+
|
|
4022
|
+
/**
|
|
4023
|
+
* @source docs/api/controllers.md#set-cookie-header
|
|
4024
|
+
*/
|
|
4025
|
+
it('should define handler returning Response with Set-Cookie header', () => {
|
|
4026
|
+
// From docs: setting cookies via Set-Cookie header
|
|
4027
|
+
@Controller('/api')
|
|
4028
|
+
class AuthController extends BaseController {
|
|
4029
|
+
@Post('/login')
|
|
4030
|
+
|
|
4031
|
+
async login(@Body() _body: unknown) {
|
|
4032
|
+
const headers = new Headers();
|
|
4033
|
+
headers.set('Content-Type', 'application/json');
|
|
4034
|
+
headers.append('Set-Cookie', 'session=abc123; Path=/; HttpOnly');
|
|
4035
|
+
headers.append('Set-Cookie', 'theme=dark; Path=/');
|
|
4036
|
+
|
|
4037
|
+
return new Response(JSON.stringify({ loggedIn: true }), {
|
|
4038
|
+
status: 200,
|
|
4039
|
+
headers,
|
|
4040
|
+
});
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
|
|
4044
|
+
expect(AuthController).toBeDefined();
|
|
4045
|
+
});
|
|
4046
|
+
});
|
|
4047
|
+
|
|
4048
|
+
describe('Working with Cookies (docs/api/controllers.md)', () => {
|
|
4049
|
+
/**
|
|
4050
|
+
* @source docs/api/controllers.md#reading-cookies-via-decorator
|
|
4051
|
+
*/
|
|
4052
|
+
it('should define handler reading cookies via @Cookie decorator', () => {
|
|
4053
|
+
// From docs: reading cookies via @Cookie('name')
|
|
4054
|
+
@Controller('/api')
|
|
4055
|
+
class PrefsController extends BaseController {
|
|
4056
|
+
@Get('/preferences')
|
|
4057
|
+
async getPrefs(
|
|
4058
|
+
@Cookie('theme') theme?: string,
|
|
4059
|
+
@Cookie('lang') lang?: string,
|
|
4060
|
+
) {
|
|
4061
|
+
return {
|
|
4062
|
+
theme: theme ?? 'light',
|
|
4063
|
+
lang: lang ?? 'en',
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
expect(PrefsController).toBeDefined();
|
|
4069
|
+
});
|
|
4070
|
+
|
|
4071
|
+
/**
|
|
4072
|
+
* @source docs/api/controllers.md#reading-cookies-via-req
|
|
4073
|
+
*/
|
|
4074
|
+
it('should define handler reading cookies via req.cookies', () => {
|
|
4075
|
+
// From docs: reading cookies through @Req() with req.cookies.get()
|
|
4076
|
+
@Controller('/api')
|
|
4077
|
+
class ApiController extends BaseController {
|
|
4078
|
+
@Get('/session')
|
|
4079
|
+
async session(@Req() req: OneBunRequest) {
|
|
4080
|
+
const session = req.cookies.get('session');
|
|
4081
|
+
|
|
4082
|
+
return { session };
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
|
|
4086
|
+
expect(ApiController).toBeDefined();
|
|
4087
|
+
});
|
|
4088
|
+
|
|
4089
|
+
/**
|
|
4090
|
+
* @source docs/api/controllers.md#setting-cookies-via-req
|
|
4091
|
+
*/
|
|
4092
|
+
it('should define handler setting cookies via req.cookies', () => {
|
|
4093
|
+
// From docs: setting cookies using req.cookies.set()
|
|
4094
|
+
@Controller('/api')
|
|
4095
|
+
class AuthController extends BaseController {
|
|
4096
|
+
@Post('/login')
|
|
4097
|
+
|
|
4098
|
+
async login(@Req() req: OneBunRequest, @Body() _body: unknown) {
|
|
4099
|
+
// Set cookie via CookieMap
|
|
4100
|
+
req.cookies.set('session', 'new-session-id', {
|
|
4101
|
+
httpOnly: true,
|
|
4102
|
+
path: '/',
|
|
4103
|
+
maxAge: 3600,
|
|
4104
|
+
});
|
|
4105
|
+
|
|
4106
|
+
return { loggedIn: true };
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
expect(AuthController).toBeDefined();
|
|
4111
|
+
});
|
|
4112
|
+
|
|
4113
|
+
/**
|
|
4114
|
+
* @source docs/api/controllers.md#deleting-cookies-via-req
|
|
4115
|
+
*/
|
|
4116
|
+
it('should define handler deleting cookies via req.cookies', () => {
|
|
4117
|
+
// From docs: deleting cookies using req.cookies.delete()
|
|
4118
|
+
@Controller('/api')
|
|
4119
|
+
class AuthController extends BaseController {
|
|
4120
|
+
@Post('/logout')
|
|
4121
|
+
async logout(@Req() req: OneBunRequest) {
|
|
4122
|
+
req.cookies.delete('session');
|
|
4123
|
+
|
|
4124
|
+
return { loggedOut: true };
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
expect(AuthController).toBeDefined();
|
|
4129
|
+
});
|
|
4130
|
+
});
|
package/src/index.ts
CHANGED
package/src/module/controller.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { IConfig, OneBunAppConfig } from './config.interface';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
OneBunRequest,
|
|
4
|
+
SseEvent,
|
|
5
|
+
SseOptions,
|
|
6
|
+
} from '../types';
|
|
3
7
|
import type { Context } from 'effect';
|
|
4
8
|
|
|
5
9
|
import type { SyncLogger } from '@onebun/logger';
|
|
@@ -101,14 +105,14 @@ export class Controller {
|
|
|
101
105
|
/**
|
|
102
106
|
* Check if request has JSON content type
|
|
103
107
|
*/
|
|
104
|
-
protected isJson(req: Request): boolean {
|
|
108
|
+
protected isJson(req: OneBunRequest | Request): boolean {
|
|
105
109
|
return req.headers.get('content-type')?.includes('application/json') ?? false;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
/**
|
|
109
113
|
* Parse JSON from request body
|
|
110
114
|
*/
|
|
111
|
-
protected async parseJson<T = unknown>(req: Request): Promise<T> {
|
|
115
|
+
protected async parseJson<T = unknown>(req: OneBunRequest | Request): Promise<T> {
|
|
112
116
|
return (await req.json()) as T;
|
|
113
117
|
}
|
|
114
118
|
|
|
@@ -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,18 +520,19 @@ 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',
|
|
510
526
|
}
|
|
511
527
|
|
|
512
528
|
/**
|
|
513
|
-
* Options for parameter decorators (@Query, @Header, @Body, etc.)
|
|
529
|
+
* Options for parameter decorators (@Query, @Header, @Cookie, @Body, etc.)
|
|
514
530
|
*/
|
|
515
531
|
export interface ParamDecoratorOptions {
|
|
516
532
|
/**
|
|
517
533
|
* Whether the parameter is required
|
|
518
534
|
* - @Param: always true (OpenAPI spec requirement)
|
|
519
|
-
* - @Query, @Header: false by default
|
|
535
|
+
* - @Query, @Header, @Cookie: false by default
|
|
520
536
|
* - @Body: determined from schema (accepts undefined = optional)
|
|
521
537
|
*/
|
|
522
538
|
required?: boolean;
|
|
@@ -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
|
|