@onebun/core 0.2.9 → 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.9",
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",
@@ -51,6 +51,14 @@
51
51
  "bun-types": "^1.3.8",
52
52
  "testcontainers": "^11.7.1"
53
53
  },
54
+ "peerDependencies": {
55
+ "testcontainers": ">=10.0.0"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "testcontainers": {
59
+ "optional": true
60
+ }
61
+ },
54
62
  "engines": {
55
63
  "bun": ">=1.2.12"
56
64
  }
@@ -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