opencons 0.1.0

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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +382 -0
  3. package/opencons.d.ts +55 -0
  4. package/package.json +73 -0
  5. package/scripts/vendor-d3.js +22 -0
  6. package/src/core/context.js +44 -0
  7. package/src/core/index.js +198 -0
  8. package/src/core/tracer.js +252 -0
  9. package/src/drivers/db-language.js +207 -0
  10. package/src/drivers/detect.js +62 -0
  11. package/src/drivers/drizzle.js +87 -0
  12. package/src/drivers/index.js +43 -0
  13. package/src/drivers/mongoose.js +89 -0
  14. package/src/drivers/mysql2.js +116 -0
  15. package/src/drivers/pg.js +130 -0
  16. package/src/drivers/prisma.js +109 -0
  17. package/src/drivers/record.js +158 -0
  18. package/src/index.js +28 -0
  19. package/src/integrations/nest-lifecycle.js +357 -0
  20. package/src/integrations/nest.js +89 -0
  21. package/src/interceptors/express.js +270 -0
  22. package/src/interceptors/require-hook.js +109 -0
  23. package/src/lib/config.js +139 -0
  24. package/src/lib/errors.js +54 -0
  25. package/src/lib/http-response.js +37 -0
  26. package/src/lib/logger.js +69 -0
  27. package/src/lib/serialize.js +22 -0
  28. package/src/server/static.js +165 -0
  29. package/src/server/ws.js +62 -0
  30. package/src/store/source-cache.js +120 -0
  31. package/src/store/trace-store.js +117 -0
  32. package/src/transform/ast.js +255 -0
  33. package/src/transform/natural-language.js +146 -0
  34. package/src/transform/probe.js +161 -0
  35. package/src/transform/register.js +44 -0
  36. package/src/utils/label.js +26 -0
  37. package/src/utils/observable.js +103 -0
  38. package/widget/app.js +356 -0
  39. package/widget/db-language.js +90 -0
  40. package/widget/graph.js +1167 -0
  41. package/widget/index.html +132 -0
  42. package/widget/styles.css +773 -0
  43. package/widget/timeline.js +57 -0
  44. package/widget/vendor/d3.min.js +2 -0
