@onebun/core 0.2.10 → 0.2.11

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebun/core",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "Core package for OneBun framework - decorators, DI, modules, controllers",
5
5
  "license": "LGPL-3.0",
6
6
  "author": "RemRyahirev",
@@ -41,7 +41,14 @@ import { BaseMiddleware } from '../module/middleware';
41
41
  import { clearGlobalServicesRegistry } from '../module/module';
42
42
  import { Service } from '../module/service';
43
43
  import { QueueService, QUEUE_NOT_ENABLED_ERROR_MESSAGE } from '../queue';
44
- import { Subscribe } from '../queue/decorators';
44
+ import { CronExpression } from '../queue/cron-expression';
45
+ import {
46
+ Cron,
47
+ Interval,
48
+ OnQueueReady,
49
+ Subscribe,
50
+ Timeout,
51
+ } from '../queue/decorators';
45
52
  import { makeMockLoggerLayer } from '../testing/test-utils';
46
53
 
47
54
 
@@ -4083,4 +4090,225 @@ describe('OneBunApplication', () => {
4083
4090
  await app.stop();
4084
4091
  });
4085
4092
  });
4093
+
4094
+ describe('queue auto-detection', () => {
4095
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
4096
+ const noop = () => {};
4097
+
4098
+ afterEach(async () => {
4099
+ clearGlobalServicesRegistry();
4100
+ register.clear();
4101
+ });
4102
+
4103
+ test('auto-enables queue when controller has @Subscribe', async () => {
4104
+ @Controller('/auto-sub')
4105
+ class AutoSubController extends BaseController {
4106
+ @Subscribe('auto.test')
4107
+ async handle(): Promise<void> {}
4108
+ }
4109
+
4110
+ @Module({ controllers: [AutoSubController] })
4111
+ class AutoSubModule {}
4112
+
4113
+ const app = createTestApp(AutoSubModule, { port: 0 });
4114
+ await app.start();
4115
+
4116
+ expect(app.getQueueService()).not.toBeNull();
4117
+
4118
+ await app.stop();
4119
+ });
4120
+
4121
+ test('auto-enables queue when controller has @Cron', async () => {
4122
+ @Controller('/auto-cron')
4123
+ class AutoCronController extends BaseController {
4124
+ @Cron(CronExpression.EVERY_MINUTE, { pattern: 'cron.test' })
4125
+ getData() {
4126
+ return { ts: Date.now() };
4127
+ }
4128
+ }
4129
+
4130
+ @Module({ controllers: [AutoCronController] })
4131
+ class AutoCronModule {}
4132
+
4133
+ const app = createTestApp(AutoCronModule, { port: 0 });
4134
+ await app.start();
4135
+
4136
+ expect(app.getQueueService()).not.toBeNull();
4137
+
4138
+ await app.stop();
4139
+ });
4140
+
4141
+ test('auto-enables queue when controller has @Interval', async () => {
4142
+
4143
+ const intervalMs = 5000;
4144
+
4145
+ @Controller('/auto-interval')
4146
+ class AutoIntervalController extends BaseController {
4147
+ @Interval(intervalMs, { pattern: 'interval.test' })
4148
+ getData() {
4149
+ return { ts: Date.now() };
4150
+ }
4151
+ }
4152
+
4153
+ @Module({ controllers: [AutoIntervalController] })
4154
+ class AutoIntervalModule {}
4155
+
4156
+ const app = createTestApp(AutoIntervalModule, { port: 0 });
4157
+ await app.start();
4158
+
4159
+ expect(app.getQueueService()).not.toBeNull();
4160
+
4161
+ await app.stop();
4162
+ });
4163
+
4164
+ test('auto-enables queue when controller has @Timeout', async () => {
4165
+
4166
+ const timeoutMs = 1000;
4167
+
4168
+ @Controller('/auto-timeout')
4169
+ class AutoTimeoutController extends BaseController {
4170
+ @Timeout(timeoutMs, { pattern: 'timeout.test' })
4171
+ getData() {
4172
+ return { ts: Date.now() };
4173
+ }
4174
+ }
4175
+
4176
+ @Module({ controllers: [AutoTimeoutController] })
4177
+ class AutoTimeoutModule {}
4178
+
4179
+ const app = createTestApp(AutoTimeoutModule, { port: 0 });
4180
+ await app.start();
4181
+
4182
+ expect(app.getQueueService()).not.toBeNull();
4183
+
4184
+ await app.stop();
4185
+ });
4186
+
4187
+ test('does not enable queue when no queue decorators present', async () => {
4188
+ @Controller('/plain')
4189
+ class PlainController extends BaseController {
4190
+ @Get('/')
4191
+ async index(): Promise<OneBunResponse> {
4192
+ return this.success({ ok: true });
4193
+ }
4194
+ }
4195
+
4196
+ @Module({ controllers: [PlainController] })
4197
+ class PlainModule {}
4198
+
4199
+ const app = createTestApp(PlainModule, { port: 0 });
4200
+ await app.start();
4201
+
4202
+ expect(app.getQueueService()).toBeNull();
4203
+
4204
+ await app.stop();
4205
+ });
4206
+
4207
+ test('does not enable queue when only lifecycle decorators present', async () => {
4208
+ @Controller('/lifecycle-only')
4209
+ class LifecycleOnlyController extends BaseController {
4210
+ @OnQueueReady()
4211
+ onReady() {
4212
+ noop();
4213
+ }
4214
+ }
4215
+
4216
+ @Module({ controllers: [LifecycleOnlyController] })
4217
+ class LifecycleOnlyModule {}
4218
+
4219
+ const app = createTestApp(LifecycleOnlyModule, { port: 0 });
4220
+ await app.start();
4221
+
4222
+ expect(app.getQueueService()).toBeNull();
4223
+
4224
+ await app.stop();
4225
+ });
4226
+
4227
+ test('enabled: false overrides auto-detection', async () => {
4228
+ @Controller('/force-off')
4229
+ class ForceOffController extends BaseController {
4230
+ @Subscribe('force.off')
4231
+ async handle(): Promise<void> {}
4232
+ }
4233
+
4234
+ @Module({ controllers: [ForceOffController] })
4235
+ class ForceOffModule {}
4236
+
4237
+ const app = createTestApp(ForceOffModule, {
4238
+ port: 0,
4239
+ queue: { enabled: false },
4240
+ });
4241
+ await app.start();
4242
+
4243
+ expect(app.getQueueService()).toBeNull();
4244
+
4245
+ await app.stop();
4246
+ });
4247
+
4248
+ test('auto-detects from nested child module', async () => {
4249
+ @Controller('/child-auto')
4250
+ class ChildAutoController extends BaseController {
4251
+ @Subscribe('child.auto')
4252
+ async handle(): Promise<void> {}
4253
+ }
4254
+
4255
+ @Module({ controllers: [ChildAutoController] })
4256
+ class ChildAutoModule {}
4257
+
4258
+ @Module({ imports: [ChildAutoModule] })
4259
+ class ParentAutoModule {}
4260
+
4261
+ const app = createTestApp(ParentAutoModule, { port: 0 });
4262
+ await app.start();
4263
+
4264
+ expect(app.getQueueService()).not.toBeNull();
4265
+
4266
+ await app.stop();
4267
+ });
4268
+
4269
+ test('auto-detects from deeply nested module (grandchild)', async () => {
4270
+ @Controller('/grandchild')
4271
+ class GrandchildController extends BaseController {
4272
+ @Subscribe('grandchild.event')
4273
+ async handle(): Promise<void> {}
4274
+ }
4275
+
4276
+ @Module({ controllers: [GrandchildController] })
4277
+ class GrandchildModule {}
4278
+
4279
+ @Module({ imports: [GrandchildModule] })
4280
+ class MiddleModule {}
4281
+
4282
+ @Module({ imports: [MiddleModule] })
4283
+ class TopModule {}
4284
+
4285
+ const app = createTestApp(TopModule, { port: 0 });
4286
+ await app.start();
4287
+
4288
+ expect(app.getQueueService()).not.toBeNull();
4289
+
4290
+ await app.stop();
4291
+ });
4292
+
4293
+ test('getQueueService() returns null when queue not enabled', async () => {
4294
+ @Controller('/no-queue')
4295
+ class NoQueueController extends BaseController {
4296
+ @Get('/')
4297
+ async index(): Promise<OneBunResponse> {
4298
+ return this.success({});
4299
+ }
4300
+ }
4301
+
4302
+ @Module({ controllers: [NoQueueController] })
4303
+ class NoQueueModule {}
4304
+
4305
+ const app = createTestApp(NoQueueModule, { port: 0 });
4306
+ await app.start();
4307
+
4308
+ const queueService = app.getQueueService();
4309
+ expect(queueService).toBeNull();
4310
+
4311
+ await app.stop();
4312
+ });
4313
+ });
4086
4314
  });
