te.js 2.1.6 → 2.2.1
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/README.md +1 -12
- package/auto-docs/analysis/handler-analyzer.test.js +106 -0
- package/auto-docs/analysis/source-resolver.test.js +58 -0
- package/auto-docs/constants.js +13 -2
- package/auto-docs/openapi/generator.js +7 -5
- package/auto-docs/openapi/generator.test.js +132 -0
- package/auto-docs/openapi/spec-builders.js +39 -19
- package/cli/docs-command.js +44 -36
- package/cors/index.test.js +82 -0
- package/docs/README.md +1 -2
- package/docs/api-reference.md +124 -186
- package/docs/configuration.md +0 -13
- package/docs/getting-started.md +19 -21
- package/docs/rate-limiting.md +59 -58
- package/lib/llm/client.js +7 -2
- package/lib/llm/index.js +14 -1
- package/lib/llm/parse.test.js +60 -0
- package/package.json +3 -1
- package/radar/index.js +382 -0
- package/rate-limit/base.js +12 -15
- package/rate-limit/index.js +19 -22
- package/rate-limit/index.test.js +93 -0
- package/rate-limit/storage/memory.js +13 -13
- package/rate-limit/storage/redis-install.js +70 -0
- package/rate-limit/storage/redis.js +94 -52
- package/server/ammo/body-parser.js +156 -152
- package/server/ammo/body-parser.test.js +79 -0
- package/server/ammo/enhancer.js +8 -4
- package/server/ammo.js +138 -12
- package/server/context/request-context.js +51 -0
- package/server/context/request-context.test.js +53 -0
- package/server/endpoint.js +15 -0
- package/server/error.js +56 -3
- package/server/error.test.js +45 -0
- package/server/errors/channels/channels.test.js +148 -0
- package/server/errors/channels/index.js +1 -1
- package/server/errors/llm-cache.js +1 -1
- package/server/errors/llm-cache.test.js +160 -0
- package/server/errors/llm-error-service.js +1 -1
- package/server/errors/llm-rate-limiter.test.js +105 -0
- package/server/files/uploader.js +38 -26
- package/server/handler.js +1 -1
- package/server/targets/registry.js +3 -3
- package/server/targets/registry.test.js +108 -0
- package/te.js +233 -183
- package/utils/auto-register.js +1 -1
- package/utils/configuration.js +23 -9
- package/utils/configuration.test.js +58 -0
- package/utils/errors-llm-config.js +74 -8
- package/utils/request-logger.js +49 -3
- package/utils/startup.js +80 -0
- package/database/index.js +0 -165
- package/database/mongodb.js +0 -146
- package/database/redis.js +0 -201
- package/docs/database.md +0 -390
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { env } from 'tej-env';
|
|
8
|
+
import TejLogger from 'tej-logger';
|
|
9
|
+
|
|
10
|
+
const logger = new TejLogger('Tejas.ErrorsLlm');
|
|
8
11
|
|
|
9
12
|
const MESSAGE_TYPES = /** @type {const} */ (['endUser', 'developer']);
|
|
10
13
|
const LLM_MODES = /** @type {const} */ (['sync', 'async']);
|
|
@@ -65,6 +68,7 @@ function normalizeChannel(v) {
|
|
|
65
68
|
* rateLimit: number,
|
|
66
69
|
* cache: boolean,
|
|
67
70
|
* cacheTTL: number,
|
|
71
|
+
* verifyOnStart: boolean,
|
|
68
72
|
* }}
|
|
69
73
|
*/
|
|
70
74
|
export function getErrorsLlmConfig() {
|
|
@@ -134,7 +138,14 @@ export function getErrorsLlmConfig() {
|
|
|
134
138
|
? 3600000
|
|
135
139
|
: cacheTTLNum;
|
|
136
140
|
|
|
137
|
-
|
|
141
|
+
const verifyOnStartRaw = env('ERRORS_LLM_VERIFY_ON_START') ?? '';
|
|
142
|
+
const verifyOnStart =
|
|
143
|
+
verifyOnStartRaw === true ||
|
|
144
|
+
verifyOnStartRaw === 'true' ||
|
|
145
|
+
verifyOnStartRaw === '1' ||
|
|
146
|
+
verifyOnStartRaw === 1;
|
|
147
|
+
|
|
148
|
+
return Object.freeze({
|
|
138
149
|
enabled: Boolean(enabled),
|
|
139
150
|
baseURL: String(baseURL ?? '').trim(),
|
|
140
151
|
apiKey: String(apiKey ?? '').trim(),
|
|
@@ -147,11 +158,66 @@ export function getErrorsLlmConfig() {
|
|
|
147
158
|
rateLimit,
|
|
148
159
|
cache,
|
|
149
160
|
cacheTTL,
|
|
150
|
-
|
|
161
|
+
verifyOnStart,
|
|
162
|
+
});
|
|
151
163
|
}
|
|
152
164
|
|
|
153
165
|
export { MESSAGE_TYPES, LLM_MODES, LLM_CHANNELS };
|
|
154
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Fire a lightweight probe to the configured LLM provider and verify it
|
|
169
|
+
* responds correctly. Intended to run once at takeoff when `verifyOnStart: true`.
|
|
170
|
+
*
|
|
171
|
+
* Never throws — a flaky provider does not prevent the server from starting.
|
|
172
|
+
*
|
|
173
|
+
* @returns {Promise<{ ok: boolean, status: { feature: string, ok: boolean, detail: string } }>}
|
|
174
|
+
*/
|
|
175
|
+
export async function verifyLlmConnection() {
|
|
176
|
+
const { baseURL, apiKey, model, timeout, mode } = getErrorsLlmConfig();
|
|
177
|
+
|
|
178
|
+
const { createProvider } = await import('../lib/llm/index.js');
|
|
179
|
+
const provider = createProvider({ baseURL, apiKey, model, timeout });
|
|
180
|
+
|
|
181
|
+
const shortModel = model.split('/').pop().split(':')[0] || model;
|
|
182
|
+
const start = Date.now();
|
|
183
|
+
try {
|
|
184
|
+
const { content } = await provider.analyze(
|
|
185
|
+
'Respond with only the JSON object {"status":"ok"}. No explanation.',
|
|
186
|
+
);
|
|
187
|
+
const elapsed = Date.now() - start;
|
|
188
|
+
|
|
189
|
+
if (content.includes('"ok"')) {
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
status: {
|
|
193
|
+
feature: 'LLM Errors',
|
|
194
|
+
ok: true,
|
|
195
|
+
detail: `verified (${shortModel}, ${elapsed}ms, mode: ${mode})`,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
status: {
|
|
203
|
+
feature: 'LLM Errors',
|
|
204
|
+
ok: false,
|
|
205
|
+
detail: `unexpected response from ${shortModel} (${elapsed}ms)`,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
} catch (err) {
|
|
209
|
+
const elapsed = Date.now() - start;
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
status: {
|
|
213
|
+
feature: 'LLM Errors',
|
|
214
|
+
ok: false,
|
|
215
|
+
detail: `${err.message} (${elapsed}ms)`,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
155
221
|
/**
|
|
156
222
|
* Validate errors.llm when enabled: require baseURL, apiKey, and model (after LLM_ fallback).
|
|
157
223
|
* Also warns about misconfigurations (e.g. channel set with sync mode).
|
|
@@ -187,8 +253,8 @@ export function validateErrorsLlmAtTakeoff() {
|
|
|
187
253
|
env('ERRORS_LLM_CHANNEL') ?? env('LLM_CHANNEL') ?? '',
|
|
188
254
|
).trim();
|
|
189
255
|
if (mode === 'sync' && channelRaw) {
|
|
190
|
-
|
|
191
|
-
`
|
|
256
|
+
logger.warn(
|
|
257
|
+
`errors.llm: channel="${channel}" is set but mode is "sync" — channel output only applies in async mode. Set ERRORS_LLM_MODE=async to use it.`,
|
|
192
258
|
);
|
|
193
259
|
}
|
|
194
260
|
|
|
@@ -200,15 +266,15 @@ export function validateErrorsLlmAtTakeoff() {
|
|
|
200
266
|
rateLimitRaw &&
|
|
201
267
|
(isNaN(Number(rateLimitRaw)) || Number(rateLimitRaw) <= 0)
|
|
202
268
|
) {
|
|
203
|
-
|
|
204
|
-
`
|
|
269
|
+
logger.warn(
|
|
270
|
+
`errors.llm: rateLimit value "${rateLimitRaw}" is invalid; defaulting to 10.`,
|
|
205
271
|
);
|
|
206
272
|
}
|
|
207
273
|
|
|
208
274
|
const cacheTTLRaw = String(env('ERRORS_LLM_CACHE_TTL') ?? '').trim();
|
|
209
275
|
if (cacheTTLRaw && (isNaN(Number(cacheTTLRaw)) || Number(cacheTTLRaw) <= 0)) {
|
|
210
|
-
|
|
211
|
-
`
|
|
276
|
+
logger.warn(
|
|
277
|
+
`errors.llm: cacheTTL value "${cacheTTLRaw}" is invalid; defaulting to 3600000.`,
|
|
212
278
|
);
|
|
213
279
|
}
|
|
214
280
|
}
|
package/utils/request-logger.js
CHANGED
|
@@ -5,11 +5,46 @@ import TejLogger from 'tej-logger';
|
|
|
5
5
|
const logger = new TejLogger('Tejas.Request');
|
|
6
6
|
const { italic, bold, blue, white, bgGreen, bgRed, whiteBright } = ansi;
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Best-effort field names to mask when logging request/response bodies to the
|
|
10
|
+
* console. This is a hardcoded safety net — it does not replace the
|
|
11
|
+
* non-bypassable scrubbing enforced by the Radar collector on telemetry data.
|
|
12
|
+
*/
|
|
13
|
+
const CONSOLE_MASK_FIELDS = new Set([
|
|
14
|
+
'password',
|
|
15
|
+
'passwd',
|
|
16
|
+
'secret',
|
|
17
|
+
'token',
|
|
18
|
+
'authorization',
|
|
19
|
+
'api_key',
|
|
20
|
+
'apikey',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Recursively mask sensitive fields in a value for safe console output.
|
|
25
|
+
* Replaces matched key values with `"*"`.
|
|
26
|
+
*
|
|
27
|
+
* @param {unknown} value
|
|
28
|
+
* @returns {unknown}
|
|
29
|
+
*/
|
|
30
|
+
function maskForLog(value) {
|
|
31
|
+
if (value === null || typeof value !== 'object') return value;
|
|
32
|
+
if (Array.isArray(value)) return value.map(maskForLog);
|
|
33
|
+
|
|
34
|
+
const result = Object.create(null);
|
|
35
|
+
for (const [k, v] of Object.entries(value)) {
|
|
36
|
+
result[k] = CONSOLE_MASK_FIELDS.has(k.toLowerCase()) ? '*' : maskForLog(v);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
8
41
|
function logHttpRequest(ammo, next) {
|
|
9
42
|
if (!env('LOG_HTTP_REQUESTS')) return;
|
|
10
43
|
|
|
11
44
|
const startTime = new Date();
|
|
12
|
-
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
ammo.res.on('finish', { signal: controller.signal }, () => {
|
|
47
|
+
controller.abort();
|
|
13
48
|
const res = ammo.res;
|
|
14
49
|
const method = italic(whiteBright(ammo.method));
|
|
15
50
|
const endpoint = bold(ammo.endpoint);
|
|
@@ -19,10 +54,21 @@ function logHttpRequest(ammo, next) {
|
|
|
19
54
|
: bgGreen(whiteBright(bold(`✔ ${res.statusCode}`)));
|
|
20
55
|
|
|
21
56
|
const duration = white(`${new Date() - startTime}ms`);
|
|
57
|
+
|
|
58
|
+
const maskedPayload = maskForLog(ammo.payload);
|
|
22
59
|
const payload = `${blue('Request')}: ${white(
|
|
23
|
-
JSON.stringify(
|
|
60
|
+
JSON.stringify(maskedPayload),
|
|
24
61
|
)}`;
|
|
25
|
-
|
|
62
|
+
|
|
63
|
+
let maskedResponse = ammo.dispatchedData;
|
|
64
|
+
try {
|
|
65
|
+
maskedResponse = JSON.stringify(
|
|
66
|
+
maskForLog(JSON.parse(ammo.dispatchedData)),
|
|
67
|
+
);
|
|
68
|
+
} catch {
|
|
69
|
+
// Non-JSON response — log as-is
|
|
70
|
+
}
|
|
71
|
+
const dispatchedData = `${blue('Response')}: ${white(maskedResponse)}`;
|
|
26
72
|
const nextLine = '\n';
|
|
27
73
|
|
|
28
74
|
logger.log(
|
package/utils/startup.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read the framework's own package.json version.
|
|
7
|
+
* Resolves relative to the framework source, not the user's app.
|
|
8
|
+
* @returns {Promise<string>}
|
|
9
|
+
*/
|
|
10
|
+
export async function readFrameworkVersion() {
|
|
11
|
+
try {
|
|
12
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const raw = await readFile(path.join(dir, '..', 'package.json'), 'utf8');
|
|
14
|
+
return JSON.parse(raw).version ?? 'unknown';
|
|
15
|
+
} catch {
|
|
16
|
+
return 'unknown';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Format milliseconds into a compact human-readable string. */
|
|
21
|
+
export function fmtMs(ms) {
|
|
22
|
+
return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SPINNER = [
|
|
26
|
+
'\u280B',
|
|
27
|
+
'\u2819',
|
|
28
|
+
'\u2839',
|
|
29
|
+
'\u2838',
|
|
30
|
+
'\u283C',
|
|
31
|
+
'\u2834',
|
|
32
|
+
'\u2826',
|
|
33
|
+
'\u2827',
|
|
34
|
+
'\u2807',
|
|
35
|
+
'\u280F',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a live status line writer for startup progress.
|
|
40
|
+
* On TTY terminals, shows a spinning animation that gets overwritten in-place
|
|
41
|
+
* when the step completes. On non-TTY (piped, CI), only prints the final result.
|
|
42
|
+
* @param {boolean} isTTY
|
|
43
|
+
*/
|
|
44
|
+
export function statusLine(isTTY) {
|
|
45
|
+
let timer = null;
|
|
46
|
+
|
|
47
|
+
function stop() {
|
|
48
|
+
if (timer) {
|
|
49
|
+
clearInterval(timer);
|
|
50
|
+
timer = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
start(feature, message) {
|
|
56
|
+
if (!isTTY) return;
|
|
57
|
+
let frame = 0;
|
|
58
|
+
const render = () => {
|
|
59
|
+
const s = SPINNER[frame % SPINNER.length];
|
|
60
|
+
process.stdout.write(
|
|
61
|
+
`\r\x1b[K ${feature.padEnd(14)} \x1b[36m${s}\x1b[0m \x1b[2m${message}\x1b[0m`,
|
|
62
|
+
);
|
|
63
|
+
frame++;
|
|
64
|
+
};
|
|
65
|
+
render();
|
|
66
|
+
timer = setInterval(render, 80);
|
|
67
|
+
},
|
|
68
|
+
finish(feature, ok, detail) {
|
|
69
|
+
stop();
|
|
70
|
+
if (isTTY) process.stdout.write('\r\x1b[K');
|
|
71
|
+
const icon =
|
|
72
|
+
ok === true
|
|
73
|
+
? '\x1b[32m\u2713\x1b[0m'
|
|
74
|
+
: ok === false
|
|
75
|
+
? '\x1b[31m\u2717\x1b[0m'
|
|
76
|
+
: '\x1b[2m\u2014\x1b[0m';
|
|
77
|
+
process.stdout.write(` ${feature.padEnd(14)} ${icon} ${detail}\n`);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
package/database/index.js
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import redis from './redis.js';
|
|
2
|
-
import mongodb from './mongodb.js';
|
|
3
|
-
import TejError from '../server/error.js';
|
|
4
|
-
import TejLogger from 'tej-logger';
|
|
5
|
-
|
|
6
|
-
const logger = new TejLogger('DatabaseManager');
|
|
7
|
-
|
|
8
|
-
class DatabaseManager {
|
|
9
|
-
static #instance = null;
|
|
10
|
-
static #isInitializing = false;
|
|
11
|
-
|
|
12
|
-
// Enhanced connection tracking with metadata
|
|
13
|
-
#connections = new Map();
|
|
14
|
-
#initializingConnections = new Map();
|
|
15
|
-
|
|
16
|
-
// Helper method for sleeping
|
|
17
|
-
async #sleep(ms) {
|
|
18
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
constructor() {
|
|
22
|
-
if (DatabaseManager.#instance) {
|
|
23
|
-
return DatabaseManager.#instance;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (!DatabaseManager.#isInitializing) {
|
|
27
|
-
throw new TejError(
|
|
28
|
-
500,
|
|
29
|
-
'Use DatabaseManager.getInstance() to get the instance',
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
DatabaseManager.#isInitializing = false;
|
|
34
|
-
DatabaseManager.#instance = this;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
static getInstance() {
|
|
38
|
-
if (!DatabaseManager.#instance) {
|
|
39
|
-
DatabaseManager.#isInitializing = true;
|
|
40
|
-
DatabaseManager.#instance = new DatabaseManager();
|
|
41
|
-
}
|
|
42
|
-
return DatabaseManager.#instance;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async initializeConnection(dbType, config) {
|
|
46
|
-
const key = dbType.toLowerCase();
|
|
47
|
-
|
|
48
|
-
// If a connection already exists for this config, return it
|
|
49
|
-
if (this.#connections.has(key)) {
|
|
50
|
-
return this.#connections.get(key).client;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Set initializing flag
|
|
54
|
-
this.#initializingConnections.set(key, true);
|
|
55
|
-
|
|
56
|
-
let client;
|
|
57
|
-
try {
|
|
58
|
-
switch (key) {
|
|
59
|
-
case 'redis':
|
|
60
|
-
client = await redis.createConnection({
|
|
61
|
-
isCluster: config.isCluster || false,
|
|
62
|
-
options: config || {},
|
|
63
|
-
});
|
|
64
|
-
break;
|
|
65
|
-
case 'mongodb':
|
|
66
|
-
client = await mongodb.createConnection(config);
|
|
67
|
-
break;
|
|
68
|
-
default:
|
|
69
|
-
throw new TejError(400, `Unsupported database type: ${dbType}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
this.#connections.set(key, {
|
|
73
|
-
type: dbType,
|
|
74
|
-
client,
|
|
75
|
-
config,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Clear initializing flag
|
|
79
|
-
this.#initializingConnections.delete(key);
|
|
80
|
-
|
|
81
|
-
return client;
|
|
82
|
-
} catch (error) {
|
|
83
|
-
// Clear initializing flag on error
|
|
84
|
-
this.#initializingConnections.delete(key);
|
|
85
|
-
logger.error(`Failed to initialize ${dbType} connection:`, error);
|
|
86
|
-
throw error;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
getConnection(dbType) {
|
|
91
|
-
const key = dbType.toLowerCase();
|
|
92
|
-
const connection = this.#connections.get(key);
|
|
93
|
-
if (!connection) {
|
|
94
|
-
throw new TejError(
|
|
95
|
-
404,
|
|
96
|
-
`No connection found for ${dbType} with given config`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
return connection.client;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async closeConnection(dbType, config) {
|
|
103
|
-
const key = dbType.toLowerCase();
|
|
104
|
-
if (!this.#connections.has(key)) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
const connection = this.#connections.get(key);
|
|
110
|
-
switch (key) {
|
|
111
|
-
case 'redis':
|
|
112
|
-
await redis.closeConnection(connection.client);
|
|
113
|
-
break;
|
|
114
|
-
case 'mongodb':
|
|
115
|
-
await mongodb.closeConnection(connection.client);
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
this.#connections.delete(key);
|
|
120
|
-
} catch (error) {
|
|
121
|
-
logger.error(`Error closing ${dbType} connection:`, error);
|
|
122
|
-
throw error;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Close all database connections
|
|
128
|
-
* @returns {Promise<void>}
|
|
129
|
-
*/
|
|
130
|
-
async closeAllConnections() {
|
|
131
|
-
const closePromises = [];
|
|
132
|
-
for (const [key, connection] of this.#connections) {
|
|
133
|
-
closePromises.push(
|
|
134
|
-
this.closeConnection(connection.type, connection.config),
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
await Promise.all(closePromises);
|
|
138
|
-
this.#connections.clear();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Get all active connections
|
|
143
|
-
* @returns {Map<string, {type: string, client: any, config: Object}>}
|
|
144
|
-
*/
|
|
145
|
-
getActiveConnections() {
|
|
146
|
-
return new Map(this.#connections);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Check if a connection exists or is being initialized
|
|
151
|
-
* @param {string} dbType - Type of database
|
|
152
|
-
* @param {Object} config - Database configuration
|
|
153
|
-
* @returns {{ exists: boolean, initializing: boolean }}
|
|
154
|
-
*/
|
|
155
|
-
hasConnection(dbType, config) {
|
|
156
|
-
const key = dbType.toLowerCase();
|
|
157
|
-
return {
|
|
158
|
-
exists: this.#connections.has(key),
|
|
159
|
-
initializing: this.#initializingConnections.has(key),
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const dbManager = DatabaseManager.getInstance();
|
|
165
|
-
export default dbManager;
|
package/database/mongodb.js
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import TejLogger from 'tej-logger';
|
|
6
|
-
import TejError from '../server/error.js';
|
|
7
|
-
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = path.dirname(__filename);
|
|
10
|
-
|
|
11
|
-
const logger = new TejLogger('MongoDBConnectionManager');
|
|
12
|
-
|
|
13
|
-
function checkMongooseInstallation() {
|
|
14
|
-
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
15
|
-
const nodeModulesPath = path.join(
|
|
16
|
-
__dirname,
|
|
17
|
-
'..',
|
|
18
|
-
'node_modules',
|
|
19
|
-
'mongoose',
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
// Check if mongoose exists in package.json
|
|
24
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
25
|
-
const inPackageJson = !!packageJson.dependencies?.mongoose;
|
|
26
|
-
|
|
27
|
-
// Check if mongoose exists in node_modules
|
|
28
|
-
const inNodeModules = fs.existsSync(nodeModulesPath);
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
needsInstall: !inPackageJson || !inNodeModules,
|
|
32
|
-
reason: !inPackageJson
|
|
33
|
-
? 'not in package.json'
|
|
34
|
-
: !inNodeModules
|
|
35
|
-
? 'not in node_modules'
|
|
36
|
-
: null,
|
|
37
|
-
};
|
|
38
|
-
} catch (error) {
|
|
39
|
-
return { needsInstall: true, reason: 'error checking installation' };
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function installMongooseSync() {
|
|
44
|
-
const spinner = ['|', '/', '-', '\\'];
|
|
45
|
-
let current = 0;
|
|
46
|
-
let intervalId;
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const { needsInstall, reason } = checkMongooseInstallation();
|
|
50
|
-
|
|
51
|
-
if (!needsInstall) {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Start the spinner
|
|
56
|
-
intervalId = setInterval(() => {
|
|
57
|
-
process.stdout.write(`\r${spinner[current]} Installing mongoose...`);
|
|
58
|
-
current = (current + 1) % spinner.length;
|
|
59
|
-
}, 100);
|
|
60
|
-
|
|
61
|
-
logger.info(`Tejas will install mongoose (${reason})...`);
|
|
62
|
-
|
|
63
|
-
const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
64
|
-
const result = spawn.sync(command, ['install', 'mongoose'], {
|
|
65
|
-
stdio: 'inherit',
|
|
66
|
-
shell: true,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
process.stdout.write('\r');
|
|
70
|
-
clearInterval(intervalId);
|
|
71
|
-
|
|
72
|
-
if (result.status === 0) {
|
|
73
|
-
logger.info('Mongoose installed successfully');
|
|
74
|
-
return true;
|
|
75
|
-
} else {
|
|
76
|
-
logger.error('Mongoose installation failed');
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
} catch (error) {
|
|
80
|
-
if (intervalId) {
|
|
81
|
-
process.stdout.write('\r');
|
|
82
|
-
clearInterval(intervalId);
|
|
83
|
-
}
|
|
84
|
-
logger.error('Error installing mongoose:', error);
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Create a new MongoDB connection
|
|
91
|
-
* @param {Object} config - MongoDB configuration
|
|
92
|
-
* @param {string} config.uri - MongoDB connection URI
|
|
93
|
-
* @param {Object} [config.options={}] - Additional Mongoose options
|
|
94
|
-
* @returns {Promise<mongoose.Connection>} Mongoose connection instance
|
|
95
|
-
*/
|
|
96
|
-
async function createConnection(config) {
|
|
97
|
-
const { needsInstall } = checkMongooseInstallation();
|
|
98
|
-
|
|
99
|
-
if (needsInstall) {
|
|
100
|
-
const installed = installMongooseSync();
|
|
101
|
-
if (!installed) {
|
|
102
|
-
throw new TejError(500, 'Failed to install required mongoose package');
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const { uri, options = {} } = config;
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
const mongoose = await import('mongoose').then((mod) => mod.default);
|
|
110
|
-
const connection = await mongoose.createConnection(uri, options);
|
|
111
|
-
|
|
112
|
-
connection.on('error', (err) =>
|
|
113
|
-
logger.error(`MongoDB connection error:`, err),
|
|
114
|
-
);
|
|
115
|
-
connection.on('connected', () => {
|
|
116
|
-
logger.info(`MongoDB connected to ${uri}`);
|
|
117
|
-
});
|
|
118
|
-
connection.on('disconnected', () => {
|
|
119
|
-
logger.info(`MongoDB disconnected from ${uri}`);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
return connection;
|
|
123
|
-
} catch (error) {
|
|
124
|
-
logger.error(`Failed to create MongoDB connection:`, error);
|
|
125
|
-
throw new TejError(
|
|
126
|
-
500,
|
|
127
|
-
`Failed to create MongoDB connection: ${error.message}`,
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Close a MongoDB connection
|
|
134
|
-
* @param {mongoose.Connection} connection - Mongoose connection to close
|
|
135
|
-
* @returns {Promise<void>}
|
|
136
|
-
*/
|
|
137
|
-
async function closeConnection(connection) {
|
|
138
|
-
if (connection) {
|
|
139
|
-
await connection.close();
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export default {
|
|
144
|
-
createConnection,
|
|
145
|
-
closeConnection,
|
|
146
|
-
};
|