@veloxts/core 0.7.7 → 0.7.9
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/CHANGELOG.md +12 -0
- package/dist/app.d.ts +18 -0
- package/dist/app.js +74 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/module/define-module.d.ts +58 -0
- package/dist/module/define-module.js +73 -0
- package/dist/module/index.d.ts +11 -0
- package/dist/module/index.js +10 -0
- package/dist/module/register.d.ts +26 -0
- package/dist/module/register.js +92 -0
- package/dist/module/types.d.ts +103 -0
- package/dist/module/types.js +10 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @veloxts/core
|
|
2
2
|
|
|
3
|
+
## 0.7.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- feat(router): swagger auto-discovery of module collections
|
|
8
|
+
|
|
9
|
+
## 0.7.8
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- New feature: Domain Module. defineModule() implementation
|
|
14
|
+
|
|
3
15
|
## 0.7.7
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/app.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @module app
|
|
5
5
|
*/
|
|
6
6
|
import { type FastifyInstance, type FastifyPluginAsync } from 'fastify';
|
|
7
|
+
import type { ServiceDefinitions, VeloxModule } from './module/types.js';
|
|
7
8
|
import type { PluginOptions, VeloxPlugin } from './plugin.js';
|
|
8
9
|
import type { StaticOptions } from './plugins/static.js';
|
|
9
10
|
import type { ShutdownHandler } from './types.js';
|
|
@@ -41,6 +42,9 @@ export declare class VeloxApp {
|
|
|
41
42
|
private readonly _lifecycle;
|
|
42
43
|
private _isRunning;
|
|
43
44
|
private _address;
|
|
45
|
+
private readonly _bootHooks;
|
|
46
|
+
private readonly _moduleNames;
|
|
47
|
+
private readonly _serviceNames;
|
|
44
48
|
/**
|
|
45
49
|
* Creates a new VeloxApp instance
|
|
46
50
|
*
|
|
@@ -122,6 +126,20 @@ export declare class VeloxApp {
|
|
|
122
126
|
* ```
|
|
123
127
|
*/
|
|
124
128
|
register<Options extends PluginOptions>(plugin: VeloxPlugin<Options> | FastifyPluginAsync<Options>, options?: Options): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Registers a domain module with the application.
|
|
131
|
+
*
|
|
132
|
+
* Modules are self-contained vertical slices that bundle services,
|
|
133
|
+
* middleware, routes, and lifecycle hooks into a single unit.
|
|
134
|
+
*
|
|
135
|
+
* @param mod - VeloxModule created via defineModule()
|
|
136
|
+
* @returns The app instance for method chaining
|
|
137
|
+
* @throws {VeloxError} INVALID_MODULE - If module was not created via defineModule()
|
|
138
|
+
* @throws {VeloxError} DUPLICATE_MODULE - If a module with the same name is already registered
|
|
139
|
+
* @throws {VeloxError} SERVICE_NAME_COLLISION - If a service name conflicts with another module
|
|
140
|
+
* @throws {VeloxError} MODULE_REGISTRATION_TOO_LATE - If called after server has started
|
|
141
|
+
*/
|
|
142
|
+
module<TName extends string, TServices extends ServiceDefinitions>(mod: VeloxModule<TName, TServices>): this;
|
|
125
143
|
/**
|
|
126
144
|
* Registers routes with the application
|
|
127
145
|
*
|
package/dist/app.js
CHANGED
|
@@ -7,6 +7,8 @@ import fastify from 'fastify';
|
|
|
7
7
|
import fp from 'fastify-plugin';
|
|
8
8
|
import { setupContextHook } from './context.js';
|
|
9
9
|
import { ConflictError, isVeloxError, VeloxError } from './errors.js';
|
|
10
|
+
import { isVeloxModule } from './module/define-module.js';
|
|
11
|
+
import { createModulePlugin } from './module/register.js';
|
|
10
12
|
import { isFastifyPlugin, isVeloxPlugin, validatePluginMetadata } from './plugin.js';
|
|
11
13
|
import { requestLogger } from './plugins/request-logger.js';
|
|
12
14
|
import { registerStatic } from './plugins/static.js';
|
|
@@ -41,6 +43,9 @@ export class VeloxApp {
|
|
|
41
43
|
_lifecycle;
|
|
42
44
|
_isRunning = false;
|
|
43
45
|
_address = null;
|
|
46
|
+
_bootHooks = [];
|
|
47
|
+
_moduleNames = new Set();
|
|
48
|
+
_serviceNames = new Map();
|
|
44
49
|
/**
|
|
45
50
|
* Creates a new VeloxApp instance
|
|
46
51
|
*
|
|
@@ -232,6 +237,71 @@ export class VeloxApp {
|
|
|
232
237
|
}
|
|
233
238
|
throw new VeloxError('Invalid plugin: must be a VeloxPlugin object or FastifyPluginAsync function', 500, 'INVALID_PLUGIN_TYPE');
|
|
234
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Registers a domain module with the application.
|
|
242
|
+
*
|
|
243
|
+
* Modules are self-contained vertical slices that bundle services,
|
|
244
|
+
* middleware, routes, and lifecycle hooks into a single unit.
|
|
245
|
+
*
|
|
246
|
+
* @param mod - VeloxModule created via defineModule()
|
|
247
|
+
* @returns The app instance for method chaining
|
|
248
|
+
* @throws {VeloxError} INVALID_MODULE - If module was not created via defineModule()
|
|
249
|
+
* @throws {VeloxError} DUPLICATE_MODULE - If a module with the same name is already registered
|
|
250
|
+
* @throws {VeloxError} SERVICE_NAME_COLLISION - If a service name conflicts with another module
|
|
251
|
+
* @throws {VeloxError} MODULE_REGISTRATION_TOO_LATE - If called after server has started
|
|
252
|
+
*/
|
|
253
|
+
module(mod) {
|
|
254
|
+
if (this._isRunning) {
|
|
255
|
+
throw new VeloxError('Cannot register modules after server has started', 500, 'MODULE_REGISTRATION_TOO_LATE');
|
|
256
|
+
}
|
|
257
|
+
if (!isVeloxModule(mod)) {
|
|
258
|
+
throw new VeloxError('Invalid module: must be created via defineModule()', 500, 'INVALID_MODULE');
|
|
259
|
+
}
|
|
260
|
+
if (this._moduleNames.has(mod.name)) {
|
|
261
|
+
throw new VeloxError(`Module "${mod.name}" is already registered`, 500, 'DUPLICATE_MODULE');
|
|
262
|
+
}
|
|
263
|
+
// Check for service name collisions (all checks before any mutations)
|
|
264
|
+
const serviceNames = mod.config.services ? Object.keys(mod.config.services) : [];
|
|
265
|
+
for (const serviceName of serviceNames) {
|
|
266
|
+
const existingModule = this._serviceNames.get(serviceName);
|
|
267
|
+
if (existingModule) {
|
|
268
|
+
throw new VeloxError(`Service name "${serviceName}" in module "${mod.name}" conflicts with module "${existingModule}"`, 500, 'SERVICE_NAME_COLLISION');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// All checks passed — commit all mutations
|
|
272
|
+
this._moduleNames.add(mod.name);
|
|
273
|
+
for (const serviceName of serviceNames) {
|
|
274
|
+
this._serviceNames.set(serviceName, mod.name);
|
|
275
|
+
}
|
|
276
|
+
// Track resolved services for boot/shutdown callbacks
|
|
277
|
+
let resolvedServices;
|
|
278
|
+
const plugin = createModulePlugin(mod, (services) => {
|
|
279
|
+
resolvedServices = services;
|
|
280
|
+
});
|
|
281
|
+
// Register module as encapsulated Fastify plugin
|
|
282
|
+
this._server.register(plugin);
|
|
283
|
+
// Register boot hook (runs between ready() and listen())
|
|
284
|
+
if (mod.config.boot) {
|
|
285
|
+
const bootFn = mod.config.boot;
|
|
286
|
+
this._bootHooks.push(async () => {
|
|
287
|
+
if (resolvedServices) {
|
|
288
|
+
await bootFn(resolvedServices);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
// Register shutdown hook
|
|
293
|
+
if (mod.config.shutdown) {
|
|
294
|
+
const shutdownFn = mod.config.shutdown;
|
|
295
|
+
this.beforeShutdown(async () => {
|
|
296
|
+
if (!resolvedServices) {
|
|
297
|
+
log.warn(`Module "${mod.name}" shutdown skipped: services were never initialized`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
await shutdownFn(resolvedServices);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
235
305
|
/**
|
|
236
306
|
* Registers routes with the application
|
|
237
307
|
*
|
|
@@ -318,6 +388,10 @@ export class VeloxApp {
|
|
|
318
388
|
const startTime = performance.now();
|
|
319
389
|
try {
|
|
320
390
|
await this._server.ready();
|
|
391
|
+
// Execute module boot hooks (after plugins loaded, before accepting traffic)
|
|
392
|
+
for (const bootHook of this._bootHooks) {
|
|
393
|
+
await bootHook();
|
|
394
|
+
}
|
|
321
395
|
const address = await this._server.listen({
|
|
322
396
|
port: this._config.port,
|
|
323
397
|
host: this._config.host,
|
package/dist/errors.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { ERROR_CATALOG, ERROR_DOMAINS, type ErrorCatalogEntry, type ErrorCode, t
|
|
|
8
8
|
* Known error codes in the VeloxTS framework core
|
|
9
9
|
* Can be extended via declaration merging by plugins
|
|
10
10
|
*/
|
|
11
|
-
export type VeloxCoreErrorCode = 'VALIDATION_ERROR' | 'NOT_FOUND' | 'CONFLICT' | 'FORBIDDEN' | 'UNAUTHORIZED' | 'SERVICE_UNAVAILABLE' | 'RATE_LIMITED' | 'UNPROCESSABLE' | 'CONFIGURATION_ERROR' | 'PLUGIN_REGISTRATION_ERROR' | 'SERVER_ALREADY_RUNNING' | 'SERVER_NOT_RUNNING' | 'SERVER_START_ERROR' | 'SERVER_STOP_ERROR' | 'INVALID_PLUGIN_METADATA';
|
|
11
|
+
export type VeloxCoreErrorCode = 'VALIDATION_ERROR' | 'NOT_FOUND' | 'CONFLICT' | 'FORBIDDEN' | 'UNAUTHORIZED' | 'SERVICE_UNAVAILABLE' | 'RATE_LIMITED' | 'UNPROCESSABLE' | 'CONFIGURATION_ERROR' | 'PLUGIN_REGISTRATION_ERROR' | 'SERVER_ALREADY_RUNNING' | 'SERVER_NOT_RUNNING' | 'SERVER_START_ERROR' | 'SERVER_STOP_ERROR' | 'INVALID_PLUGIN_METADATA' | 'INVALID_MODULE' | 'INVALID_MODULE_NAME' | 'DUPLICATE_MODULE' | 'SERVICE_NAME_COLLISION' | 'MODULE_REGISTRATION_TOO_LATE';
|
|
12
12
|
/**
|
|
13
13
|
* Registry for error codes - allows plugins to extend via declaration merging
|
|
14
14
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -39,3 +39,5 @@ export type { FireAndForgetOptions } from './utils/fire-and-forget.js';
|
|
|
39
39
|
export { fireAndForget } from './utils/fire-and-forget.js';
|
|
40
40
|
export type { RetryOptions } from './utils/retry.js';
|
|
41
41
|
export { withRetry } from './utils/retry.js';
|
|
42
|
+
export type { InferModuleServices, InferServices, ModuleConfig, ModuleMiddleware, ServiceDefinition, ServiceDefinitions, VeloxModule, } from './module/index.js';
|
|
43
|
+
export { defineModule, isVeloxModule, MODULE_BRAND } from './module/index.js';
|
package/dist/index.js
CHANGED
|
@@ -36,3 +36,4 @@ export { requestLogger } from './plugins/request-logger.js';
|
|
|
36
36
|
export { createLogger } from './utils/logger.js';
|
|
37
37
|
export { fireAndForget } from './utils/fire-and-forget.js';
|
|
38
38
|
export { withRetry } from './utils/retry.js';
|
|
39
|
+
export { defineModule, isVeloxModule, MODULE_BRAND } from './module/index.js';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineModule() factory and isVeloxModule() type guard
|
|
3
|
+
*
|
|
4
|
+
* Creates self-contained vertical domain slices with services,
|
|
5
|
+
* middleware, routes, and lifecycle hooks.
|
|
6
|
+
*
|
|
7
|
+
* @module module/define-module
|
|
8
|
+
*/
|
|
9
|
+
import type { ModuleConfig, ServiceDefinitions, VeloxModule } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Defines a VeloxTS module — a self-contained vertical domain slice.
|
|
12
|
+
*
|
|
13
|
+
* Modules encapsulate services, middleware, routes, and lifecycle hooks
|
|
14
|
+
* for a specific domain (e.g., billing, analytics, notifications).
|
|
15
|
+
*
|
|
16
|
+
* @template TName - Literal string name of the module
|
|
17
|
+
* @template TServices - The service definitions provided by this module
|
|
18
|
+
* @param name - Unique module name (used as route prefix default)
|
|
19
|
+
* @param config - Module configuration
|
|
20
|
+
* @returns A frozen VeloxModule ready for registration
|
|
21
|
+
*
|
|
22
|
+
* @throws {VeloxError} If name is empty or whitespace-only
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* export const billingModule = defineModule('billing', {
|
|
27
|
+
* services: {
|
|
28
|
+
* stripe: {
|
|
29
|
+
* factory: () => new Stripe(process.env.STRIPE_KEY!),
|
|
30
|
+
* close: (s) => s.close(),
|
|
31
|
+
* },
|
|
32
|
+
* },
|
|
33
|
+
* routes: rest([billingProcedures]),
|
|
34
|
+
* boot: async ({ stripe }) => {
|
|
35
|
+
* await stripe.verifyConnection();
|
|
36
|
+
* },
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function defineModule<const TName extends string, TServices extends ServiceDefinitions = ServiceDefinitions>(name: TName, config: ModuleConfig<TServices>): VeloxModule<TName, TServices>;
|
|
41
|
+
/**
|
|
42
|
+
* Type guard to check if a value is a VeloxModule.
|
|
43
|
+
*
|
|
44
|
+
* Uses the MODULE_BRAND symbol for reliable runtime identification,
|
|
45
|
+
* avoiding false positives from duck-typing.
|
|
46
|
+
*
|
|
47
|
+
* @param value - Value to check
|
|
48
|
+
* @returns true if value is a VeloxModule created by defineModule()
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* if (isVeloxModule(someValue)) {
|
|
53
|
+
* console.log('Module name:', someValue.name);
|
|
54
|
+
* await app.module(someValue);
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function isVeloxModule(value: unknown): value is VeloxModule;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* defineModule() factory and isVeloxModule() type guard
|
|
3
|
+
*
|
|
4
|
+
* Creates self-contained vertical domain slices with services,
|
|
5
|
+
* middleware, routes, and lifecycle hooks.
|
|
6
|
+
*
|
|
7
|
+
* @module module/define-module
|
|
8
|
+
*/
|
|
9
|
+
import { VeloxError } from '../errors.js';
|
|
10
|
+
import { MODULE_BRAND } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Defines a VeloxTS module — a self-contained vertical domain slice.
|
|
13
|
+
*
|
|
14
|
+
* Modules encapsulate services, middleware, routes, and lifecycle hooks
|
|
15
|
+
* for a specific domain (e.g., billing, analytics, notifications).
|
|
16
|
+
*
|
|
17
|
+
* @template TName - Literal string name of the module
|
|
18
|
+
* @template TServices - The service definitions provided by this module
|
|
19
|
+
* @param name - Unique module name (used as route prefix default)
|
|
20
|
+
* @param config - Module configuration
|
|
21
|
+
* @returns A frozen VeloxModule ready for registration
|
|
22
|
+
*
|
|
23
|
+
* @throws {VeloxError} If name is empty or whitespace-only
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* export const billingModule = defineModule('billing', {
|
|
28
|
+
* services: {
|
|
29
|
+
* stripe: {
|
|
30
|
+
* factory: () => new Stripe(process.env.STRIPE_KEY!),
|
|
31
|
+
* close: (s) => s.close(),
|
|
32
|
+
* },
|
|
33
|
+
* },
|
|
34
|
+
* routes: rest([billingProcedures]),
|
|
35
|
+
* boot: async ({ stripe }) => {
|
|
36
|
+
* await stripe.verifyConnection();
|
|
37
|
+
* },
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function defineModule(name, config) {
|
|
42
|
+
if (name.trim() === '') {
|
|
43
|
+
throw new VeloxError('Module must have a non-empty name', 500, 'INVALID_MODULE_NAME');
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
[MODULE_BRAND]: true,
|
|
47
|
+
name,
|
|
48
|
+
config: Object.freeze({ ...config }),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Type guard to check if a value is a VeloxModule.
|
|
53
|
+
*
|
|
54
|
+
* Uses the MODULE_BRAND symbol for reliable runtime identification,
|
|
55
|
+
* avoiding false positives from duck-typing.
|
|
56
|
+
*
|
|
57
|
+
* @param value - Value to check
|
|
58
|
+
* @returns true if value is a VeloxModule created by defineModule()
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* if (isVeloxModule(someValue)) {
|
|
63
|
+
* console.log('Module name:', someValue.name);
|
|
64
|
+
* await app.module(someValue);
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export function isVeloxModule(value) {
|
|
69
|
+
if (typeof value !== 'object' || value === null) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return MODULE_BRAND in value && value[MODULE_BRAND] === true;
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module system for VeloxTS framework
|
|
3
|
+
*
|
|
4
|
+
* Provides defineModule() for creating self-contained vertical domain slices
|
|
5
|
+
* with services, middleware, routes, and lifecycle hooks.
|
|
6
|
+
*
|
|
7
|
+
* @module module
|
|
8
|
+
*/
|
|
9
|
+
export { defineModule, isVeloxModule } from './define-module.js';
|
|
10
|
+
export type { InferModuleServices, InferServices, ModuleConfig, ModuleMiddleware, ServiceDefinition, ServiceDefinitions, VeloxModule, } from './types.js';
|
|
11
|
+
export { MODULE_BRAND } from './types.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module system for VeloxTS framework
|
|
3
|
+
*
|
|
4
|
+
* Provides defineModule() for creating self-contained vertical domain slices
|
|
5
|
+
* with services, middleware, routes, and lifecycle hooks.
|
|
6
|
+
*
|
|
7
|
+
* @module module
|
|
8
|
+
*/
|
|
9
|
+
export { defineModule, isVeloxModule } from './define-module.js';
|
|
10
|
+
export { MODULE_BRAND } from './types.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createModulePlugin() — creates a Fastify plugin from a VeloxModule definition.
|
|
3
|
+
*
|
|
4
|
+
* The plugin creates an encapsulated scope that:
|
|
5
|
+
* 1. Creates service instances via factories (sync or async)
|
|
6
|
+
* 2. Injects services into every request via decorateRequest + onRequest hook
|
|
7
|
+
* 3. Applies module middleware as onRequest hooks
|
|
8
|
+
* 4. Registers routes with the module prefix (auto from name, custom, or disabled)
|
|
9
|
+
* 5. Adds onClose hooks for service cleanup
|
|
10
|
+
*
|
|
11
|
+
* @module module/register
|
|
12
|
+
*/
|
|
13
|
+
import type { FastifyInstance } from 'fastify';
|
|
14
|
+
import type { InferServices, ServiceDefinitions, VeloxModule } from './types.js';
|
|
15
|
+
/**
|
|
16
|
+
* Creates a Fastify plugin from a VeloxModule definition.
|
|
17
|
+
*
|
|
18
|
+
* @param mod - The module definition
|
|
19
|
+
* @param onServicesResolved - Optional callback invoked after all services are created,
|
|
20
|
+
* during plugin registration (before server.ready()). Used by app.module() to capture
|
|
21
|
+
* resolved service references for boot/shutdown hooks.
|
|
22
|
+
* @returns A Fastify plugin function
|
|
23
|
+
*
|
|
24
|
+
* @internal Used by app.module() — not typically called directly.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createModulePlugin<TName extends string, TServices extends ServiceDefinitions>(mod: VeloxModule<TName, TServices>, onServicesResolved?: (services: InferServices<TServices>) => void): (server: FastifyInstance) => Promise<void>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createModulePlugin() — creates a Fastify plugin from a VeloxModule definition.
|
|
3
|
+
*
|
|
4
|
+
* The plugin creates an encapsulated scope that:
|
|
5
|
+
* 1. Creates service instances via factories (sync or async)
|
|
6
|
+
* 2. Injects services into every request via decorateRequest + onRequest hook
|
|
7
|
+
* 3. Applies module middleware as onRequest hooks
|
|
8
|
+
* 4. Registers routes with the module prefix (auto from name, custom, or disabled)
|
|
9
|
+
* 5. Adds onClose hooks for service cleanup
|
|
10
|
+
*
|
|
11
|
+
* @module module/register
|
|
12
|
+
*/
|
|
13
|
+
import { createLogger } from '../utils/logger.js';
|
|
14
|
+
const log = createLogger('module');
|
|
15
|
+
/**
|
|
16
|
+
* Creates a Fastify plugin from a VeloxModule definition.
|
|
17
|
+
*
|
|
18
|
+
* @param mod - The module definition
|
|
19
|
+
* @param onServicesResolved - Optional callback invoked after all services are created,
|
|
20
|
+
* during plugin registration (before server.ready()). Used by app.module() to capture
|
|
21
|
+
* resolved service references for boot/shutdown hooks.
|
|
22
|
+
* @returns A Fastify plugin function
|
|
23
|
+
*
|
|
24
|
+
* @internal Used by app.module() — not typically called directly.
|
|
25
|
+
*/
|
|
26
|
+
export function createModulePlugin(mod, onServicesResolved) {
|
|
27
|
+
const { config } = mod;
|
|
28
|
+
return async (server) => {
|
|
29
|
+
const services = {};
|
|
30
|
+
if (config.services) {
|
|
31
|
+
for (const [key, def] of Object.entries(config.services)) {
|
|
32
|
+
const service = await def.factory();
|
|
33
|
+
services[key] = service;
|
|
34
|
+
server.decorateRequest(key, undefined);
|
|
35
|
+
server.addHook('onRequest', async (request) => {
|
|
36
|
+
request[key] = service;
|
|
37
|
+
});
|
|
38
|
+
// Cleanup on close
|
|
39
|
+
if (def.close) {
|
|
40
|
+
addCloseHook(server, service, def.close);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Notify caller of resolved services (for boot/shutdown)
|
|
45
|
+
if (onServicesResolved) {
|
|
46
|
+
onServicesResolved(services);
|
|
47
|
+
}
|
|
48
|
+
// Apply module middleware
|
|
49
|
+
if (config.middleware) {
|
|
50
|
+
for (const mw of config.middleware) {
|
|
51
|
+
server.addHook('onRequest', mw);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Register routes with prefix
|
|
55
|
+
if (config.routes) {
|
|
56
|
+
const prefix = resolvePrefix(mod.name, config.prefix);
|
|
57
|
+
if (prefix) {
|
|
58
|
+
await server.register(config.routes, { prefix });
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await server.register(config.routes);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
log.debug(`Module "${mod.name}" registered`);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Registers an onClose hook with properly correlated types.
|
|
69
|
+
* Object.entries() erases per-key generics, so we use a helper
|
|
70
|
+
* to preserve the relationship between the service and its close function.
|
|
71
|
+
*/
|
|
72
|
+
function addCloseHook(server, service, closeFn) {
|
|
73
|
+
server.addHook('onClose', async () => {
|
|
74
|
+
await closeFn(service);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the effective route prefix for a module.
|
|
79
|
+
*
|
|
80
|
+
* - undefined -> /${name} (auto from module name)
|
|
81
|
+
* - false -> no prefix
|
|
82
|
+
* - string -> custom prefix (ensures leading slash)
|
|
83
|
+
*/
|
|
84
|
+
function resolvePrefix(name, prefix) {
|
|
85
|
+
if (prefix === false) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (typeof prefix === 'string') {
|
|
89
|
+
return prefix.startsWith('/') ? prefix : `/${prefix}`;
|
|
90
|
+
}
|
|
91
|
+
return `/${name}`;
|
|
92
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module system type definitions for VeloxTS framework
|
|
3
|
+
*
|
|
4
|
+
* Defines the shape of a VeloxTS module — a self-contained vertical domain slice
|
|
5
|
+
* with services, middleware, routes, and lifecycle hooks.
|
|
6
|
+
*
|
|
7
|
+
* @module module/types
|
|
8
|
+
*/
|
|
9
|
+
import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';
|
|
10
|
+
/**
|
|
11
|
+
* Service definition within a module.
|
|
12
|
+
* Each service has a factory and optional cleanup.
|
|
13
|
+
*/
|
|
14
|
+
export interface ServiceDefinition<T = unknown> {
|
|
15
|
+
/** Factory function to create the service instance */
|
|
16
|
+
factory: () => T | Promise<T>;
|
|
17
|
+
/** Optional cleanup called on server close */
|
|
18
|
+
close?: (service: T) => void | Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Record of named service definitions
|
|
22
|
+
*/
|
|
23
|
+
export type ServiceDefinitions = Record<string, ServiceDefinition>;
|
|
24
|
+
/**
|
|
25
|
+
* Infer resolved service instances from their definitions.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const services = {
|
|
30
|
+
* db: { factory: () => new Database() },
|
|
31
|
+
* cache: { factory: () => new Cache() },
|
|
32
|
+
* } satisfies ServiceDefinitions;
|
|
33
|
+
*
|
|
34
|
+
* type Resolved = InferServices<typeof services>;
|
|
35
|
+
* // { db: Database; cache: Cache }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export type InferServices<T extends ServiceDefinitions> = {
|
|
39
|
+
[K in keyof T]: T[K] extends ServiceDefinition<infer S> ? S : never;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Module middleware — a Fastify onRequest hook applied to all routes in the module scope.
|
|
43
|
+
*/
|
|
44
|
+
export type ModuleMiddleware = (request: FastifyRequest, reply: FastifyReply) => void | Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Configuration for defineModule().
|
|
47
|
+
*
|
|
48
|
+
* @template TServices - The service definitions provided by this module
|
|
49
|
+
*/
|
|
50
|
+
export interface ModuleConfig<TServices extends ServiceDefinitions = ServiceDefinitions> {
|
|
51
|
+
/** Services this module provides (created once, injected per-request) */
|
|
52
|
+
services?: TServices;
|
|
53
|
+
/** Module-wide middleware applied to all routes in the module scope */
|
|
54
|
+
middleware?: ModuleMiddleware[];
|
|
55
|
+
/**
|
|
56
|
+
* Fastify plugin for route registration.
|
|
57
|
+
* Typically the result of rest([...]) from @veloxts/router.
|
|
58
|
+
*/
|
|
59
|
+
routes?: FastifyPluginAsync;
|
|
60
|
+
/**
|
|
61
|
+
* REST route prefix. Defaults to /${moduleName}.
|
|
62
|
+
* Set false to disable auto-prefix.
|
|
63
|
+
* Set a string for a custom prefix.
|
|
64
|
+
*/
|
|
65
|
+
prefix?: string | false;
|
|
66
|
+
/** Called after all modules registered, before server starts listening */
|
|
67
|
+
boot?: (services: InferServices<TServices>) => void | Promise<void>;
|
|
68
|
+
/** Called during graceful shutdown */
|
|
69
|
+
shutdown?: (services: InferServices<TServices>) => void | Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
/** Brand symbol for VeloxModule type guard */
|
|
72
|
+
export declare const MODULE_BRAND: unique symbol;
|
|
73
|
+
/**
|
|
74
|
+
* A VeloxTS module — the return type of defineModule().
|
|
75
|
+
*
|
|
76
|
+
* Branded with a unique symbol for reliable runtime type checking
|
|
77
|
+
* via isVeloxModule().
|
|
78
|
+
*
|
|
79
|
+
* @template TName - Literal string name of the module
|
|
80
|
+
* @template TServices - The service definitions provided by this module
|
|
81
|
+
*/
|
|
82
|
+
export interface VeloxModule<TName extends string = string, TServices extends ServiceDefinitions = ServiceDefinitions> {
|
|
83
|
+
readonly [MODULE_BRAND]: true;
|
|
84
|
+
readonly name: TName;
|
|
85
|
+
readonly config: Readonly<ModuleConfig<TServices>>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Infer the services type from a VeloxModule instance.
|
|
89
|
+
* Useful for extending BaseContext via declaration merging.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* const billing = defineModule('billing', {
|
|
94
|
+
* services: {
|
|
95
|
+
* stripe: { factory: () => new Stripe(process.env.STRIPE_KEY!) },
|
|
96
|
+
* },
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* type BillingServices = InferModuleServices<typeof billing>;
|
|
100
|
+
* // { stripe: Stripe }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export type InferModuleServices<M> = M extends VeloxModule<string, infer S> ? InferServices<S> : never;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module system type definitions for VeloxTS framework
|
|
3
|
+
*
|
|
4
|
+
* Defines the shape of a VeloxTS module — a self-contained vertical domain slice
|
|
5
|
+
* with services, middleware, routes, and lifecycle hooks.
|
|
6
|
+
*
|
|
7
|
+
* @module module/types
|
|
8
|
+
*/
|
|
9
|
+
/** Brand symbol for VeloxModule type guard */
|
|
10
|
+
export const MODULE_BRAND = Symbol.for('velox:module');
|