@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
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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
|
// ============================================================================
|