@futdevpro/nts-dynamo 1.15.118 → 1.15.120

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.
Files changed (91) hide show
  1. package/.dynamo/logs/cicd-pipeline/output.log +1845 -1703
  2. package/.dynamo/logs/cicd-pipeline/status.json +42 -36
  3. package/build/_models/control-models/endpoint-params.control-model.d.ts +5 -0
  4. package/build/_models/control-models/endpoint-params.control-model.d.ts.map +1 -1
  5. package/build/_models/control-models/endpoint-params.control-model.js +41 -11
  6. package/build/_models/control-models/endpoint-params.control-model.js.map +1 -1
  7. package/build/_models/data-models/schema-migration.data-model.d.ts +29 -0
  8. package/build/_models/data-models/schema-migration.data-model.d.ts.map +1 -0
  9. package/build/_models/data-models/schema-migration.data-model.js +47 -0
  10. package/build/_models/data-models/schema-migration.data-model.js.map +1 -0
  11. package/build/_models/interfaces/migration-entry.interface.d.ts +20 -0
  12. package/build/_models/interfaces/migration-entry.interface.d.ts.map +1 -0
  13. package/build/_models/interfaces/migration-entry.interface.js +3 -0
  14. package/build/_models/interfaces/migration-entry.interface.js.map +1 -0
  15. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.d.ts +6 -1
  16. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.d.ts.map +1 -1
  17. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.js +40 -10
  18. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.js.map +1 -1
  19. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.d.ts +9 -0
  20. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.d.ts.map +1 -1
  21. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.js +12 -0
  22. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.js.map +1 -1
  23. package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.d.ts +12 -2
  24. package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.d.ts.map +1 -1
  25. package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.js +54 -7
  26. package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.js.map +1 -1
  27. package/build/_modules/server/server-status/server-status.control-service.d.ts +6 -0
  28. package/build/_modules/server/server-status/server-status.control-service.d.ts.map +1 -1
  29. package/build/_modules/server/server-status/server-status.control-service.js +10 -0
  30. package/build/_modules/server/server-status/server-status.control-service.js.map +1 -1
  31. package/build/_modules/server/server-status/server-status.controller.d.ts.map +1 -1
  32. package/build/_modules/server/server-status/server-status.controller.js +26 -0
  33. package/build/_modules/server/server-status/server-status.controller.js.map +1 -1
  34. package/build/_modules/socket/_models/socket-server-service-params.control-model.d.ts +11 -1
  35. package/build/_modules/socket/_models/socket-server-service-params.control-model.d.ts.map +1 -1
  36. package/build/_modules/socket/_models/socket-server-service-params.control-model.js +12 -1
  37. package/build/_modules/socket/_models/socket-server-service-params.control-model.js.map +1 -1
  38. package/build/_modules/socket/_services/socket-server.service.d.ts +2 -0
  39. package/build/_modules/socket/_services/socket-server.service.d.ts.map +1 -1
  40. package/build/_modules/socket/_services/socket-server.service.js +2 -0
  41. package/build/_modules/socket/_services/socket-server.service.js.map +1 -1
  42. package/build/_modules/socket/app-extended.server.d.ts.map +1 -1
  43. package/build/_modules/socket/app-extended.server.js +13 -8
  44. package/build/_modules/socket/app-extended.server.js.map +1 -1
  45. package/build/_services/base/db.service.d.ts +14 -0
  46. package/build/_services/base/db.service.d.ts.map +1 -1
  47. package/build/_services/base/db.service.js +27 -0
  48. package/build/_services/base/db.service.js.map +1 -1
  49. package/build/_services/core/api.service.d.ts.map +1 -1
  50. package/build/_services/core/api.service.js +29 -9
  51. package/build/_services/core/api.service.js.map +1 -1
  52. package/build/_services/core/db-stats.service.d.ts +34 -0
  53. package/build/_services/core/db-stats.service.d.ts.map +1 -0
  54. package/build/_services/core/db-stats.service.js +65 -0
  55. package/build/_services/core/db-stats.service.js.map +1 -0
  56. package/build/_services/core/diagnostics.service.d.ts +43 -0
  57. package/build/_services/core/diagnostics.service.d.ts.map +1 -0
  58. package/build/_services/core/diagnostics.service.js +122 -0
  59. package/build/_services/core/diagnostics.service.js.map +1 -0
  60. package/build/_services/core/migration-runner.service.d.ts +40 -0
  61. package/build/_services/core/migration-runner.service.d.ts.map +1 -0
  62. package/build/_services/core/migration-runner.service.js +75 -0
  63. package/build/_services/core/migration-runner.service.js.map +1 -0
  64. package/build/index.d.ts +5 -0
  65. package/build/index.d.ts.map +1 -1
  66. package/build/index.js +6 -0
  67. package/build/index.js.map +1 -1
  68. package/package.json +1 -1
  69. package/src/_models/control-models/endpoint-params.control-model.ts +44 -16
  70. package/src/_models/data-models/schema-migration.data-model.ts +50 -0
  71. package/src/_models/interfaces/migration-entry.interface.ts +19 -0
  72. package/src/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.spec.ts +16 -0
  73. package/src/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.ts +50 -14
  74. package/src/_modules/local-vector-search/_services/lvs-vector-persist.data-service.ts +22 -0
  75. package/src/_modules/local-vector-search/_services/lvs-vector-pool.control-service.spec.ts +5 -3
  76. package/src/_modules/local-vector-search/_services/lvs-vector-pool.control-service.ts +62 -11
  77. package/src/_modules/server/server-status/server-status.control-service.ts +12 -0
  78. package/src/_modules/server/server-status/server-status.controller.ts +28 -0
  79. package/src/_modules/socket/_models/socket-server-service-params.control-model.spec.ts +23 -0
  80. package/src/_modules/socket/_models/socket-server-service-params.control-model.ts +12 -1
  81. package/src/_modules/socket/_services/socket-server.service.ts +2 -0
  82. package/src/_modules/socket/app-extended.server.ts +15 -9
  83. package/src/_services/base/db.service.ts +39 -1
  84. package/src/_services/core/api.service.spec.ts +47 -0
  85. package/src/_services/core/api.service.ts +32 -12
  86. package/src/_services/core/db-stats.service.ts +104 -0
  87. package/src/_services/core/diagnostics.service.spec.ts +44 -0
  88. package/src/_services/core/diagnostics.service.ts +158 -0
  89. package/src/_services/core/migration-runner.service.spec.ts +91 -0
  90. package/src/_services/core/migration-runner.service.ts +106 -0
  91. package/src/index.ts +7 -0