@@ -0,0 +1,198 @@
1
+ 'use strict';
2
+
3
+ const { runWithContext, getCurrentContext } = require('./context');
4
+ const { TraceTracer } = require('./tracer');
5
+ const { createTraceStore } = require('../store/trace-store');
6
+ const { createWebSocketServer } = require('../server/ws');
7
+ const { createStaticServer } = require('../server/static');
8
+ const { installRequireHook } = require('../interceptors/require-hook');
9
+ const { installDrivers } = require('../drivers');
10
+ const { resolveOptions, isProductionDisabled } = require('../lib/config');
11
+ const { logger } = require('../lib/logger');
12
+ const { safeClone } = require('../lib/serialize');
13
+
14
+ /** @typedef {import('../lib/config').OpenconsOptions} OpenconsOptions */
15
+
16
+ /** @type {ReturnType<import('../store/trace-store').createTraceStore> | null} */
17
+ let traceStore = null;
18
+
19
+ /** @type {boolean} */
20
+ let initialised = false;
21
+
22
+ /** @type {OpenconsOptions | null} */
23
+ let activeOptions = null;
24
+
25
+ /**
26
+ * @param {Partial<OpenconsOptions>} [userOptions]
27
+ * @returns {import('express').RequestHandler}
28
+ */
29
+ function createOpencons(userOptions = {}) {
30
+ let options;
31
+
32
+ try {
33
+ options = resolveOptions(userOptions);
34
+ } catch (err) {
35
+ logger.error(`Invalid configuration: ${err.message}`);
36
+ throw err;
37
+ }
38
+
39
+ if (isProductionDisabled(options)) {
40
+ logger.warn('Disabled in production. Opencons is intended for development only.');
41
+ return (_req, _res, next) => next();
42
+ }
43
+
44
+ if (initialised) {
45
+ if (activeOptions && JSON.stringify(activeOptions) !== JSON.stringify(options)) {
46
+ logger.warn('Already initialised — additional options are ignored. Call opencons() once.');
47
+ }
48
+
49
+ return buildMiddleware(options);
50
+ }
51
+
52
+ initialised = true;
53
+ activeOptions = options;
54
+ traceStore = createTraceStore(options.maxTraces);
55
+
56
+ if (options.transform?.enabled) {
57
+ installRequireHook({
58
+ projectRoot: options.transform.projectRoot || process.cwd(),
59
+ exclude: options.transform.exclude || [],
60
+ });
61
+ }
62
+
63
+ installDrivers(options.drivers);
64
+
65
+ if (options.enableWidget) {
66
+ createStaticServer(options.port)
67
+ .then(({ port }) => {
68
+ options.widgetPort = port;
69
+ createWebSocketServer(traceStore);
70
+ })
71
+ .catch((err) => {
72
+ logger.error(`Widget server failed to start: ${err.message}`, err);
73
+ });
74
+ }
75
+
76
+ return buildMiddleware(options);
77
+ }
78
+
79
+ /**
80
+ * @param {OpenconsOptions} options
81
+ * @returns {import('express').RequestHandler}
82
+ */
83
+ function buildMiddleware(options) {
84
+ let loggedFirstRequest = false;
85
+
86
+ /**
87
+ * @param {import('express').Request} req
88
+ * @param {import('express').Response} res
89
+ * @param {import('express').NextFunction} next
90
+ */
91
+ function middleware(req, res, next) {
92
+ if (!traceStore) {
93
+ return next();
94
+ }
95
+
96
+ // Nest hook + prepend can both invoke this — trace once per request.
97
+ if (getCurrentContext()) {
98
+ return next();
99
+ }
100
+
101
+ if (shouldExclude(req, options.exclude)) {
102
+ return next();
103
+ }
104
+
105
+ if (!loggedFirstRequest) {
106
+ loggedFirstRequest = true;
107
+ logger.info(`Tracing ${req.method} ${req.originalUrl || req.url}`);
108
+ }
109
+
110
+ /** @type {unknown} */
111
+ let capturedResponse;
112
+
113
+ if (options.captureResponse) {
114
+ attachResponseCapture(res, (body) => {
115
+ capturedResponse = safeClone(body);
116
+ });
117
+ }
118
+
119
+ const tracer = new TraceTracer({
120
+ method: req.method,
121
+ url: req.originalUrl || req.url,
122
+ params: req.params,
123
+ body: options.captureBody ? safeClone(req.body) : undefined,
124
+ });
125
+
126
+ tracer.onChange = () => {
127
+ traceStore.update(tracer.snapshot());
128
+ };
129
+
130
+ traceStore.start(tracer.snapshot());
131
+
132
+ const context = {
133
+ id: tracer.id,
134
+ startTime: tracer.startTime,
135
+ tracer,
136
+ };
137
+
138
+ let finished = false;
139
+
140
+ const onFinish = () => {
141
+ if (finished) return;
142
+ finished = true;
143
+ tracer.onChange = null;
144
+ traceStore.complete(tracer.finish(res.statusCode, capturedResponse));
145
+ };
146
+
147
+ res.on('finish', onFinish);
148
+ res.on('close', onFinish);
149
+
150
+ runWithContext(context, () => next());
151
+ }
152
+
153
+ middleware.getTraces = () => (traceStore ? traceStore.getAll() : []);
154
+ middleware.options = options;
155
+ middleware.__openconsEntry = true;
156
+
157
+ return middleware;
158
+ }
159
+
160
+ /**
161
+ * @param {import('express').Response} res
162
+ * @param {(body: unknown) => void} onCapture
163
+ */
164
+ function attachResponseCapture(res, onCapture) {
165
+ const originalJson = res.json.bind(res);
166
+ res.json = function patchedJson(body) {
167
+ onCapture(body);
168
+ return originalJson(body);
169
+ };
170
+
171
+ const originalSend = res.send.bind(res);
172
+ res.send = function patchedSend(body) {
173
+ onCapture(body);
174
+ return originalSend(body);
175
+ };
176
+ }
177
+
178
+ /**
179
+ * @param {import('express').Request} req
180
+ * @param {string[]} excludePatterns
181
+ */
182
+ function shouldExclude(req, excludePatterns) {
183
+ const path = req.originalUrl || req.url;
184
+
185
+ return excludePatterns.some((pattern) => {
186
+ if (pattern.includes('*')) {
187
+ const regex = new RegExp(
188
+ `^${pattern.replace(/\*/g, '.*').replace(/\//g, '\\/')}$`
189
+ );
190
+ return regex.test(path.split('?')[0]);
191
+ }
192
+ return path.startsWith(pattern);
193
+ });
194
+ }
195
+
196
+ module.exports = {
197
+ createOpencons,
198
+ };
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ const { randomBytes } = require('crypto');
4
+ const { getCurrentContext } = require('./context');
5
+
6
+ /**
7
+ * @typedef {'request' | 'middleware' | 'controller' | 'branch' | 'loop' | 'db' | 'response' | 'error'} NodeType
8
+ */
9
+
10
+ /**
11
+ * @typedef {Object} TraceNode
12
+ * @property {string} id
13
+ * @property {NodeType} type
14
+ * @property {string} label
15
+ * @property {string} [summary]
16
+ * @property {string} [condition]
17
+ * @property {boolean} [has_else]
18
+ * @property {{ key: string, label: string, taken: boolean }[]} [outcomes]
19
+ * @property {string | null} [taken_outcome]
20
+ * @property {number | null} duration_ms
21
+ * @property {boolean} [called_next]
22
+ * @property {string} [exit_reason]
23
+ * @property {*} [value]
24
+ * @property {number} [rows]
25
+ * @property {string} [query]
26
+ * @property {unknown} [params]
27
+ * @property {string} [driver]
28
+ * @property {string} [operation]
29
+ * @property {string} [collection]
30
+ * @property {'select' | 'insert' | 'update' | 'delete' | 'count' | 'transaction' | 'query'} [db_action]
31
+ * @property {string} [db_intent]
32
+ * @property {string} [db_result]
33
+ * @property {{ file: string, line: number | null, kind?: string }} [source]
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} TraceEdge
38
+ * @property {string} from
39
+ * @property {string} to
40
+ * @property {boolean} [parallel]
41
+ */
42
+
43
+ /**
44
+ * @typedef {Object} TraceGraph
45
+ * @property {string} id
46
+ * @property {number} timestamp
47
+ * @property {string} method
48
+ * @property {string} url
49
+ * @property {Record<string, string>} params
50
+ * @property {unknown} [body]
51
+ * @property {unknown} [response]
52
+ * @property {number | null} status
53
+ * @property {number} duration_ms
54
+ * @property {'active' | 'complete'} [state]
55
+ * @property {TraceNode[]} nodes
56
+ * @property {TraceEdge[]} edges
57
+ */
58
+
59
+ class TraceTracer {
60
+ /**
61
+ * @param {Object} meta
62
+ * @param {string} meta.method
63
+ * @param {string} meta.url
64
+ * @param {Record<string, string>} [meta.params]
65
+ * @param {unknown} [meta.body]
66
+ */
67
+ constructor(meta) {
68
+ this.id = `req_${randomBytes(4).toString('hex')}`;
69
+ this.timestamp = Date.now();
70
+ this.method = meta.method;
71
+ this.url = meta.url;
72
+ this.params = meta.params || {};
73
+ this.body = meta.body;
74
+ this.status = null;
75
+ this.startTime = performance.now();
76
+ this._nodeCounter = 0;
77
+ this._lastNodeId = null;
78
+
79
+ const requestNode = this._createNode({
80
+ type: 'request',
81
+ label: `${meta.method} ${meta.url}`,
82
+ duration_ms: null,
83
+ });
84
+
85
+ this.nodes = [requestNode];
86
+ this.edges = [];
87
+ this._lastNodeId = requestNode.id;
88
+ this._finished = false;
89
+
90
+ /** @type {(() => void) | null} */
91
+ this.onChange = null;
92
+ }
93
+
94
+ _nextNodeId() {
95
+ this._nodeCounter += 1;
96
+ return `n${this._nodeCounter}`;
97
+ }
98
+
99
+ /**
100
+ * @param {Omit<TraceNode, 'id'>} nodeData
101
+ * @returns {TraceNode}
102
+ */
103
+ _createNode(nodeData) {
104
+ return {
105
+ id: this._nextNodeId(),
106
+ ...nodeData,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * @param {Omit<TraceNode, 'id'>} nodeData
112
+ * @returns {TraceNode}
113
+ */
114
+ addNode(nodeData) {
115
+ const node = this._createNode(nodeData);
116
+ this.nodes.push(node);
117
+
118
+ if (this._lastNodeId) {
119
+ this.edges.push({ from: this._lastNodeId, to: node.id });
120
+ }
121
+
122
+ this._lastNodeId = node.id;
123
+ this._setScopeNode(node);
124
+ this._notifyChange();
125
+ return node;
126
+ }
127
+
128
+ /**
129
+ * Record a concurrent branch (e.g. database query) from the active handler scope.
130
+ * @param {string | null | undefined} parentId
131
+ * @param {Omit<TraceNode, 'id'>} nodeData
132
+ * @returns {TraceNode}
133
+ */
134
+ addForkNode(parentId, nodeData) {
135
+ const node = this._createNode(nodeData);
136
+ const from = parentId || this._lastNodeId;
137
+
138
+ this.nodes.push(node);
139
+
140
+ if (from) {
141
+ this.edges.push({ from, to: node.id, parallel: true });
142
+ }
143
+
144
+ this._notifyChange();
145
+ return node;
146
+ }
147
+
148
+ /**
149
+ * @param {string} nodeId
150
+ * @param {Partial<TraceNode>} patch
151
+ */
152
+ updateNode(nodeId, patch) {
153
+ const node = this.nodes.find((entry) => entry.id === nodeId);
154
+ if (!node) return;
155
+ Object.assign(node, patch);
156
+ this._notifyChange();
157
+ }
158
+
159
+ /**
160
+ * @returns {string | null}
161
+ */
162
+ getLastSequentialNodeId() {
163
+ return this._lastNodeId;
164
+ }
165
+
166
+ /**
167
+ * @param {TraceNode} node
168
+ */
169
+ _setScopeNode(node) {
170
+ if (node.type === 'db') return;
171
+
172
+ const ctx = getCurrentContext();
173
+ if (ctx) {
174
+ ctx.scopeNodeId = node.id;
175
+ }
176
+ }
177
+
178
+ _notifyChange() {
179
+ if (this.onChange && !this._finished) {
180
+ this.onChange();
181
+ }
182
+ }
183
+
184
+ /**
185
+ * @returns {TraceGraph}
186
+ */
187
+ snapshot() {
188
+ return {
189
+ id: this.id,
190
+ timestamp: this.timestamp,
191
+ method: this.method,
192
+ url: this.url,
193
+ params: this.params,
194
+ body: this.body,
195
+ status: this.status,
196
+ state: 'active',
197
+ duration_ms: Math.round((performance.now() - this.startTime) * 10) / 10,
198
+ nodes: this.nodes,
199
+ edges: this.edges,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * @param {string} from
205
+ * @param {string} to
206
+ */
207
+ addEdge(from, to) {
208
+ this.edges.push({ from, to });
209
+ }
210
+
211
+ /**
212
+ * @param {number} status
213
+ * @param {unknown} [response]
214
+ * @returns {TraceGraph}
215
+ */
216
+ finish(status, response) {
217
+ this._finished = true;
218
+ this.status = status;
219
+ const duration_ms = Math.round((performance.now() - this.startTime) * 10) / 10;
220
+
221
+ const responseNode = this._createNode({
222
+ type: 'response',
223
+ label: `${status}`,
224
+ duration_ms: null,
225
+ });
226
+
227
+ this.nodes.push(responseNode);
228
+
229
+ if (this._lastNodeId) {
230
+ this.edges.push({ from: this._lastNodeId, to: responseNode.id });
231
+ }
232
+
233
+ return {
234
+ id: this.id,
235
+ timestamp: this.timestamp,
236
+ method: this.method,
237
+ url: this.url,
238
+ params: this.params,
239
+ body: this.body,
240
+ response,
241
+ status: this.status,
242
+ state: 'complete',
243
+ duration_ms,
244
+ nodes: this.nodes,
245
+ edges: this.edges,
246
+ };
247
+ }
248
+ }
249
+
250
+ module.exports = {
251
+ TraceTracer,
252
+ };
@@ -0,0 +1,207 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @param {string} sql
5
+ */
6
+ function extractTableFromSql(sql) {
7
+ const text = String(sql || '').replace(/\s+/g, ' ').trim();
8
+
9
+ const patterns = [
10
+ /\bfrom\s+["'`]?([a-zA-Z_][\w$]*)/i,
11
+ /\binto\s+["'`]?([a-zA-Z_][\w$]*)/i,
12
+ /\bupdate\s+["'`]?([a-zA-Z_][\w$]*)/i,
13
+ /\bjoin\s+["'`]?([a-zA-Z_][\w$]*)/i,
14
+ ];
15
+
16
+ for (const pattern of patterns) {
17
+ const match = text.match(pattern);
18
+ if (match) return match[1];
19
+ }
20
+
21
+ return undefined;
22
+ }
23
+
24
+ /**
25
+ * @param {string | undefined} name
26
+ */
27
+ function humanizeName(name) {
28
+ if (!name) return 'records';
29
+
30
+ return String(name)
31
+ .replace(/^["'`]|["'`]$/g, '')
32
+ .replace(/_/g, ' ')
33
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
34
+ .toLowerCase();
35
+ }
36
+
37
+ /**
38
+ * @param {object} meta
39
+ * @param {string} [meta.driver]
40
+ * @param {string} [meta.operation]
41
+ * @param {string} [meta.query]
42
+ */
43
+ function inferDbAction(meta) {
44
+ const operation = String(meta.operation || '').toLowerCase();
45
+ const query = String(meta.query || '').trim();
46
+ const sql = query.toUpperCase();
47
+
48
+ if (meta.driver === 'prisma') {
49
+ if (/^find|aggregate|groupby/i.test(operation)) return 'select';
50
+ if (/^create/i.test(operation)) return 'insert';
51
+ if (/^update|upsert/i.test(operation)) return 'update';
52
+ if (/^delete/i.test(operation)) return 'delete';
53
+ if (operation === 'count') return 'count';
54
+ if (operation === 'queryRaw' || operation === 'executeRaw') return 'query';
55
+ return 'query';
56
+ }
57
+
58
+ if (meta.driver === 'mongoose') {
59
+ if (/find|distinct|count/i.test(operation)) return operation.includes('count') ? 'count' : 'select';
60
+ if (/insert|save|create/i.test(operation)) return 'insert';
61
+ if (/update|replace/i.test(operation)) return 'update';
62
+ if (/delete|remove/i.test(operation)) return 'delete';
63
+ return 'query';
64
+ }
65
+
66
+ if (sql.startsWith('SELECT') || sql.startsWith('WITH')) return 'select';
67
+ if (sql.startsWith('INSERT')) return 'insert';
68
+ if (sql.startsWith('UPDATE')) return 'update';
69
+ if (sql.startsWith('DELETE')) return 'delete';
70
+ if (/\bCOUNT\s*\(/i.test(sql)) return 'count';
71
+ if (/^(BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE)/i.test(sql)) return 'transaction';
72
+
73
+ return 'query';
74
+ }
75
+
76
+ /**
77
+ * @param {'select' | 'insert' | 'update' | 'delete' | 'count' | 'transaction' | 'query'} action
78
+ * @param {string | undefined} table
79
+ */
80
+ function describeDbIntent(action, table) {
81
+ const target = humanizeName(table);
82
+
83
+ switch (action) {
84
+ case 'select':
85
+ return `Fetching ${target}`;
86
+ case 'insert':
87
+ return `Saving to ${target}`;
88
+ case 'update':
89
+ return `Updating ${target}`;
90
+ case 'delete':
91
+ return `Removing from ${target}`;
92
+ case 'count':
93
+ return `Counting ${target}`;
94
+ case 'transaction':
95
+ return 'Running a database transaction';
96
+ default:
97
+ return table ? `Querying ${target}` : 'Running a database query';
98
+ }
99
+ }
100
+
101
+ /**
102
+ * @param {'select' | 'insert' | 'update' | 'delete' | 'count' | 'transaction' | 'query'} action
103
+ * @param {number | undefined} rows
104
+ * @param {string | undefined} error
105
+ */
106
+ function describeDbResult(action, rows, error) {
107
+ if (error) {
108
+ const short = error.length > 72 ? `${error.slice(0, 72)}…` : error;
109
+ return `Failed — ${short}`;
110
+ }
111
+
112
+ switch (action) {
113
+ case 'select':
114
+ if (rows === 0) return 'Nothing found';
115
+ if (rows === 1) return 'Returned 1 record';
116
+ if (rows != null) return `Returned ${rows} records`;
117
+ return 'Lookup finished';
118
+ case 'insert':
119
+ if (rows === 1) return 'Submitted 1 new row';
120
+ if (rows != null && rows > 1) return `Submitted ${rows} new rows`;
121
+ return 'Save completed';
122
+ case 'update':
123
+ if (rows === 0) return 'No rows changed';
124
+ if (rows === 1) return 'Updated 1 row';
125
+ if (rows != null) return `Updated ${rows} rows`;
126
+ return 'Update completed';
127
+ case 'delete':
128
+ if (rows === 0) return 'Nothing removed';
129
+ if (rows === 1) return 'Removed 1 row';
130
+ if (rows != null) return `Removed ${rows} rows`;
131
+ return 'Delete completed';
132
+ case 'count':
133
+ if (rows != null) return `Count is ${rows}`;
134
+ return 'Count completed';
135
+ case 'transaction':
136
+ return 'Transaction step completed';
137
+ default:
138
+ if (rows === 0) return 'No rows affected';
139
+ if (rows === 1) return '1 row affected';
140
+ if (rows != null) return `${rows} rows affected`;
141
+ return 'Query completed';
142
+ }
143
+ }
144
+
145
+ /**
146
+ * @param {'select' | 'insert' | 'update' | 'delete' | 'count' | 'transaction' | 'query'} action
147
+ * @param {string | undefined} table
148
+ */
149
+ function describeDbLabel(action, table) {
150
+ const target = humanizeName(table);
151
+
152
+ switch (action) {
153
+ case 'select':
154
+ return `Fetch ${target}`;
155
+ case 'insert':
156
+ return `Save ${target}`;
157
+ case 'update':
158
+ return `Update ${target}`;
159
+ case 'delete':
160
+ return `Delete ${target}`;
161
+ case 'count':
162
+ return `Count ${target}`;
163
+ case 'transaction':
164
+ return 'Transaction';
165
+ default:
166
+ return table ? `Query ${target}` : 'Database query';
167
+ }
168
+ }
169
+
170
+ /**
171
+ * @param {object} meta
172
+ * @param {string} [meta.driver]
173
+ * @param {string} [meta.operation]
174
+ * @param {string} [meta.query]
175
+ * @param {string} [meta.collection]
176
+ * @param {number} [meta.rows]
177
+ * @param {number} [meta.duration_ms]
178
+ * @param {string} [meta.error]
179
+ */
180
+ function buildDbNodeLanguage(meta) {
181
+ const action = inferDbAction(meta);
182
+ const table = meta.collection || extractTableFromSql(meta.query);
183
+ const intent = describeDbIntent(action, table);
184
+ const result = describeDbResult(action, meta.rows, meta.error);
185
+ const label = describeDbLabel(action, table);
186
+ const summary = meta.error
187
+ ? result
188
+ : `${result}${meta.duration_ms != null ? ` · ${meta.duration_ms}ms` : ''}`;
189
+
190
+ return {
191
+ label,
192
+ summary,
193
+ db_action: action,
194
+ db_intent: intent,
195
+ db_result: result,
196
+ };
197
+ }
198
+
199
+ module.exports = {
200
+ extractTableFromSql,
201
+ humanizeName,
202
+ inferDbAction,
203
+ describeDbIntent,
204
+ describeDbResult,
205
+ describeDbLabel,
206
+ buildDbNodeLanguage,
207
+ };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const { createRequire } = require('module');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ /**
8
+ * @param {string} packageName
9
+ */
10
+ function isPackageInstalled(packageName) {
11
+ const searchPaths = [
12
+ process.cwd(),
13
+ path.join(process.cwd(), 'node_modules'),
14
+ ...(require.main?.paths || []),
15
+ ];
16
+
17
+ const hostRequire = createRequire(path.join(process.cwd(), 'package.json'));
18
+
19
+ for (const base of searchPaths) {
20
+ try {
21
+ hostRequire.resolve(packageName, { paths: [base] });
22
+ return true;
23
+ } catch {
24
+ // try next
25
+ }
26
+ }
27
+
28
+ const localNodeModules = path.join(process.cwd(), 'node_modules', packageName);
29
+ return fs.existsSync(localNodeModules);
30
+ }
31
+
32
+ /**
33
+ * @param {Partial<Record<'mongoose' | 'pg' | 'prisma' | 'mysql2' | 'drizzle', boolean>>} [config]
34
+ */
35
+ function resolveDriverConfig(config = {}) {
36
+ const detected = {
37
+ mongoose: isPackageInstalled('mongoose'),
38
+ pg: isPackageInstalled('pg'),
39
+ prisma: isPackageInstalled('@prisma/client'),
40
+ mysql2: isPackageInstalled('mysql2'),
41
+ drizzle: isPackageInstalled('drizzle-orm'),
42
+ };
43
+
44
+ const drizzle = config.drizzle !== false && detected.drizzle;
45
+
46
+ // Drizzle uses pg/mysql2 underneath — patch at the ORM layer to avoid duplicate nodes.
47
+ const pgExplicit = config.pg === true;
48
+ const mysql2Explicit = config.mysql2 === true;
49
+
50
+ return {
51
+ mongoose: config.mongoose !== false && detected.mongoose,
52
+ drizzle,
53
+ pg: config.pg !== false && detected.pg && (!drizzle || pgExplicit),
54
+ prisma: config.prisma !== false && detected.prisma,
55
+ mysql2: config.mysql2 !== false && detected.mysql2 && (!drizzle || mysql2Explicit),
56
+ };
57
+ }
58
+
59
+ module.exports = {
60
+ isPackageInstalled,
61
+ resolveDriverConfig,
62
+ };