@lenne.tech/nest-server 11.31.2 → 11.31.3
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/.claude/rules/architecture.md +2 -0
- package/.claude/rules/configurable-features.md +2 -0
- package/CLAUDE.md +28 -1
- package/FRAMEWORK-API.md +4 -1
- package/dist/config.env.d.ts +4 -0
- package/dist/config.env.js +32 -2
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/logging.helper.d.ts +1 -0
- package/dist/core/common/helpers/logging.helper.js +12 -0
- package/dist/core/common/helpers/logging.helper.js.map +1 -1
- package/dist/core/common/helpers/meta.helper.d.ts +1 -0
- package/dist/core/common/helpers/meta.helper.js +32 -1
- package/dist/core/common/helpers/meta.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +9 -0
- package/dist/core/common/services/email.service.d.ts +3 -1
- package/dist/core/common/services/email.service.js +33 -2
- package/dist/core/common/services/email.service.js.map +1 -1
- package/dist/core/common/services/template.service.js +9 -4
- package/dist/core/common/services/template.service.js.map +1 -1
- package/dist/core/modules/hub/core-hub-actions.controller.d.ts +22 -0
- package/dist/core/modules/hub/core-hub-actions.controller.js +141 -0
- package/dist/core/modules/hub/core-hub-actions.controller.js.map +1 -0
- package/dist/core/modules/hub/core-hub-html.service.d.ts +8 -0
- package/dist/core/modules/hub/core-hub-html.service.js +123 -0
- package/dist/core/modules/hub/core-hub-html.service.js.map +1 -0
- package/dist/core/modules/hub/core-hub.controller.d.ts +56 -0
- package/dist/core/modules/hub/core-hub.controller.js +398 -0
- package/dist/core/modules/hub/core-hub.controller.js.map +1 -0
- package/dist/core/modules/hub/core-hub.module.d.ts +17 -0
- package/dist/core/modules/hub/core-hub.module.js +109 -0
- package/dist/core/modules/hub/core-hub.module.js.map +1 -0
- package/dist/core/modules/hub/core-hub.service.d.ts +18 -0
- package/dist/core/modules/hub/core-hub.service.js +153 -0
- package/dist/core/modules/hub/core-hub.service.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.js +755 -0
- package/dist/core/modules/hub/helpers/hub-client-js.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.js +33 -0
- package/dist/core/modules/hub/helpers/hub-command-shape.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.d.ts +1 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.js +46 -0
- package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.d.ts +10 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.js +35 -0
- package/dist/core/modules/hub/helpers/hub-mermaid.helper.js.map +1 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.d.ts +9 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.js +62 -0
- package/dist/core/modules/hub/helpers/hub-shell.helper.js.map +1 -0
- package/dist/core/modules/hub/hub-action-messages.d.ts +17 -0
- package/dist/core/modules/hub/hub-action-messages.js +21 -0
- package/dist/core/modules/hub/hub-action-messages.js.map +1 -0
- package/dist/core/modules/hub/hub-config.helper.d.ts +11 -0
- package/dist/core/modules/hub/hub-config.helper.js +164 -0
- package/dist/core/modules/hub/hub-config.helper.js.map +1 -0
- package/dist/core/modules/hub/hub-nav.d.ts +12 -0
- package/dist/core/modules/hub/hub-nav.js +43 -0
- package/dist/core/modules/hub/hub-nav.js.map +1 -0
- package/dist/core/modules/hub/hub-ring-buffer.d.ts +22 -0
- package/dist/core/modules/hub/hub-ring-buffer.js +59 -0
- package/dist/core/modules/hub/hub-ring-buffer.js.map +1 -0
- package/dist/core/modules/hub/hub.constants.d.ts +18 -0
- package/dist/core/modules/hub/hub.constants.js +22 -0
- package/dist/core/modules/hub/hub.constants.js.map +1 -0
- package/dist/core/modules/hub/index.d.ts +25 -0
- package/dist/core/modules/hub/index.js +42 -0
- package/dist/core/modules/hub/index.js.map +1 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.d.ts +115 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.js +3 -0
- package/dist/core/modules/hub/interfaces/hub-config.interface.js.map +1 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.d.ts +231 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.js +3 -0
- package/dist/core/modules/hub/interfaces/hub-panels.interface.js.map +1 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.d.ts +8 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.js +50 -0
- package/dist/core/modules/hub/middleware/hub-trace.middleware.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.d.ts +48 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.js +136 -0
- package/dist/core/modules/hub/services/core-hub-actions.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-db.service.d.ts +19 -0
- package/dist/core/modules/hub/services/core-hub-db.service.js +180 -0
- package/dist/core/modules/hub/services/core-hub-db.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-email.service.d.ts +24 -0
- package/dist/core/modules/hub/services/core-hub-email.service.js +148 -0
- package/dist/core/modules/hub/services/core-hub-email.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +20 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js +103 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.d.ts +25 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.js +115 -0
- package/dist/core/modules/hub/services/core-hub-migrations.service.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.d.ts +28 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.js +187 -0
- package/dist/core/modules/hub/services/core-hub-sources.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +24 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.js +210 -0
- package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +38 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.js +235 -0
- package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +18 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js +123 -0
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -0
- package/dist/core.module.js +42 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +1 -0
- package/migration-guides/11.31.2-to-11.31.3.md +135 -0
- package/package.json +5 -4
- package/src/config.env.ts +75 -2
- package/src/core/common/helpers/logging.helper.spec.ts +61 -0
- package/src/core/common/helpers/logging.helper.ts +48 -0
- package/src/core/common/helpers/meta.helper.ts +46 -1
- package/src/core/common/interfaces/server-options.interface.ts +46 -0
- package/src/core/common/services/email.service.ts +33 -1
- package/src/core/common/services/template.service.ts +21 -16
- package/src/core/modules/hub/INTEGRATION-CHECKLIST.md +64 -0
- package/src/core/modules/hub/README.md +159 -0
- package/src/core/modules/hub/core-hub-actions.controller.ts +137 -0
- package/src/core/modules/hub/core-hub-html.service.ts +135 -0
- package/src/core/modules/hub/core-hub.controller.ts +286 -0
- package/src/core/modules/hub/core-hub.module.spec.ts +108 -0
- package/src/core/modules/hub/core-hub.module.ts +159 -0
- package/src/core/modules/hub/core-hub.service.ts +169 -0
- package/src/core/modules/hub/helpers/hub-client-js.helper.ts +768 -0
- package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +48 -0
- package/src/core/modules/hub/helpers/hub-command-shape.helper.ts +47 -0
- package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +67 -0
- package/src/core/modules/hub/helpers/hub-mask.helper.ts +78 -0
- package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +54 -0
- package/src/core/modules/hub/helpers/hub-mermaid.helper.ts +62 -0
- package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +106 -0
- package/src/core/modules/hub/helpers/hub-shell.helper.ts +90 -0
- package/src/core/modules/hub/hub-action-messages.ts +47 -0
- package/src/core/modules/hub/hub-config.helper.spec.ts +108 -0
- package/src/core/modules/hub/hub-config.helper.ts +233 -0
- package/src/core/modules/hub/hub-nav.ts +66 -0
- package/src/core/modules/hub/hub-ring-buffer.spec.ts +95 -0
- package/src/core/modules/hub/hub-ring-buffer.ts +101 -0
- package/src/core/modules/hub/hub.constants.ts +84 -0
- package/src/core/modules/hub/index.ts +25 -0
- package/src/core/modules/hub/interfaces/hub-config.interface.ts +265 -0
- package/src/core/modules/hub/interfaces/hub-panels.interface.ts +186 -0
- package/src/core/modules/hub/middleware/hub-trace.middleware.ts +45 -0
- package/src/core/modules/hub/services/core-hub-actions.service.ts +133 -0
- package/src/core/modules/hub/services/core-hub-db.service.ts +185 -0
- package/src/core/modules/hub/services/core-hub-email.service.ts +158 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +116 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.ts +121 -0
- package/src/core/modules/hub/services/core-hub-migrations.service.ts +112 -0
- package/src/core/modules/hub/services/core-hub-sources.service.ts +194 -0
- package/src/core/modules/hub/services/hub-log-buffer.service.ts +252 -0
- package/src/core/modules/hub/services/hub-query-profiler.service.ts +274 -0
- package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +112 -0
- package/src/core/modules/hub/services/hub-trace-buffer.service.ts +134 -0
- package/src/core.module.ts +61 -1
- package/src/index.ts +6 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { Inject, Injectable, Logger, OnModuleDestroy, OnModuleInit, Optional } from '@nestjs/common';
|
|
2
|
+
import { InjectConnection } from '@nestjs/mongoose';
|
|
3
|
+
import { Connection } from 'mongoose';
|
|
4
|
+
|
|
5
|
+
import { redactSensitiveText } from '../../../common/helpers/logging.helper';
|
|
6
|
+
import { HUB_CONFIG, HUB_QUERY_PENDING_LIMIT } from '../hub.constants';
|
|
7
|
+
import { HubRingBuffer } from '../hub-ring-buffer';
|
|
8
|
+
import { normalizeCommandShape } from '../helpers/hub-command-shape.helper';
|
|
9
|
+
import { HubQueriesData, HubQueryRecord, HubQueryTemplate } from '../interfaces/hub-panels.interface';
|
|
10
|
+
import { ResolvedHubConfig } from '../interfaces/hub-config.interface';
|
|
11
|
+
|
|
12
|
+
interface ProfilerRecord extends HubQueryRecord {
|
|
13
|
+
timestamp: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface PendingCommand {
|
|
17
|
+
collection: string;
|
|
18
|
+
operation: string;
|
|
19
|
+
shape: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Admin/heartbeat/metadata commands that are pure noise for a query profiler.
|
|
24
|
+
*
|
|
25
|
+
* `dbstats` / `listcollections` / `listindexes` are metadata reads (issued, among others, by the
|
|
26
|
+
* Hub's own DB panel) and are never app-level queries worth profiling. `aggregate` and `find` are
|
|
27
|
+
* deliberately NOT ignored — they carry real query shapes — so the Hub's own `$collStats`/`find`
|
|
28
|
+
* self-noise on the DB/Files panels is inherent and cannot be filtered by command name alone.
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_IGNORE = new Set([
|
|
31
|
+
'authenticate',
|
|
32
|
+
'buildinfo',
|
|
33
|
+
'connectionstatus',
|
|
34
|
+
'dbstats',
|
|
35
|
+
'getnonce',
|
|
36
|
+
'getparameter',
|
|
37
|
+
'hello',
|
|
38
|
+
'ismaster',
|
|
39
|
+
'listcollections',
|
|
40
|
+
'listindexes',
|
|
41
|
+
'logout',
|
|
42
|
+
'ping',
|
|
43
|
+
'saslcontinue',
|
|
44
|
+
'saslstart',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/** Only these command names carry a query shape worth normalizing. */
|
|
48
|
+
const SHAPED_FIELDS = ['filter', 'query', 'pipeline', 'sort', 'projection', 'q', 'u', 'updates', 'deletes'];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* MongoDB query profiler backed by driver command monitoring.
|
|
52
|
+
*
|
|
53
|
+
* Requires the connection to be created with `monitorCommands: true` (opted in from core.module.ts
|
|
54
|
+
* only when this collector is enabled). Records command SHAPES (values → '?') — never values — which
|
|
55
|
+
* doubles as the N+1 template key. Zero cost when disabled: no `getClient`, no listeners.
|
|
56
|
+
*/
|
|
57
|
+
@Injectable()
|
|
58
|
+
export class HubQueryProfilerService implements OnModuleDestroy, OnModuleInit {
|
|
59
|
+
protected readonly logger = new Logger(HubQueryProfilerService.name);
|
|
60
|
+
|
|
61
|
+
private buffer?: HubRingBuffer<ProfilerRecord>;
|
|
62
|
+
private client?: {
|
|
63
|
+
off?: (e: string, l: (...a: any[]) => void) => void;
|
|
64
|
+
on: (e: string, l: (...a: any[]) => void) => void;
|
|
65
|
+
options?: { monitorCommands?: boolean };
|
|
66
|
+
removeListener?: (e: string, l: (...a: any[]) => void) => void;
|
|
67
|
+
};
|
|
68
|
+
private cfg?: Exclude<ResolvedHubConfig['collectors']['queries'], false>;
|
|
69
|
+
private ignore = DEFAULT_IGNORE;
|
|
70
|
+
// Hot-path driver-event handlers. Each is fully guarded: an exception here runs inside the MongoDB
|
|
71
|
+
// driver's synchronous `emit()`, so it would surface as an UNCAUGHT exception and take the whole
|
|
72
|
+
// process down. Observability must never crash the app — swallow everything.
|
|
73
|
+
private readonly onFailed = (e: any): void => this.safe(() => this.handleFinished(e, e?.failure?.message));
|
|
74
|
+
private readonly onStarted = (e: any): void => this.safe(() => this.handleStarted(e));
|
|
75
|
+
private readonly onSucceeded = (e: any): void => this.safe(() => this.handleFinished(e, undefined));
|
|
76
|
+
private readonly pending = new Map<number, PendingCommand>();
|
|
77
|
+
|
|
78
|
+
constructor(
|
|
79
|
+
@Inject(HUB_CONFIG) protected readonly config: ResolvedHubConfig,
|
|
80
|
+
@Optional() @InjectConnection() protected readonly connection?: Connection,
|
|
81
|
+
) {}
|
|
82
|
+
|
|
83
|
+
/** Clear the buffer (Hub action). */
|
|
84
|
+
clear(): void {
|
|
85
|
+
this.buffer?.clear();
|
|
86
|
+
this.pending.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get enabled(): boolean {
|
|
90
|
+
return this.config.collectors.queries !== false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getData(): HubQueriesData {
|
|
94
|
+
const records = this.buffer?.recent() ?? [];
|
|
95
|
+
return {
|
|
96
|
+
cursor: this.buffer?.lastSeq ?? -1,
|
|
97
|
+
recent: records.slice(-100),
|
|
98
|
+
slowest: [...records].sort((a, b) => b.durationMs - a.durationMs).slice(0, 10),
|
|
99
|
+
summary: this.summary(records),
|
|
100
|
+
topTemplates: this.topTemplates(records),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
onModuleDestroy(): void {
|
|
105
|
+
if (!this.client) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const off = this.client.removeListener ?? this.client.off;
|
|
109
|
+
off?.call(this.client, 'commandStarted', this.onStarted);
|
|
110
|
+
off?.call(this.client, 'commandSucceeded', this.onSucceeded);
|
|
111
|
+
off?.call(this.client, 'commandFailed', this.onFailed);
|
|
112
|
+
this.pending.clear();
|
|
113
|
+
this.client = undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
onModuleInit(): void {
|
|
117
|
+
if (this.config.collectors.queries === false) {
|
|
118
|
+
return; // zero cost when disabled
|
|
119
|
+
}
|
|
120
|
+
this.cfg = this.config.collectors.queries;
|
|
121
|
+
this.buffer = new HubRingBuffer<ProfilerRecord>(this.cfg.capacity);
|
|
122
|
+
if (this.cfg.ignoreCommands?.length) {
|
|
123
|
+
this.ignore = new Set(this.cfg.ignoreCommands.map((c) => c.toLowerCase()));
|
|
124
|
+
}
|
|
125
|
+
const client = this.connection?.getClient?.() as unknown as HubQueryProfilerService['client'];
|
|
126
|
+
if (!client) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (client.options?.monitorCommands !== true) {
|
|
130
|
+
this.logger.warn(
|
|
131
|
+
'hub.collectors.queries is enabled but the MongoDB driver was not started with monitorCommands — query profiling is inactive.',
|
|
132
|
+
);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.client = client;
|
|
136
|
+
client.on('commandStarted', this.onStarted);
|
|
137
|
+
client.on('commandSucceeded', this.onSucceeded);
|
|
138
|
+
client.on('commandFailed', this.onFailed);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Run a hot-path handler, swallowing any error so it can never escape into the driver's emit(). */
|
|
142
|
+
protected safe(fn: () => void): void {
|
|
143
|
+
try {
|
|
144
|
+
fn();
|
|
145
|
+
} catch {
|
|
146
|
+
/* observability must never crash the app */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
protected classify(durationMs: number): HubQueryRecord['classification'] {
|
|
151
|
+
if (!this.cfg) {
|
|
152
|
+
return 'ok';
|
|
153
|
+
}
|
|
154
|
+
if (durationMs > this.cfg.criticalMs) {
|
|
155
|
+
return 'critical';
|
|
156
|
+
}
|
|
157
|
+
if (durationMs > this.cfg.warnMs) {
|
|
158
|
+
return 'warn';
|
|
159
|
+
}
|
|
160
|
+
return 'ok';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
protected handleFinished(event: { duration?: number; requestId?: number }, errorMessage?: string): void {
|
|
164
|
+
const requestId = event?.requestId;
|
|
165
|
+
if (requestId === undefined) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const pending = this.pending.get(requestId);
|
|
169
|
+
this.pending.delete(requestId);
|
|
170
|
+
if (!pending || !this.buffer) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const durationMs = Number(event.duration ?? 0);
|
|
174
|
+
this.buffer.add({
|
|
175
|
+
classification: this.classify(durationMs),
|
|
176
|
+
collection: pending.collection,
|
|
177
|
+
commandSummary: pending.shape,
|
|
178
|
+
durationMs,
|
|
179
|
+
errorMessage: errorMessage ? redactSensitiveText(String(errorMessage)).slice(0, 300) : undefined,
|
|
180
|
+
failed: !!errorMessage,
|
|
181
|
+
operation: pending.operation,
|
|
182
|
+
requestId,
|
|
183
|
+
} as Omit<ProfilerRecord, 'seq' | 'timestamp'>);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
protected handleStarted(event: {
|
|
187
|
+
command?: Record<string, unknown>;
|
|
188
|
+
commandName?: string;
|
|
189
|
+
requestId?: number;
|
|
190
|
+
}): void {
|
|
191
|
+
const operation = String(event?.commandName ?? '').toLowerCase();
|
|
192
|
+
if (!operation || this.ignore.has(operation) || event.requestId === undefined) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const command = event.command ?? {};
|
|
196
|
+
const collection = this.extractCollection(command, operation);
|
|
197
|
+
const shape = this.buildShape(operation, collection, command);
|
|
198
|
+
|
|
199
|
+
// Bound the pending map even if succeeded/failed events are lost (connection drop).
|
|
200
|
+
if (this.pending.size >= HUB_QUERY_PENDING_LIMIT) {
|
|
201
|
+
const oldest = this.pending.keys().next().value;
|
|
202
|
+
if (oldest !== undefined) {
|
|
203
|
+
this.pending.delete(oldest);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
this.pending.set(event.requestId, { collection, operation, shape });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private buildShape(operation: string, collection: string, command: Record<string, unknown>): string {
|
|
210
|
+
const sections: Record<string, unknown> = {};
|
|
211
|
+
for (const field of SHAPED_FIELDS) {
|
|
212
|
+
if (command[field] !== undefined) {
|
|
213
|
+
sections[field] = normalizeCommandShape(command[field]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (Array.isArray(command.documents)) {
|
|
217
|
+
sections.documents = `documents(${command.documents.length})`;
|
|
218
|
+
}
|
|
219
|
+
const maxLen = this.cfg?.maxShapeLength ?? 512;
|
|
220
|
+
return `${operation} ${collection} ${JSON.stringify(sections)}`.slice(0, maxLen);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private extractCollection(command: Record<string, unknown>, operation: string): string {
|
|
224
|
+
const byName = command[operation] ?? command[Object.keys(command)[0]];
|
|
225
|
+
if (typeof byName === 'string') {
|
|
226
|
+
return byName;
|
|
227
|
+
}
|
|
228
|
+
if (typeof command.collection === 'string') {
|
|
229
|
+
return command.collection;
|
|
230
|
+
}
|
|
231
|
+
return '(unknown)';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private summary(records: ProfilerRecord[]): HubQueriesData['summary'] {
|
|
235
|
+
let total = 0;
|
|
236
|
+
let warnCount = 0;
|
|
237
|
+
let criticalCount = 0;
|
|
238
|
+
let failedCount = 0;
|
|
239
|
+
for (const r of records) {
|
|
240
|
+
total += r.durationMs;
|
|
241
|
+
if (r.classification === 'warn') {
|
|
242
|
+
warnCount++;
|
|
243
|
+
}
|
|
244
|
+
if (r.classification === 'critical') {
|
|
245
|
+
criticalCount++;
|
|
246
|
+
}
|
|
247
|
+
if (r.failed) {
|
|
248
|
+
failedCount++;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
avgMs: records.length ? total / records.length : 0,
|
|
253
|
+
criticalCount,
|
|
254
|
+
failedCount,
|
|
255
|
+
total: records.length,
|
|
256
|
+
warnCount,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private topTemplates(records: ProfilerRecord[]): HubQueryTemplate[] {
|
|
261
|
+
const map = new Map<string, { count: number; max: number; total: number }>();
|
|
262
|
+
for (const r of records) {
|
|
263
|
+
const entry = map.get(r.commandSummary) ?? { count: 0, max: 0, total: 0 };
|
|
264
|
+
entry.count++;
|
|
265
|
+
entry.total += r.durationMs;
|
|
266
|
+
entry.max = Math.max(entry.max, r.durationMs);
|
|
267
|
+
map.set(r.commandSummary, entry);
|
|
268
|
+
}
|
|
269
|
+
return [...map.entries()]
|
|
270
|
+
.map(([template, e]) => ({ avgMs: e.total / e.count, count: e.count, maxMs: e.max, template }))
|
|
271
|
+
.sort((a, b) => b.count - a.count)
|
|
272
|
+
.slice(0, 10);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { HubTraceBufferService } from './hub-trace-buffer.service';
|
|
4
|
+
import { ResolvedHubConfig } from '../interfaces/hub-config.interface';
|
|
5
|
+
|
|
6
|
+
function makeConfig(overrides: Partial<ResolvedHubConfig> = {}): ResolvedHubConfig {
|
|
7
|
+
return {
|
|
8
|
+
actions: true,
|
|
9
|
+
allowPublicAccessInProduction: false,
|
|
10
|
+
collectors: {
|
|
11
|
+
logs: false,
|
|
12
|
+
queries: false,
|
|
13
|
+
traces: { capacity: 50, captureGraphQlOperation: true, excludePaths: [], slowMs: 1000 },
|
|
14
|
+
},
|
|
15
|
+
db: false,
|
|
16
|
+
emailPreview: true,
|
|
17
|
+
env: 'local',
|
|
18
|
+
links: {},
|
|
19
|
+
loginEndpoint: '/iam/sign-in/email',
|
|
20
|
+
logoutEndpoint: '/iam/sign-out',
|
|
21
|
+
mailbox: false,
|
|
22
|
+
migrations: false,
|
|
23
|
+
path: 'hub',
|
|
24
|
+
pollIntervalMs: 5000,
|
|
25
|
+
roles: ['admin'],
|
|
26
|
+
version: '1.0.0',
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Minimal Express req/res doubles for record(). */
|
|
32
|
+
function reqRes(path: string, method = 'GET', statusCode = 200) {
|
|
33
|
+
return {
|
|
34
|
+
req: { baseUrl: '', method, path, route: undefined } as never,
|
|
35
|
+
res: { getHeader: () => undefined, statusCode, writableFinished: true } as never,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('HubTraceBufferService', () => {
|
|
40
|
+
describe('isExcluded (boundary-aware)', () => {
|
|
41
|
+
it('excludes the Hub base path and its sub-paths', () => {
|
|
42
|
+
const service = new HubTraceBufferService(makeConfig());
|
|
43
|
+
expect(service.isExcluded('/hub')).toBe(true);
|
|
44
|
+
expect(service.isExcluded('/hub/logs.json')).toBe(true);
|
|
45
|
+
expect(service.isExcluded('/hub/actions/collectors/traces/clear')).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('does NOT exclude a sibling route that merely shares the prefix string', () => {
|
|
49
|
+
const service = new HubTraceBufferService(makeConfig());
|
|
50
|
+
// Regression guard for the over-broad `startsWith(prefix)` bug: `/hubble` is not a Hub route.
|
|
51
|
+
expect(service.isExcluded('/hubble')).toBe(false);
|
|
52
|
+
expect(service.isExcluded('/hub-admin')).toBe(false);
|
|
53
|
+
expect(service.isExcluded('/hubbard/x')).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('honours additional configured exclude prefixes with the same boundary rule', () => {
|
|
57
|
+
const service = new HubTraceBufferService(
|
|
58
|
+
makeConfig({
|
|
59
|
+
collectors: {
|
|
60
|
+
logs: false,
|
|
61
|
+
queries: false,
|
|
62
|
+
traces: { capacity: 50, captureGraphQlOperation: true, excludePaths: ['/health'], slowMs: 1000 },
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
expect(service.isExcluded('/health')).toBe(true);
|
|
67
|
+
expect(service.isExcluded('/health/live')).toBe(true);
|
|
68
|
+
expect(service.isExcluded('/healthcheck')).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('record + getData', () => {
|
|
73
|
+
it('records a completed request with method, path and duration, and summarises', () => {
|
|
74
|
+
const service = new HubTraceBufferService(makeConfig());
|
|
75
|
+
const { req, res } = reqRes('/permissions/json');
|
|
76
|
+
service.record(req, res, 12.5, false, '/permissions/json');
|
|
77
|
+
|
|
78
|
+
const data = service.getData();
|
|
79
|
+
expect(data.traces).toHaveLength(1);
|
|
80
|
+
expect(data.traces[0].method).toBe('GET');
|
|
81
|
+
expect(data.traces[0].path).toBe('/permissions/json');
|
|
82
|
+
expect(data.traces[0].durationMs).toBe(12.5);
|
|
83
|
+
expect(data.summary.total).toBe(1);
|
|
84
|
+
expect(data.summary.avgMs).toBe(12.5);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('flags slow and error traces in the summary', () => {
|
|
88
|
+
const service = new HubTraceBufferService(makeConfig());
|
|
89
|
+
const slow = reqRes('/slow');
|
|
90
|
+
service.record(slow.req, slow.res, 5000, false, '/slow');
|
|
91
|
+
const err = reqRes('/boom', 'GET', 500);
|
|
92
|
+
service.record(err.req, err.res, 3, false, '/boom');
|
|
93
|
+
|
|
94
|
+
const data = service.getData();
|
|
95
|
+
expect(data.summary.slowCount).toBe(1);
|
|
96
|
+
expect(data.summary.errorCount).toBe(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('supports cursor-based polling via since', () => {
|
|
100
|
+
const service = new HubTraceBufferService(makeConfig());
|
|
101
|
+
const a = reqRes('/a');
|
|
102
|
+
service.record(a.req, a.res, 1, false, '/a');
|
|
103
|
+
const cursor = service.getData().cursor;
|
|
104
|
+
const b = reqRes('/b');
|
|
105
|
+
service.record(b.req, b.res, 2, false, '/b');
|
|
106
|
+
|
|
107
|
+
const next = service.getData(cursor);
|
|
108
|
+
expect(next.traces).toHaveLength(1);
|
|
109
|
+
expect(next.traces[0].path).toBe('/b');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
import { Request, Response } from 'express';
|
|
3
|
+
|
|
4
|
+
import { HUB_CONFIG } from '../hub.constants';
|
|
5
|
+
import { HubRingBuffer } from '../hub-ring-buffer';
|
|
6
|
+
import { HubTraceRecord, HubTracesData } from '../interfaces/hub-panels.interface';
|
|
7
|
+
import { ResolvedHubConfig } from '../interfaces/hub-config.interface';
|
|
8
|
+
|
|
9
|
+
interface TraceRecord extends HubTraceRecord {
|
|
10
|
+
timestamp: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Ring buffer for HTTP request traces, fed by {@link HubTraceMiddleware}.
|
|
15
|
+
*
|
|
16
|
+
* Structurally incapable of leaking secrets: it stores only method, route pattern (never the query
|
|
17
|
+
* string), status, duration, an optional GraphQL operation name and the user id — no headers,
|
|
18
|
+
* cookies, bodies or GraphQL variables.
|
|
19
|
+
*/
|
|
20
|
+
@Injectable()
|
|
21
|
+
export class HubTraceBufferService {
|
|
22
|
+
private readonly buffer?: HubRingBuffer<TraceRecord>;
|
|
23
|
+
private readonly cfg?: Exclude<ResolvedHubConfig['collectors']['traces'], false>;
|
|
24
|
+
private readonly excludePrefixes: string[];
|
|
25
|
+
|
|
26
|
+
constructor(@Inject(HUB_CONFIG) protected readonly config: ResolvedHubConfig) {
|
|
27
|
+
if (config.collectors.traces !== false) {
|
|
28
|
+
this.cfg = config.collectors.traces;
|
|
29
|
+
this.buffer = new HubRingBuffer<TraceRecord>(this.cfg.capacity);
|
|
30
|
+
}
|
|
31
|
+
// Always exclude the Hub's own routes so polling does not flood the trace list with self-noise.
|
|
32
|
+
this.excludePrefixes = [...(this.cfg?.excludePaths ?? []), '/' + config.path];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
clear(): void {
|
|
36
|
+
this.buffer?.clear();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get enabled(): boolean {
|
|
40
|
+
return this.config.collectors.traces !== false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getData(since?: number): HubTracesData {
|
|
44
|
+
const records = !this.buffer ? [] : since === undefined ? this.buffer.recent() : this.buffer.since(since);
|
|
45
|
+
let totalMs = 0;
|
|
46
|
+
let slowCount = 0;
|
|
47
|
+
let errorCount = 0;
|
|
48
|
+
for (const r of records) {
|
|
49
|
+
totalMs += r.durationMs;
|
|
50
|
+
if (r.slow) {
|
|
51
|
+
slowCount++;
|
|
52
|
+
}
|
|
53
|
+
if (r.error) {
|
|
54
|
+
errorCount++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
cursor: this.buffer?.lastSeq ?? -1,
|
|
59
|
+
dropped: this.buffer?.firstRetainedSeq ?? -1,
|
|
60
|
+
summary: { avgMs: records.length ? totalMs / records.length : 0, errorCount, slowCount, total: records.length },
|
|
61
|
+
traces: records,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** True when the path is excluded (Hub routes, configured prefixes). */
|
|
66
|
+
isExcluded(path: string): boolean {
|
|
67
|
+
// Boundary-aware: a prefix of `/hub` excludes `/hub` and `/hub/...` but NOT `/hubble` — a bare
|
|
68
|
+
// `startsWith(prefix)` would wrongly drop sibling routes that merely share the prefix string.
|
|
69
|
+
return this.excludePrefixes.some((prefix) => path === prefix || path.startsWith(prefix + '/'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Record a completed request. Called from the middleware at `finish`/`close`. */
|
|
73
|
+
record(req: Request, res: Response, durationMs: number, aborted: boolean, fullPath?: string): void {
|
|
74
|
+
if (!this.buffer || !this.cfg) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const statusCode = res.statusCode;
|
|
78
|
+
const contentLength = Number(res.getHeader('content-length')) || undefined;
|
|
79
|
+
this.buffer.add({
|
|
80
|
+
aborted: aborted || undefined,
|
|
81
|
+
contentLength,
|
|
82
|
+
durationMs,
|
|
83
|
+
error: statusCode >= 500 || undefined,
|
|
84
|
+
graphqlOperation: this.cfg.captureGraphQlOperation ? this.graphqlOperation(req) : undefined,
|
|
85
|
+
method: req.method,
|
|
86
|
+
path: this.routePattern(req, fullPath),
|
|
87
|
+
slow: durationMs > this.cfg.slowMs || undefined,
|
|
88
|
+
statusCode,
|
|
89
|
+
userId: (req as unknown as { user?: { id?: string } }).user?.id,
|
|
90
|
+
} as Omit<TraceRecord, 'seq' | 'timestamp'>);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Collapse high-cardinality ids so the buffer does not explode on 404s / unmatched routes. */
|
|
94
|
+
private collapseParams(path: string): string {
|
|
95
|
+
return path
|
|
96
|
+
.replace(/\/[a-f0-9]{24}(?=\/|$)/gi, '/:id')
|
|
97
|
+
.replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?=\/|$)/gi, '/:id')
|
|
98
|
+
.replace(/\/\d+(?=\/|$)/g, '/:n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private graphqlOperation(req: Request): string | undefined {
|
|
102
|
+
if (req.method !== 'POST' || !req.path.endsWith('/graphql')) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const body = (req as unknown as { body?: unknown }).body;
|
|
106
|
+
if (Array.isArray(body)) {
|
|
107
|
+
return `batch(${body.length})`;
|
|
108
|
+
}
|
|
109
|
+
const op = (body as { operationName?: string; query?: string })?.operationName;
|
|
110
|
+
if (op) {
|
|
111
|
+
return op;
|
|
112
|
+
}
|
|
113
|
+
const query = (body as { query?: string })?.query;
|
|
114
|
+
if (typeof query === 'string') {
|
|
115
|
+
const match = /\b(query|mutation|subscription)\s+([A-Za-z_]\w*)/.exec(query.slice(0, 500));
|
|
116
|
+
if (match) {
|
|
117
|
+
return match[2];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return 'anonymous';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Route pattern when the router resolved one, else a param-collapsed raw path (never the query string). */
|
|
124
|
+
private routePattern(req: Request, fullPath?: string): string {
|
|
125
|
+
const route = (req as unknown as { route?: { path?: string } }).route?.path;
|
|
126
|
+
const base = req.baseUrl || '';
|
|
127
|
+
if (route && base) {
|
|
128
|
+
return base + (route === '/' ? '' : route) || '/';
|
|
129
|
+
}
|
|
130
|
+
// Fallback: the mount-stripped `req.path` is unreliable under `forRoutes('*')`; prefer the full
|
|
131
|
+
// path captured by the middleware, with high-cardinality ids collapsed.
|
|
132
|
+
return this.collapseParams(fullPath || req.baseUrl || req.path);
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/core.module.ts
CHANGED
|
@@ -43,6 +43,8 @@ import { CoreBetterAuthModule } from './core/modules/better-auth/core-better-aut
|
|
|
43
43
|
import { CoreBetterAuthService } from './core/modules/better-auth/core-better-auth.service';
|
|
44
44
|
import { ErrorCodeModule } from './core/modules/error-code/error-code.module';
|
|
45
45
|
import { CoreHealthCheckModule } from './core/modules/health-check/core-health-check.module';
|
|
46
|
+
import { CoreHubModule } from './core/modules/hub/core-hub.module';
|
|
47
|
+
import { isHubEnabled, isHubQueriesEnabled } from './core/modules/hub/hub-config.helper';
|
|
46
48
|
import { CorePermissionsModule } from './core/modules/permissions/core-permissions.module';
|
|
47
49
|
import { CoreSystemSetupModule } from './core/modules/system-setup/core-system-setup.module';
|
|
48
50
|
import { CoreTenantModule } from './core/modules/tenant/core-tenant.module';
|
|
@@ -335,6 +337,16 @@ export class CoreModule implements NestModule {
|
|
|
335
337
|
// and: https://mongoosejs.com/docs/guide.html#strictQuery
|
|
336
338
|
mongoose.set('strictQuery', config.mongoose.strictQuery || false);
|
|
337
339
|
|
|
340
|
+
// Opt the MongoDB driver into command monitoring only when the Hub query profiler is enabled
|
|
341
|
+
// (zero cost otherwise). An explicit `monitorCommands: false` by the consumer wins — the profiler
|
|
342
|
+
// then warns once and stays inert.
|
|
343
|
+
if (isHubQueriesEnabled(config.hub)) {
|
|
344
|
+
config.mongoose.options = config.mongoose.options || {};
|
|
345
|
+
if ((config.mongoose.options as { monitorCommands?: boolean }).monitorCommands === undefined) {
|
|
346
|
+
(config.mongoose.options as { monitorCommands?: boolean }).monitorCommands = true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
338
350
|
const imports: any[] = [MongooseModule.forRoot(config.mongoose.uri, config.mongoose.options)];
|
|
339
351
|
|
|
340
352
|
if (isGraphQlEnabled && config.graphQl) {
|
|
@@ -375,10 +387,39 @@ export class CoreModule implements NestModule {
|
|
|
375
387
|
|
|
376
388
|
// Permissions report (development tool)
|
|
377
389
|
const permissionsConfig = config.permissions;
|
|
378
|
-
|
|
390
|
+
const permissionsEnabled =
|
|
391
|
+
permissionsConfig === true || (typeof permissionsConfig === 'object' && permissionsConfig.enabled !== false);
|
|
392
|
+
if (permissionsEnabled) {
|
|
379
393
|
imports.push(CorePermissionsModule.forRoot(permissionsConfig));
|
|
380
394
|
}
|
|
381
395
|
|
|
396
|
+
// Hub admin area (operator cockpit). Presence implies enabled — but NEVER implicitly:
|
|
397
|
+
// it must be switched on per environment. Overrides pass project subclasses through.
|
|
398
|
+
if (isHubEnabled(config.hub)) {
|
|
399
|
+
const permissionsPath =
|
|
400
|
+
permissionsEnabled && typeof permissionsConfig === 'object' && permissionsConfig.path
|
|
401
|
+
? permissionsConfig.path
|
|
402
|
+
: permissionsEnabled
|
|
403
|
+
? 'permissions'
|
|
404
|
+
: undefined;
|
|
405
|
+
imports.push(
|
|
406
|
+
CoreHubModule.forRoot({
|
|
407
|
+
actionsController: overrides?.hub?.actionsController,
|
|
408
|
+
actionsService: overrides?.hub?.actionsService,
|
|
409
|
+
config: config.hub,
|
|
410
|
+
controller: overrides?.hub?.controller,
|
|
411
|
+
ctx: {
|
|
412
|
+
env: config.env,
|
|
413
|
+
graphQlEnabled: isGraphQlEnabled,
|
|
414
|
+
permissionsPath,
|
|
415
|
+
version: config.version ?? 'unknown',
|
|
416
|
+
},
|
|
417
|
+
htmlService: overrides?.hub?.htmlService,
|
|
418
|
+
service: overrides?.hub?.service,
|
|
419
|
+
}),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
382
423
|
// Add CoreBetterAuthModule based on mode
|
|
383
424
|
// IAM-only mode: BetterAuth is enabled by default (it's the only auth option)
|
|
384
425
|
// Legacy mode: Only register if autoRegister is explicitly true
|
|
@@ -438,6 +479,25 @@ export class CoreModule implements NestModule {
|
|
|
438
479
|
}
|
|
439
480
|
}
|
|
440
481
|
|
|
482
|
+
// SECURITY: the Hub's ADMIN gate is enforced by the app-wide roles guard, which is registered by
|
|
483
|
+
// BetterAuth (IAM mode) or the legacy Auth module. When the Hub is enabled and gated but NEITHER is
|
|
484
|
+
// active, nothing enforces its @Roles(ADMIN) metadata — the whole cockpit, INCLUDING destructive
|
|
485
|
+
// migration/file-delete/cron actions, would be reachable unauthenticated, silently. A project may
|
|
486
|
+
// still register its own APP_GUARD, so this is a loud warning rather than a hard error.
|
|
487
|
+
if (isHubEnabled(config.hub)) {
|
|
488
|
+
const hubIsGated = !(typeof config.hub === 'object' && config.hub.roles === false);
|
|
489
|
+
const noBuiltInRolesGuard = isIamOnlyMode && !isBetterAuthEnabled;
|
|
490
|
+
if (hubIsGated && noBuiltInRolesGuard) {
|
|
491
|
+
console.warn(
|
|
492
|
+
'CoreModule: the Hub is enabled and ADMIN-gated, but neither BetterAuth (IAM) nor the legacy ' +
|
|
493
|
+
'Auth module is active — so no built-in guard enforces its @Roles(ADMIN) metadata. Unless you ' +
|
|
494
|
+
'have registered your own APP_GUARD that reads @Roles, the Hub (including its destructive ' +
|
|
495
|
+
'migration/file-delete/cron actions) is reachable UNAUTHENTICATED. Enable an auth module or ' +
|
|
496
|
+
'register a roles guard.',
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
441
501
|
// Add CoreSystemSetupModule when BetterAuth is active
|
|
442
502
|
// Enabled by default - disable explicitly via systemSetup: { enabled: false }
|
|
443
503
|
if (isBetterAuthEnabled && config.systemSetup?.enabled !== false) {
|
package/src/index.ts
CHANGED
|
@@ -181,6 +181,12 @@ export * from './core/modules/health-check/core-health-check.module';
|
|
|
181
181
|
export * from './core/modules/health-check/core-health-check.resolver';
|
|
182
182
|
export * from './core/modules/health-check/core-health-check.service';
|
|
183
183
|
|
|
184
|
+
// =====================================================================================================================
|
|
185
|
+
// Core - Modules - Hub
|
|
186
|
+
// =====================================================================================================================
|
|
187
|
+
|
|
188
|
+
export * from './core/modules/hub';
|
|
189
|
+
|
|
184
190
|
// =====================================================================================================================
|
|
185
191
|
// Core - Modules - Migrate
|
|
186
192
|
// =====================================================================================================================
|