@@ -677,8 +677,46 @@ export class DyNTS_DBService<T extends DyFM_Metadata> {
677
677
  * @param narrowByDependencyIds id
678
678
  * @returns dataList
679
679
  */
680
+ /**
681
+ * BFR-AM-013 — LEAN + projektált + BATCH-elt cursor-scan (OOM-safe nagy collection-ökhöz).
682
+ *
683
+ * A teljes result-set SOSEM él egyszerre a heapen: a Mongo-cursor batch-enként adagolja a
684
+ * PLAIN (lean, nem-Mongoose-doc) objektumokat a `handler`-nek — a Mongoose-doc-boxing tranziens
685
+ * (a 32k-vektoros LVS-hidratálás 93%-heap-spike-jának gyökere) kimarad. A `projection`-nel csak
686
+ * a szükséges mezők jönnek (pl. `{ vectorId: 1, embedding: 1 }` a content-szöveg NÉLKÜL).
687
+ * Visszaadja a feldolgozott dokumentumok számát.
688
+ */
689
+ async findLeanBatched(
690
+ filterBy: DyFM_DBFilter<T>,
691
+ set: {
692
+ projection?: Record<string, number>,
693
+ batchSize?: number,
694
+ handler: (batch: Partial<T>[]) => Promise<void> | void,
695
+ }
696
+ ): Promise<number> {
697
+ const batchSize: number = set.batchSize && set.batchSize > 0 ? set.batchSize : 500;
698
+ const cursor = this.dataModel.find(filterBy, set.projection).lean().cursor();
699
+
700
+ let batch: Partial<T>[] = [];
701
+ let total: number = 0;
702
+
703
+ for await (const doc of cursor) {
704
+ batch.push(doc as Partial<T>);
705
+ total++;
706
+ if (batch.length >= batchSize) {
707
+ await set.handler(batch);
708
+ batch = [];
709
+ }
710
+ }
711
+ if (batch.length) {
712
+ await set.handler(batch);
713
+ }
714
+
715
+ return total;
716
+ }
717
+
680
718
  async searchData(
681
- filterBy: DyFM_DBFilter<T>,
719
+ filterBy: DyFM_DBFilter<T>,
682
720
  narrowByDependencyIds?: string[],
683
721
  narrowByDependencyKey?: string,
684
722
  findDeleted?: boolean
@@ -138,3 +138,50 @@ describe('| DyNTS_ApiService', () => {
138
138
  );
139
139
  });
140
140
  });