@@ -15,7 +15,11 @@ import {
15
15
  type RouteOptions,
16
16
  } from '../types';
17
17
 
18
- import { getConstructorParamTypes as getDesignParamTypes, Reflect } from './metadata';
18
+ import {
19
+ copyAllMetadata,
20
+ getConstructorParamTypes as getDesignParamTypes,
21
+ Reflect,
22
+ } from './metadata';
19
23
 
20
24
  /**
21
25
  * Metadata storage for controllers
@@ -174,6 +178,9 @@ export function controllerDecorator(basePath: string = '') {
174
178
  }
175
179
  }
176
180
 
181
+ // Copy all method-decorator metadata (queue decorators, etc.) from original to wrapped class
182
+ copyAllMetadata(target, WrappedController);
183
+
177
184
  // Copy metadata and static properties
178
185
  META_CONTROLLERS.set(WrappedController, metadata);
179
186
  META_CONTROLLERS.set(target, metadata); // Keep original for compatibility
@@ -43,6 +43,31 @@ export function defineMetadata(
43
43
  keyMetadata.set(propertyKey || '', metadataValue);
44
44
  }
45
45
 
46
+ /**
47
+ * Copy all metadata from one target to another.
48
+ * Used by @Controller to preserve method-decorator metadata (e.g. queue decorators)
49
+ * when wrapping the original class.
50
+ */
51
+ export function copyAllMetadata(source: object, destination: object): void {
52
+ const sourceMetadata = metadataStorage.get(source);
53
+ if (!sourceMetadata) {
54
+ return;
55
+ }
56
+
57
+ let destMetadata = metadataStorage.get(destination);
58
+ if (!destMetadata) {
59
+ destMetadata = new Map<string, Map<string | symbol, any>>();
60
+ metadataStorage.set(destination, destMetadata);
61
+ }
62
+
63
+ for (const [metadataKey, keyMap] of sourceMetadata) {
64
+ // Only copy if destination doesn't already have this key
65
+ if (!destMetadata.has(metadataKey)) {
66
+ destMetadata.set(metadataKey, new Map(keyMap));
67
+ }
68
+ }
69
+ }
70
+
46
71
  /**
47
72
  * Get metadata from a target object
48
73
  * @param metadataKey - The key for the metadata
@@ -22,7 +22,11 @@ import { Controller as BaseController } from '../module/controller';
22
22
  import { BaseService, Service } from '../module/service';
23
23
 
24
24
  import { createTestController, createTestService } from './service-helpers';
25
- import { createMockConfig, useFakeTimers } from './test-utils';
25
+ import {
26
+ createMockConfig,
27
+ createMockLogger,
28
+ useFakeTimers,
29
+ } from './test-utils';
26
30
  import { TestingModule } from './testing-module';
27
31
 
28
32
  // ============================================================================
@@ -161,6 +165,50 @@ describe('docs/testing.md — TestingModule', () => {
161
165
  await module?.close();
162
166
  }
163
167
  });
168
+
169
+ /**
170
+ * @source docs/testing.md#overrideproviderserviceclass
171
+ */
172
+ it('overrideProvider — replaces service with mock value', async () => {
173
+ const mockUser = { id: '1', name: 'MockUser' };
174
+ let module: CompiledTestingModule | undefined;
175
+
176
+ try {
177
+ module = await TestingModule
178
+ .create({ controllers: [UserController], providers: [UserService] })
179
+ .overrideProvider(UserService).useValue({ findById: () => mockUser })
180
+ .compile();
181
+
182
+ const response = await module.inject('GET', '/users/1');
183
+
184
+ expect(response.status).toBe(200);
185
+
186
+ const body = await response.json() as { result: { id: string; name: string } };
187
+ expect(body.result.name).toBe('MockUser');
188
+ } finally {
189
+ await module?.close();
190
+ }
191
+ });
192
+
193
+ /**
194
+ * @source docs/testing.md#setoptionsoptions
195
+ */
196
+ it('setOptions — applies basePath to routes', async () => {
197
+ let module: CompiledTestingModule | undefined;
198
+
199
+ try {
200
+ module = await TestingModule
201
+ .create({ controllers: [UserController], providers: [UserService] })
202
+ .setOptions({ basePath: '/api' })
203
+ .compile();
204
+
205
+ const response = await module.inject('GET', '/api/users/1');
206
+
207
+ expect(response.status).toBe(200);
208
+ } finally {
209
+ await module?.close();
210
+ }
211
+ });
164
212
  });
165
213
 
166
214
  // ============================================================================
@@ -191,6 +239,24 @@ describe('docs/testing.md — useFakeTimers', () => {
191
239
  });
192
240
  });
193
241
 
242
+ // ============================================================================
243
+ // createMockLogger — docs/testing.md
244
+ // ============================================================================
245
+
246
+ describe('docs/testing.md — createMockLogger', () => {
247
+ /**
248
+ * @source docs/testing.md#createmocklogger
249
+ */
250
+ it('basic usage — creates silent async logger', () => {
251
+ const logger = createMockLogger();
252
+
253
+ expect(logger).toBeDefined();
254
+ expect(typeof logger.info).toBe('function');
255
+ expect(typeof logger.child).toBe('function');
256
+ expect(logger.child({ context: 'test' })).toBe(logger);
257
+ });
258
+ });
259
+
194
260
  // ============================================================================
195
261
  // createMockConfig — docs/testing.md
196
262
  // ============================================================================