141
+
142
+ // BFR-DYNAMOBUILDER-006 — a belső BE-BE URL NEM kerülhet a _message-be (kliens-leak);
143
+ // a confidentialContent-be megy (a response-strip env-függetlenül levágja).
144
+ describe('| DyNTS_ApiService.handleError — internal URL leak guard (BFR-DYNAMOBUILDER-006)', () => {
145
+ const internalUrl: string = 'http://auth-service:39005/api/auth/login';
146
+
147
+ function buildCallParams(): DyNTS_ApiCall_Params {
148
+ return new DyNTS_ApiCall_Params({
149
+ baseUrl: 'http://auth-service:39005/api',
150
+ endpoint: '/auth/login',
151
+ type: DyFM_HttpCallType.post,
152
+ httpOptions: { headers: {}, responseType: DyFM_HttpResponseType.json },
153
+ name: 'loginCall',
154
+ getFullResponse: false,
155
+ });
156
+ }
157
+
158
+ function invokeHandleError(error: unknown): DyFM_Error {
159
+ try {
160
+ DyNTS_ApiService['handleError'](buildCallParams(), error, { pathParams: {}, body: {} }, internalUrl);
161
+ } catch (thrown: unknown) {
162
+ return thrown as DyFM_Error;
163
+ }
164
+ throw new Error('handleError did not throw');
165
+ }
166
+
167
+ it('| HE3 (ECONNREFUSED): a _message NEM tartalmazza a belső URL-t; a confidentialContent igen', () => {
168
+ const thrown: DyFM_Error = invokeHandleError({ code: 'ECONNREFUSED', message: 'connect ECONNREFUSED' });
169
+
170
+ expect(thrown._message).not.toContain('auth-service:39005');
171
+ expect(thrown._message).toContain('loginCall');
172
+ expect(thrown.confidentialContent?.url).toBe(internalUrl);
173
+ });
174
+
175
+ it('| HE4 (status code): a _message NEM tartalmazza a belső endpointot/URL-t', () => {
176
+ const axiosLike = {
177
+ config: {},
178
+ message: 'Request failed with status code 401',
179
+ response: { status: 401 },
180
+ };
181
+ const thrown: DyFM_Error = invokeHandleError(axiosLike);
182
+
183
+ expect(thrown._message).not.toContain('auth-service:39005');
184
+ expect(thrown.confidentialContent?.url).toBe(internalUrl);
185
+ expect(thrown.additionalContent?.callName).toBe('loginCall');
186
+ });
187
+ });
@@ -264,12 +264,19 @@ export class DyNTS_ApiService {
264
264
  ),
265
265
 
266
266
  errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE1`,
267
- message:
267
+ // BFR-DYNAMOBUILDER-006: a BELSŐ BE-BE URL (Docker-hostname+port) NEM kerülhet a _message-be —
268
+ // non-prod env-en a teljes error kimegy a kliensnek → info-disclosure. A _message csak a
269
+ // hívás NEVÉT hordozza; az URL + callParams a confidentialContent-be megy (error-store-ba
270
+ // perzisztálódik, a response-strip env-FÜGGETLENÜL levágja).
271
+ message:
268
272
  `API call "${callParams.name}" failed on the other end!`+
269
- `\nurl: ${url}` +
270
273
  `\nerror response found: ${errorMsg}`,
271
274
  error: dyfmErrorInAxiosResponse,
272
275
  additionalContent: {
276
+ callName: callParams.name,
277
+ },
278
+ confidentialContent: {
279
+ url: url,
273
280
  callParams: callParams,
274
281
  inputParams: inputParams,
275
282
  response: (error as Axios.AxiosError)?.response,
@@ -287,8 +294,12 @@ export class DyNTS_ApiService {
287
294
 
288
295
  status: 404,
289
296
  errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE2`,
290
- message: 'DNS error; address cannot be resolved!',
297
+ message: `DNS error; address cannot be resolved! (call: "${callParams.name}")`,
291
298
  additionalContent: {
299
+ callName: callParams.name,
300
+ },
301
+ confidentialContent: {
302
+ url: url,
292
303
  callParams: callParams,
293
304
  inputParams: inputParams,
294
305
  response: (error as Axios.AxiosError)?.response,
@@ -306,11 +317,15 @@ export class DyNTS_ApiService {
306
317
 
307
318
  status: 404,
308
319
  errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE3`,
309
- message: `Can't connect to the endpoint! \n(${callParams.type} "${url}")`,
320
+ // BFR-DYNAMOBUILDER-006: az URL confidentialContent-be (nem a _message-be) — belső host-leak ellen.
321
+ message: `Can't connect to the endpoint! (call: "${callParams.name}")`,
310
322
  additionalContent: {
323
+ callName: callParams.name,
324
+ },
325
+ confidentialContent: {
326
+ url: url,
311
327
  callParams: callParams,
312
328
  inputParams: inputParams,
313
- url: url,
314
329
  response: (error as Axios.AxiosError)?.response,
315
330
  },
316
331
  });
@@ -325,15 +340,14 @@ export class DyNTS_ApiService {
325
340
  500;
326
341
 
327
342
  if ((error as Axios.AxiosError)?.message?.includes('status code')) {
328
- msg += `"${callParams.name}"` +
329
- `\nendpoint: ${callParams.type} "${callParams.baseUrl}${callParams.endpoint}"` +
330
- `\nurl: ${url}` +
343
+ // BFR-DYNAMOBUILDER-006: a _message CSAK a hívás-nevet + path-param-KULCSOKAT hordozza —
344
+ // a belső endpoint/URL/stack a confidentialContent-be megy (response-ba SOHA).
345
+ msg += ` "${callParams.name}"` +
331
346
  `\npathParams: ${
332
- inputParams?.pathParams ?
333
- Object.keys(inputParams.pathParams).join(', ') :
347
+ inputParams?.pathParams ?
348
+ Object.keys(inputParams.pathParams).join(', ') :
334
349
  '-no path params-'
335
- }` +
336
- `\nenpointStackLocation: ${callParams.stack}`;
350
+ }`;
337
351
  }
338
352
 
339
353
  throw new DyFM_Error({
@@ -346,6 +360,12 @@ export class DyNTS_ApiService {
346
360
  message: msg,
347
361
  errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE4`,
348
362
  additionalContent: {
363
+ callName: callParams.name,
364
+ },
365
+ confidentialContent: {
366
+ url: url,
367
+ endpoint: `${callParams.type} "${callParams.baseUrl}${callParams.endpoint}"`,
368
+ endpointStackLocation: callParams.stack,
349
369
  callParams: callParams,
350
370
  inputParams: inputParams,
351
371
  response: (error as Axios.AxiosError)?.response,
@@ -0,0 +1,104 @@
1
+ import * as mongoose from 'mongoose';
2
+
3
+ import { DyFM_Log } from '@futdevpro/fsm-dynamo';
4
+
5
+ /**
6
+ * BFR-OVERSEER-004 — stats-only DB-diagnosztika (NO direct data read).
7
+ *
8
+ * Per-collection rekordszám + adat-méret + index-méret (a `$collStats` aggregation-stage-en át —
9
+ * metaadat-olvasás, SOHA nem dokumentum-tartalom) + összesített DB-méret. A 2026-06-23 overseer-incidens
10
+ * (3 GB-ra hízott archív-collection, csak kézi mongosh-sal látszott) proaktív early-warningja: minden
11
+ * NTS-szerver default megkapja a `/server/db-stats` endpointon (app-auth-tal gate-elhető).
12
+ */
13
+
14
+ export interface DyNTS_CollectionStats {
15
+ name: string;
16
+ documentCount: number;
17
+ sizeMB: number;
18
+ indexSizeMB: number;
19
+ }
20
+
21
+ export interface DyNTS_DbStats {
22
+ dbName: string;
23
+ collections: DyNTS_CollectionStats[];
24
+ totalSizeMB: number;
25
+ totalIndexSizeMB: number;
26
+ totalDocumentCount: number;
27
+ /** A gyűjtés közben elhasalt collection-ök (never-silent — a kihagyás látható). */
28
+ failedCollections: string[];
29
+ }
30
+
31
+ const BYTES_PER_MB: number = 1024 * 1024;
32
+
33
+ export class DyNTS_DbStats_Util {
34
+
35
+ /**
36
+ * A teljes stats-only pillanatkép. Hibatűrő: egy-egy collection statjának elhasalása NEM
37
+ * borítja a többit (failedCollections-be kerül, warn-nal — no-silent-skip).
38
+ */
39
+ static async collect(connection: mongoose.Connection): Promise<DyNTS_DbStats> {
40
+ const db: typeof connection.db = connection?.db;
41
+ if (!db) {
42
+ return {
43
+ dbName: connection?.name ?? '-no-connection-',
44
+ collections: [],
45
+ totalSizeMB: 0,
46
+ totalIndexSizeMB: 0,
47
+ totalDocumentCount: 0,
48
+ failedCollections: [],
49
+ };
50
+ }
51
+
52
+ const collectionInfos: { name: string }[] = await db.listCollections({}, { nameOnly: true }).toArray();
53
+ const collections: DyNTS_CollectionStats[] = [];
54
+ const failedCollections: string[] = [];
55
+
56
+ for (const info of collectionInfos) {
57
+ try {
58
+ // $collStats storageStats: metaadat-only (size/count/totalIndexSize) — nincs dokumentum-olvasás.
59
+ const statsCursor = db.collection(info.name).aggregate([
60
+ { $collStats: { storageStats: {} } },
61
+ ]);
62
+ const statsDocs: Record<string, unknown>[] = await statsCursor.toArray();
63
+ const storage: Record<string, unknown> =
64
+ (statsDocs[0]?.['storageStats'] ?? {}) as Record<string, unknown>;
65
+
66
+ collections.push({
67
+ name: info.name,
68
+ documentCount: Number(storage['count'] ?? 0),
69
+ sizeMB: DyNTS_DbStats_Util.toMB(Number(storage['size'] ?? 0)),
70
+ indexSizeMB: DyNTS_DbStats_Util.toMB(Number(storage['totalIndexSize'] ?? 0)),
71
+ });
72
+ } catch (error) {
73
+ failedCollections.push(info.name);
74
+ DyFM_Log.warn(`(DyNTS_DbStats) collStats failed for "${info.name}" (non-fatal)`, error);
75
+ }
76
+ }
77
+
78
+ // Méret szerint csökkenő — a bloat-gyanús collection legyen elöl.
79
+ collections.sort((a: DyNTS_CollectionStats, b: DyNTS_CollectionStats): number => b.sizeMB - a.sizeMB);
80
+
81
+ return {
82
+ dbName: connection.name,
83
+ collections: collections,
84
+ totalSizeMB: DyNTS_DbStats_Util.round2(
85
+ collections.reduce((sum: number, c: DyNTS_CollectionStats): number => sum + c.sizeMB, 0)
86
+ ),
87
+ totalIndexSizeMB: DyNTS_DbStats_Util.round2(
88
+ collections.reduce((sum: number, c: DyNTS_CollectionStats): number => sum + c.indexSizeMB, 0)
89
+ ),
90
+ totalDocumentCount: collections.reduce(
91
+ (sum: number, c: DyNTS_CollectionStats): number => sum + c.documentCount, 0
92
+ ),
93
+ failedCollections: failedCollections,
94
+ };
95
+ }
96
+
97
+ static toMB(bytes: number): number {
98
+ return DyNTS_DbStats_Util.round2(bytes / BYTES_PER_MB);
99
+ }
100
+
101
+ static round2(value: number): number {
102
+ return Math.round(value * 100) / 100;
103
+ }
104
+ }
@@ -0,0 +1,44 @@
1
+ import { DyNTS_DbStats_Util } from './db-stats.service';
2
+ import { DyNTS_Diagnostics, DyNTS_DoctorCheck } from './diagnostics.service';
3
+
4
+ /**
5
+ * BFR-AM-015 / BFR-OVERSEER-004 — a diagnosztika PURE részeinek unit-tesztjei
6
+ * (interpretHealth / buildSummary / MB-kerekítés).
7
+ */
8
+ describe('| DyNTS_Diagnostics (pure)', () => {
9
+
10
+ const ok: DyNTS_DoctorCheck = { name: 'a', health: 'ok' };
11
+ const warn: DyNTS_DoctorCheck = { name: 'b', health: 'warn', fixHint: 'do b' };
12
+ const fail: DyNTS_DoctorCheck = { name: 'c', health: 'fail', fixHint: 'do c' };
13
+
14
+ it('| interpretHealth: fail dominál, aztán warn, különben ok', () => {
15
+ expect(DyNTS_Diagnostics.interpretHealth([ ok, ok ])).toBe('ok');
16
+ expect(DyNTS_Diagnostics.interpretHealth([ ok, warn ])).toBe('warn');
17
+ expect(DyNTS_Diagnostics.interpretHealth([ warn, fail, ok ])).toBe('fail');
18
+ expect(DyNTS_Diagnostics.interpretHealth([])).toBe('ok');
19
+ });
20
+
21
+ it('| buildSummary: all-ok → "all checks ok"; nem-ok check-ek fixHint-tel listázva', () => {
22
+ expect(DyNTS_Diagnostics.buildSummary([ ok ])).toBe('all checks ok');
23
+ const summary: string = DyNTS_Diagnostics.buildSummary([ ok, warn, fail ]);
24
+ expect(summary).toContain('b: warn — do b');
25
+ expect(summary).toContain('c: fail — do c');
26
+ expect(summary).not.toContain('a:');
27
+ });
28
+ });
29
+
30
+ describe('| DyNTS_DbStats_Util (pure)', () => {
31
+
32
+ it('| toMB: byte → MB két tizedesre', () => {
33
+ expect(DyNTS_DbStats_Util.toMB(1024 * 1024)).toBe(1);
34
+ expect(DyNTS_DbStats_Util.toMB(1536 * 1024)).toBe(1.5);
35
+ expect(DyNTS_DbStats_Util.toMB(0)).toBe(0);
36
+ });
37
+
38
+ it('| collect: kapcsolat nélkül üres, nem-dobó eredmény', async () => {
39
+ const stats = await DyNTS_DbStats_Util.collect(undefined as never);
40
+ expect(stats.collections).toEqual([]);
41
+ expect(stats.totalSizeMB).toBe(0);
42
+ expect(stats.failedCollections).toEqual([]);
43
+ });
44
+ });
@@ -0,0 +1,158 @@
1
+ import * as mongoose from 'mongoose';
2
+
3
+ import { DyFM_Log } from '@futdevpro/fsm-dynamo';
4
+
5
+ import { DyNTS_DbStats, DyNTS_DbStats_Util } from './db-stats.service';
6
+ import { DyNTS_EventLoopDiag } from './event-loop-diag.service';
7
+ import { DyNTS_MemoryGuard } from './memory-guard.service';
8
+
9
+ /**
10
+ * BFR-AM-015 (+ BFR-OVERSEER-004 aggregátor-fele) — generikus, újrahasználható diagnosztikai
11
+ * aggregátor MINDEN `DyNTS_App`-hoz: a `GET /server/doctor` endpoint forrása.
12
+ *
13
+ * Check-ek (mind SAJÁT try-ban — egy elhasaló check nem borítja a doctort, néma hiba TILOS):
14
+ * - mongo: kapcsolat + ping (readiness-minta);
15
+ * - memory: MemoryGuard pressure-snapshot;
16
+ * - eventLoop: EventLoopDiag snapshot;
17
+ * - dbStats: stats-only collection-méretek + küszöb-warn a bloat-gyanúra.
18
+ * Az app APP-SPECIFIKUS check-eket regisztrálhat (`registerCheck`) — pl. env-validáció,
19
+ * provider-probe (a FAM `FAM_Doctor` mintája bedrock-magként).
20
+ */
21
+
22
+ export interface DyNTS_DoctorCheck {
23
+ name: string;
24
+ health: 'ok' | 'warn' | 'fail';
25
+ detail?: unknown;
26
+ /** Mit tegyen az operátor, ha nem ok (néma hiba tilos — a hint kötelező nem-ok esetén). */
27
+ fixHint?: string;
28
+ }
29
+
30
+ export interface DyNTS_DoctorReport {
31
+ health: 'ok' | 'warn' | 'fail';
32
+ checks: DyNTS_DoctorCheck[];
33
+ summary: string;
34
+ uptimeSec: number;
35
+ }
36
+
37
+ /** App-specifikus check-függvény (a doctor futtatja, saját try-ban). */
38
+ export type DyNTS_DoctorCheckFn = () => Promise<DyNTS_DoctorCheck>;
39
+
40
+ /** Collection-méret warn-küszöb (MB) — e fölött a dbStats-check warn-ra vált. */
41
+ const DEFAULT_COLLECTION_WARN_MB: number = 512;
42
+
43
+ export class DyNTS_Diagnostics {
44
+
45
+ private static readonly customChecks: DyNTS_DoctorCheckFn[] = [];
46
+
47
+ /** App-specifikus check regisztrálása (pl. env-validáció, provider-probe). */
48
+ static registerCheck(check: DyNTS_DoctorCheckFn): void {
49
+ DyNTS_Diagnostics.customChecks.push(check);
50
+ }
51
+
52
+ /** A teljes doctor-riport. SOSEM dob — a hibás check `fail`-ként jelenik meg. */
53
+ static async runDoctor(set: {
54
+ connection: mongoose.Connection,
55
+ collectionWarnMB?: number,
56
+ }): Promise<DyNTS_DoctorReport> {
57
+ const checks: DyNTS_DoctorCheck[] = [];
58
+
59
+ // 1) Mongo-probe (readiness-minta: readyState + admin ping).
60
+ try {
61
+ const readyState: number = set.connection?.readyState ?? 0;
62
+ let pingOk: boolean = false;
63
+ if (readyState === 1) {
64
+ await set.connection.db?.admin().ping();
65
+ pingOk = true;
66
+ }
67
+ checks.push({
68
+ name: 'mongo',
69
+ health: readyState === 1 && pingOk ? 'ok' : 'fail',
70
+ detail: { readyState: readyState, pingOk: pingOk },
71
+ fixHint: readyState === 1 && pingOk ? undefined
72
+ : 'Check MONGO_URL + DB container/network reachability (readyState 1 + ping expected).',
73
+ });
74
+ } catch (error) {
75
+ checks.push({
76
+ name: 'mongo', health: 'fail', detail: String(error),
77
+ fixHint: 'Mongo ping threw — check DB availability/credentials.',
78
+ });
79
+ }
80
+
81
+ // 2) Memória (MemoryGuard pressure).
82
+ try {
83
+ const pressure: unknown = DyNTS_MemoryGuard.getInstance().pressure();
84
+ const level: string = String((pressure as { level?: string })?.level ?? 'ok');
85
+ checks.push({
86
+ name: 'memory',
87
+ health: level === 'critical' ? 'fail' : level === 'warning' ? 'warn' : 'ok',
88
+ detail: pressure,
89
+ fixHint: level === 'ok' ? undefined
90
+ : 'Heap pressure — check collection sizes (/server/db-stats) + recent load; consider retention.',
91
+ });
92
+ } catch (error) {
93
+ checks.push({ name: 'memory', health: 'warn', detail: String(error), fixHint: 'MemoryGuard snapshot failed.' });
94
+ }
95
+
96
+ // 3) Event-loop lag.
97
+ try {
98
+ const snapshot: unknown = DyNTS_EventLoopDiag.getInstance().getSnapshot();
99
+ checks.push({ name: 'eventLoop', health: 'ok', detail: snapshot });
100
+ } catch (error) {
101
+ checks.push({ name: 'eventLoop', health: 'warn', detail: String(error), fixHint: 'EventLoopDiag snapshot failed.' });
102
+ }
103
+
104
+ // 4) DB-stats + bloat-küszöb (BFR-OVERSEER-004 early-warning).
105
+ try {
106
+ const stats: DyNTS_DbStats = await DyNTS_DbStats_Util.collect(set.connection);
107
+ const warnMB: number = set.collectionWarnMB ?? DEFAULT_COLLECTION_WARN_MB;
108
+ const bloated: string[] = stats.collections
109
+ .filter((c): boolean => c.sizeMB + c.indexSizeMB > warnMB)
110
+ .map((c): string => `${c.name} (${c.sizeMB}MB + ${c.indexSizeMB}MB idx)`);
111
+ checks.push({
112
+ name: 'dbStats',
113
+ health: bloated.length ? 'warn' : 'ok',
114
+ detail: { totalSizeMB: stats.totalSizeMB, collectionCount: stats.collections.length, bloated: bloated },
115
+ fixHint: bloated.length
116
+ ? `Collections over ${warnMB}MB — add dataParams.retention (TTL) or prune: ${bloated.join(', ')}`
117
+ : undefined,
118
+ });
119
+ } catch (error) {
120
+ checks.push({ name: 'dbStats', health: 'warn', detail: String(error), fixHint: 'db-stats collection failed.' });
121
+ }
122
+
123
+ // 5) App-specifikus check-ek.
124
+ for (const customCheck of DyNTS_Diagnostics.customChecks) {
125
+ try {
126
+ checks.push(await customCheck());
127
+ } catch (error) {
128
+ DyFM_Log.warn('(DyNTS_Diagnostics) custom check threw (reported as fail)', error);
129
+ checks.push({ name: 'custom', health: 'fail', detail: String(error), fixHint: 'Custom check threw — see detail.' });
130
+ }
131
+ }
132
+
133
+ return {
134
+ health: DyNTS_Diagnostics.interpretHealth(checks),
135
+ checks: checks,
136
+ summary: DyNTS_Diagnostics.buildSummary(checks),
137
+ uptimeSec: Math.round(process.uptime()),
138
+ };
139
+ }
140
+
141
+ /** Aggregált health: bármely fail → fail; különben bármely warn → warn; különben ok. Pure (unit-tesztelt). */
142
+ static interpretHealth(checks: DyNTS_DoctorCheck[]): 'ok' | 'warn' | 'fail' {
143
+ if (checks.some((c: DyNTS_DoctorCheck): boolean => c.health === 'fail')) { return 'fail'; }
144
+ if (checks.some((c: DyNTS_DoctorCheck): boolean => c.health === 'warn')) { return 'warn'; }
145
+ return 'ok';
146
+ }
147
+
148
+ /** Ember-olvasható összegzés a nem-ok check-ekből. Pure (unit-tesztelt). */
149
+ static buildSummary(checks: DyNTS_DoctorCheck[]): string {
150
+ const notOk: DyNTS_DoctorCheck[] = checks.filter((c: DyNTS_DoctorCheck): boolean => c.health !== 'ok');
151
+ if (!notOk.length) {
152
+ return 'all checks ok';
153
+ }
154
+ return notOk
155
+ .map((c: DyNTS_DoctorCheck): string => `${c.name}: ${c.health}${c.fixHint ? ` — ${c.fixHint}` : ''}`)
156
+ .join(' | ');
157
+ }
158
+ }
@@ -0,0 +1,91 @@
1
+ import { DyNTS_Migration_Runner } from './migration-runner.service';
2
+ import { DyNTS_SchemaMigration } from '../../_models/data-models/schema-migration.data-model';
3
+ import { DyNTS_MigrationEntry } from '../../_models/interfaces/migration-entry.interface';
4
+
5
+ function entry(id: string, run: () => Promise<{ touched: number }>): DyNTS_MigrationEntry {
6
+ return { id: id, description: id, run: run };
7
+ }
8
+
9
+ describe('| DyNTS_Migration_Runner', () => {
10
+
11
+ describe('| selectPending (pure)', () => {
12
+ it('| kiszűri a már-alkalmazottakat, megőrzi a sorrendet', () => {
13
+ const migs: DyNTS_MigrationEntry[] = [
14
+ entry('a', async () => ({ touched: 0 })),
15
+ entry('b', async () => ({ touched: 0 })),
16
+ entry('c', async () => ({ touched: 0 })),
17
+ ];
18
+ const pending: DyNTS_MigrationEntry[] = DyNTS_Migration_Runner.selectPending(migs, new Set([ 'b' ]));
19
+ expect(pending.map((m) => m.id)).toEqual([ 'a', 'c' ]);
20
+ });
21
+
22
+ it('| mind alkalmazva → üres; üres registry → üres', () => {
23
+ const migs: DyNTS_MigrationEntry[] = [ entry('a', async () => ({ touched: 0 })) ];
24
+ expect(DyNTS_Migration_Runner.selectPending(migs, new Set([ 'a' ])).length).toBe(0);
25
+ expect(DyNTS_Migration_Runner.selectPending([], new Set()).length).toBe(0);
26
+ });
27
+ });
28
+
29
+ describe('| runPending (mock ledger)', () => {
30
+ let runner: DyNTS_Migration_Runner;
31
+ let saved: DyNTS_SchemaMigration[];
32
+ let appliedRecords: DyNTS_SchemaMigration[];
33
+
34
+ beforeEach(() => {
35
+ runner = DyNTS_Migration_Runner.getInstance();
36
+ saved = [];
37
+ appliedRecords = [];
38
+
39
+ const fakeLedger: any = { // ccap-review-disable-line no-any-type
40
+ dataDBService: { find: async (): Promise<DyNTS_SchemaMigration[]> => appliedRecords },
41
+ data: null,
42
+ saveData: async function (): Promise<void> { saved.push(this.data); appliedRecords.push(this.data); },
43
+ };
44
+
45
+ spyOn(runner as any, 'createLedgerDataService').and.returnValue(fakeLedger); // ccap-review-disable-line no-any-type
46
+ });
47
+
48
+ it('| a pending-et futtatja + rögzíti (id+version+touched), a már-alkalmazottat SKIP-eli', async () => {
49
+ appliedRecords.push(new DyNTS_SchemaMigration({ migrationId: 'm1' }));
50
+ const ran: string[] = [];
51
+
52
+ const migs: DyNTS_MigrationEntry[] = [
53
+ entry('m1', async () => { ran.push('m1'); return { touched: 1 }; }),
54
+ entry('m2', async () => { ran.push('m2'); return { touched: 5 }; }),
55
+ ];
56
+
57
+ const res = await runner.runPending(migs, '1.0.0');
58
+
59
+ expect(ran).toEqual([ 'm2' ]); // m1 skip (már applied), m2 fut
60
+ expect(res.applied).toBe(1);
61
+ expect(res.skipped).toBe(1);
62
+ expect(saved[0].migrationId).toBe('m2');
63
+ expect(saved[0].appliedVersion).toBe('1.0.0');
64
+ expect(saved[0].notes).toBe('touched=5');
65
+ });
66
+
67
+ it('| RUN-ONCE: második sweep-en (m2 már a ledgerben) semmi nem fut', async () => {
68
+ appliedRecords.push(new DyNTS_SchemaMigration({ migrationId: 'm2' }));
69
+ const ran: string[] = [];
70
+ const migs: DyNTS_MigrationEntry[] = [ entry('m2', async () => { ran.push('m2'); return { touched: 0 }; }) ];
71
+
72
+ const res = await runner.runPending(migs, '1.0.0');
73
+ expect(ran.length).toBe(0);
74
+ expect(res.applied).toBe(0);
75
+ expect(res.skipped).toBe(1);
76
+ });
77
+
78
+ it('| BOOT-SAFE: egy hibás migráció NEM dob + NEM állítja meg a többit', async () => {
79
+ const ran: string[] = [];
80
+ const migs: DyNTS_MigrationEntry[] = [
81
+ entry('bad', async () => { throw new Error('boom'); }),
82
+ entry('good', async () => { ran.push('good'); return { touched: 2 }; }),
83
+ ];
84
+
85
+ const res = await runner.runPending(migs, '1.0.0');
86
+ expect(res.failed).toBe(1);
87
+ expect(res.applied).toBe(1); // 'good' a hiba UTÁN is lefutott
88
+ expect(ran).toEqual([ 'good' ]);
89
+ });
90
+ });
91
+ });