@vipin733/nodescope 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.
- package/LICENSE +21 -0
- package/README.md +412 -0
- package/dist/chunk-6H665NNC.js +198 -0
- package/dist/chunk-6KBKW63X.js +145 -0
- package/dist/chunk-BOBKU5LG.js +163 -0
- package/dist/chunk-OF6NKXP5.js +44 -0
- package/dist/chunk-V5BR4MSS.js +195 -0
- package/dist/index.cjs +2926 -0
- package/dist/index.d.cts +928 -0
- package/dist/index.d.ts +928 -0
- package/dist/index.js +2092 -0
- package/dist/memory-5GF7O2HJ.js +7 -0
- package/dist/mysql-KNBA3N7P.js +7 -0
- package/dist/postgresql-XD7N5SFI.js +7 -0
- package/dist/sqlite-DMOIPBIO.js +7 -0
- package/package.json +108 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vipin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# @vipin733/nodescope
|
|
2
|
+
|
|
3
|
+
> 🔭 A Laravel Telescope-inspired debugging and monitoring package for Node.js and Bun applications
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@vipin733/nodescope)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## ✨ Features
|
|
9
|
+
|
|
10
|
+
- 📊 **Real-time Monitoring** - Watch requests, queries, and logs as they happen via WebSocket streaming
|
|
11
|
+
- 💾 **Multiple Storage Backends** - Choose from in-memory, SQLite, PostgreSQL, or MySQL
|
|
12
|
+
- 🔌 **Framework Agnostic** - First-class support for Express, Hono, Fastify, and vanilla Node.js
|
|
13
|
+
- 🎨 **Beautiful Dashboard** - React-based UI with dark/light mode (coming soon)
|
|
14
|
+
- 🚀 **Zero Configuration** - Sensible defaults, just install and go
|
|
15
|
+
- 🔍 **Comprehensive Watchers** - Track requests, database queries, logs, exceptions, and more
|
|
16
|
+
- ⚡ **Blazing Fast** - Minimal performance overhead, designed for production use
|
|
17
|
+
- 🌐 **WebSocket Support** - Real-time data streaming to connected clients
|
|
18
|
+
|
|
19
|
+
## 📦 Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @vipin733/nodescope
|
|
23
|
+
# or
|
|
24
|
+
pnpm add @vipin733/nodescope
|
|
25
|
+
# or
|
|
26
|
+
yarn add @vipin733/nodescope
|
|
27
|
+
# or
|
|
28
|
+
bun add @vipin733/nodescope
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Optional Database Drivers
|
|
32
|
+
|
|
33
|
+
NodeScope supports multiple storage backends. Install the driver you need:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# SQLite
|
|
37
|
+
npm install better-sqlite3
|
|
38
|
+
|
|
39
|
+
# PostgreSQL
|
|
40
|
+
npm install pg
|
|
41
|
+
|
|
42
|
+
# MySQL
|
|
43
|
+
npm install mysql2
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 🚀 Quick Start
|
|
47
|
+
|
|
48
|
+
### Express.js
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import express from 'express';
|
|
52
|
+
import { NodeScope } from '@vipin733/nodescope';
|
|
53
|
+
|
|
54
|
+
const app = express();
|
|
55
|
+
|
|
56
|
+
// Initialize NodeScope
|
|
57
|
+
const nodescope = new NodeScope({
|
|
58
|
+
storage: 'sqlite',
|
|
59
|
+
storagePath: './nodescope.db',
|
|
60
|
+
dashboardPath: '/_debug',
|
|
61
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Mount NodeScope middleware and routes
|
|
65
|
+
app.use(nodescope.middleware());
|
|
66
|
+
nodescope.mountExpressRoutes(app);
|
|
67
|
+
|
|
68
|
+
// Your app routes
|
|
69
|
+
app.get('/', (req, res) => {
|
|
70
|
+
res.send('Hello World!');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Start server
|
|
74
|
+
app.listen(3000, () => {
|
|
75
|
+
console.log('Server running on http://localhost:3000');
|
|
76
|
+
console.log('Dashboard available at http://localhost:3000/_debug');
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Hono (Bun/Node)
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { Hono } from 'hono';
|
|
84
|
+
import { createHonoMiddleware } from '@vipin733/nodescope';
|
|
85
|
+
|
|
86
|
+
const app = new Hono();
|
|
87
|
+
|
|
88
|
+
// Add NodeScope middleware
|
|
89
|
+
app.use('*', createHonoMiddleware({
|
|
90
|
+
storage: 'memory',
|
|
91
|
+
dashboardPath: '/_debug'
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
// Your app routes
|
|
95
|
+
app.get('/', (c) => c.text('Hello Hono!'));
|
|
96
|
+
|
|
97
|
+
export default app;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Fastify
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import Fastify from 'fastify';
|
|
104
|
+
import { createFastifyPlugin } from '@vipin733/nodescope';
|
|
105
|
+
|
|
106
|
+
const fastify = Fastify({ logger: true });
|
|
107
|
+
|
|
108
|
+
// Register NodeScope plugin
|
|
109
|
+
await fastify.register(createFastifyPlugin, {
|
|
110
|
+
storage: 'memory',
|
|
111
|
+
dashboardPath: '/_debug',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Your app routes
|
|
115
|
+
fastify.get('/', async (request, reply) => {
|
|
116
|
+
return { hello: 'world' };
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await fastify.listen({ port: 3000 });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## ⚙️ Configuration
|
|
123
|
+
|
|
124
|
+
### Constructor Options
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
interface NodeScopeOptions {
|
|
128
|
+
// Enable/disable NodeScope (default: true)
|
|
129
|
+
enabled?: boolean;
|
|
130
|
+
|
|
131
|
+
// Storage backend: 'memory' | 'sqlite' | 'postgresql' | 'mysql'
|
|
132
|
+
storage?: StorageType;
|
|
133
|
+
|
|
134
|
+
// Storage configuration
|
|
135
|
+
storagePath?: string; // For SQLite
|
|
136
|
+
databaseUrl?: string; // For PostgreSQL/MySQL
|
|
137
|
+
|
|
138
|
+
// Dashboard configuration
|
|
139
|
+
dashboardPath?: string; // Default: '/_debug'
|
|
140
|
+
|
|
141
|
+
// Data retention
|
|
142
|
+
maxEntries?: number; // Default: 1000
|
|
143
|
+
pruneInterval?: number; // In milliseconds, default: 60000
|
|
144
|
+
|
|
145
|
+
// WebSocket configuration
|
|
146
|
+
wsPath?: string; // Default: '/_debug/ws'
|
|
147
|
+
|
|
148
|
+
// Watchers to enable
|
|
149
|
+
watchers?: {
|
|
150
|
+
request?: boolean;
|
|
151
|
+
query?: boolean;
|
|
152
|
+
cache?: boolean;
|
|
153
|
+
log?: boolean;
|
|
154
|
+
exception?: boolean;
|
|
155
|
+
httpClient?: boolean;
|
|
156
|
+
event?: boolean;
|
|
157
|
+
job?: boolean;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Example Configurations
|
|
163
|
+
|
|
164
|
+
#### Development (In-Memory)
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const nodescope = new NodeScope({
|
|
168
|
+
storage: 'memory',
|
|
169
|
+
maxEntries: 500,
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### Production (PostgreSQL)
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
const nodescope = new NodeScope({
|
|
177
|
+
storage: 'postgresql',
|
|
178
|
+
databaseUrl: process.env.DATABASE_URL,
|
|
179
|
+
maxEntries: 5000,
|
|
180
|
+
pruneInterval: 300000, // Prune every 5 minutes
|
|
181
|
+
enabled: process.env.ENABLE_NODESCOPE === 'true',
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### SQLite for Local Development
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const nodescope = new NodeScope({
|
|
189
|
+
storage: 'sqlite',
|
|
190
|
+
storagePath: './data/nodescope.db',
|
|
191
|
+
dashboardPath: '/_telescope',
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## 📊 Watchers
|
|
196
|
+
|
|
197
|
+
NodeScope includes the following watchers to monitor different aspects of your application:
|
|
198
|
+
|
|
199
|
+
| Watcher | Description | Status |
|
|
200
|
+
|---------|-------------|--------|
|
|
201
|
+
| **Request** | HTTP requests and responses with timing | ✅ Active |
|
|
202
|
+
| **Query** | Database queries with execution time | ✅ Active |
|
|
203
|
+
| **Cache** | Cache operations (Redis, memory) | 🚧 Coming Soon |
|
|
204
|
+
| **Log** | Console and custom logs | ✅ Active |
|
|
205
|
+
| **Exception** | Errors with stack traces | ✅ Active |
|
|
206
|
+
| **HTTP Client** | Outgoing HTTP requests | 🚧 Coming Soon |
|
|
207
|
+
| **Event** | Application events | 🚧 Coming Soon |
|
|
208
|
+
| **Job** | Background job processing | 🚧 Coming Soon |
|
|
209
|
+
|
|
210
|
+
## 💾 Storage Options
|
|
211
|
+
|
|
212
|
+
### Memory (Development)
|
|
213
|
+
|
|
214
|
+
Fast, in-memory storage. **Not recommended for production** as data is lost on restart.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
{ storage: 'memory' }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### SQLite (Development/Small Production)
|
|
221
|
+
|
|
222
|
+
Single-file database, perfect for local development and small production deployments.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
{
|
|
226
|
+
storage: 'sqlite',
|
|
227
|
+
storagePath: './nodescope.db'
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### PostgreSQL (Production)
|
|
232
|
+
|
|
233
|
+
Production-ready with connection pooling and excellent performance.
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
{
|
|
237
|
+
storage: 'postgresql',
|
|
238
|
+
databaseUrl: 'postgresql://user:password@localhost:5432/mydb'
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### MySQL (Production)
|
|
243
|
+
|
|
244
|
+
Production-ready MySQL support with connection pooling.
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
{
|
|
248
|
+
storage: 'mysql',
|
|
249
|
+
databaseUrl: 'mysql://user:password@localhost:3306/mydb'
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## 🔍 Manual Logging
|
|
254
|
+
|
|
255
|
+
You can manually log events to NodeScope:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import { nodescope } from '@vipin733/nodescope';
|
|
259
|
+
|
|
260
|
+
// Log a request
|
|
261
|
+
nodescope.record('request', {
|
|
262
|
+
method: 'GET',
|
|
263
|
+
path: '/api/users',
|
|
264
|
+
statusCode: 200,
|
|
265
|
+
duration: 145,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Log a database query
|
|
269
|
+
nodescope.record('query', {
|
|
270
|
+
sql: 'SELECT * FROM users WHERE id = ?',
|
|
271
|
+
bindings: [1],
|
|
272
|
+
duration: 12,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Log an exception
|
|
276
|
+
nodescope.record('exception', {
|
|
277
|
+
message: 'User not found',
|
|
278
|
+
stack: error.stack,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Log a custom log
|
|
282
|
+
nodescope.record('log', {
|
|
283
|
+
level: 'info',
|
|
284
|
+
message: 'User logged in',
|
|
285
|
+
context: { userId: 123 },
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## 🌐 WebSocket API
|
|
290
|
+
|
|
291
|
+
NodeScope provides real-time updates via WebSocket. Connect to the WebSocket endpoint to receive live data:
|
|
292
|
+
|
|
293
|
+
```javascript
|
|
294
|
+
const ws = new WebSocket('ws://localhost:3000/_debug/ws');
|
|
295
|
+
|
|
296
|
+
ws.onmessage = (event) => {
|
|
297
|
+
const data = JSON.parse(event.data);
|
|
298
|
+
console.log('New entry:', data);
|
|
299
|
+
};
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## 🔒 Security Considerations
|
|
303
|
+
|
|
304
|
+
**⚠️ Important**: NodeScope can expose sensitive application data. Always ensure proper security measures:
|
|
305
|
+
|
|
306
|
+
1. **Never enable in production** without authentication
|
|
307
|
+
2. **Use environment variables** to control when it's enabled
|
|
308
|
+
3. **Restrict access** to the dashboard endpoint using middleware
|
|
309
|
+
4. **Consider IP whitelisting** for production debugging
|
|
310
|
+
|
|
311
|
+
### Example: Protect Dashboard with Auth
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// Express example
|
|
315
|
+
app.use('/_debug', (req, res, next) => {
|
|
316
|
+
const auth = req.headers.authorization;
|
|
317
|
+
if (auth !== 'Bearer YOUR_SECRET_TOKEN') {
|
|
318
|
+
return res.status(401).send('Unauthorized');
|
|
319
|
+
}
|
|
320
|
+
next();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
app.use(nodescope.middleware());
|
|
324
|
+
nodescope.mountExpressRoutes(app);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## 📖 API Reference
|
|
328
|
+
|
|
329
|
+
### `NodeScope` Class
|
|
330
|
+
|
|
331
|
+
#### Constructor
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
new NodeScope(options?: NodeScopeOptions)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### Methods
|
|
338
|
+
|
|
339
|
+
- `middleware()` - Returns middleware function for your framework
|
|
340
|
+
- `record(type: string, data: any)` - Manually record an entry
|
|
341
|
+
- `getEntries(type?: string, limit?: number)` - Retrieve stored entries
|
|
342
|
+
- `clear()` - Clear all stored entries
|
|
343
|
+
- `mountExpressRoutes(app: Express)` - Mount Express routes (Express only)
|
|
344
|
+
|
|
345
|
+
### Framework Helpers
|
|
346
|
+
|
|
347
|
+
#### Express
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
import { NodeScope } from '@vipin733/nodescope';
|
|
351
|
+
const nodescope = new NodeScope(options);
|
|
352
|
+
app.use(nodescope.middleware());
|
|
353
|
+
nodescope.mountExpressRoutes(app);
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
#### Hono
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import { createHonoMiddleware } from '@vipin733/nodescope';
|
|
360
|
+
app.use('*', createHonoMiddleware(options));
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
#### Fastify
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
import { createFastifyPlugin } from '@vipin733/nodescope';
|
|
367
|
+
await fastify.register(createFastifyPlugin, options);
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## 🛠️ Development
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
# Clone the repository
|
|
374
|
+
git clone https://github.com/yourusername/nodescope.git
|
|
375
|
+
cd nodescope
|
|
376
|
+
|
|
377
|
+
# Install dependencies
|
|
378
|
+
npm install
|
|
379
|
+
|
|
380
|
+
# Build the package
|
|
381
|
+
npm run build
|
|
382
|
+
|
|
383
|
+
# Run tests
|
|
384
|
+
npm test
|
|
385
|
+
|
|
386
|
+
# Watch mode for development
|
|
387
|
+
npm run dev
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## 📝 Examples
|
|
391
|
+
|
|
392
|
+
Check out the [examples](https://github.com/yourusername/nodescope/tree/main/examples) directory for complete working examples:
|
|
393
|
+
|
|
394
|
+
- Express.js example
|
|
395
|
+
- Hono example
|
|
396
|
+
- Fastify example
|
|
397
|
+
|
|
398
|
+
## 🤝 Contributing
|
|
399
|
+
|
|
400
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
401
|
+
|
|
402
|
+
## 📄 License
|
|
403
|
+
|
|
404
|
+
MIT © [Your Name]
|
|
405
|
+
|
|
406
|
+
## 🙏 Acknowledgments
|
|
407
|
+
|
|
408
|
+
Inspired by [Laravel Telescope](https://laravel.com/docs/telescope) - the amazing debugging assistant for Laravel applications.
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
**Made with ❤️ for the Node.js and Bun communities**
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultEntryCounts
|
|
3
|
+
} from "./chunk-OF6NKXP5.js";
|
|
4
|
+
|
|
5
|
+
// src/storage/postgresql.ts
|
|
6
|
+
var PostgreSQLStorage = class {
|
|
7
|
+
pool;
|
|
8
|
+
connectionString;
|
|
9
|
+
constructor(connectionString) {
|
|
10
|
+
this.connectionString = connectionString;
|
|
11
|
+
}
|
|
12
|
+
async initialize() {
|
|
13
|
+
const { Pool } = await import("pg");
|
|
14
|
+
this.pool = new Pool({ connectionString: this.connectionString });
|
|
15
|
+
await this.pool.query(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS nodescope_entries (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
batch_id TEXT NOT NULL,
|
|
19
|
+
type TEXT NOT NULL,
|
|
20
|
+
content JSONB NOT NULL,
|
|
21
|
+
tags JSONB NOT NULL,
|
|
22
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
23
|
+
duration INTEGER,
|
|
24
|
+
memory_usage INTEGER
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_nodescope_entries_batch_id ON nodescope_entries(batch_id);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_nodescope_entries_type ON nodescope_entries(type);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_nodescope_entries_created_at ON nodescope_entries(created_at);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_nodescope_entries_tags ON nodescope_entries USING GIN(tags);
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
async save(entry) {
|
|
34
|
+
await this.pool.query(
|
|
35
|
+
`INSERT INTO nodescope_entries (id, batch_id, type, content, tags, created_at, duration, memory_usage)
|
|
36
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
37
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
38
|
+
content = EXCLUDED.content,
|
|
39
|
+
tags = EXCLUDED.tags,
|
|
40
|
+
duration = EXCLUDED.duration,
|
|
41
|
+
memory_usage = EXCLUDED.memory_usage`,
|
|
42
|
+
[
|
|
43
|
+
entry.id,
|
|
44
|
+
entry.batchId,
|
|
45
|
+
entry.type,
|
|
46
|
+
JSON.stringify(entry.content),
|
|
47
|
+
JSON.stringify(entry.tags),
|
|
48
|
+
entry.createdAt,
|
|
49
|
+
entry.duration ?? null,
|
|
50
|
+
entry.memoryUsage ?? null
|
|
51
|
+
]
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
async saveBatch(entries) {
|
|
55
|
+
const client = await this.pool.connect();
|
|
56
|
+
try {
|
|
57
|
+
await client.query("BEGIN");
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
await client.query(
|
|
60
|
+
`INSERT INTO nodescope_entries (id, batch_id, type, content, tags, created_at, duration, memory_usage)
|
|
61
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
62
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
63
|
+
content = EXCLUDED.content,
|
|
64
|
+
tags = EXCLUDED.tags,
|
|
65
|
+
duration = EXCLUDED.duration,
|
|
66
|
+
memory_usage = EXCLUDED.memory_usage`,
|
|
67
|
+
[
|
|
68
|
+
entry.id,
|
|
69
|
+
entry.batchId,
|
|
70
|
+
entry.type,
|
|
71
|
+
JSON.stringify(entry.content),
|
|
72
|
+
JSON.stringify(entry.tags),
|
|
73
|
+
entry.createdAt,
|
|
74
|
+
entry.duration ?? null,
|
|
75
|
+
entry.memoryUsage ?? null
|
|
76
|
+
]
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
await client.query("COMMIT");
|
|
80
|
+
} catch (error) {
|
|
81
|
+
await client.query("ROLLBACK");
|
|
82
|
+
throw error;
|
|
83
|
+
} finally {
|
|
84
|
+
client.release();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async find(id) {
|
|
88
|
+
const result = await this.pool.query(
|
|
89
|
+
"SELECT * FROM nodescope_entries WHERE id = $1",
|
|
90
|
+
[id]
|
|
91
|
+
);
|
|
92
|
+
if (result.rows.length === 0) return null;
|
|
93
|
+
return this.rowToEntry(result.rows[0]);
|
|
94
|
+
}
|
|
95
|
+
async list(options = {}) {
|
|
96
|
+
const { type, batchId, tags, search, before, after, limit = 50, offset = 0 } = options;
|
|
97
|
+
let where = "1=1";
|
|
98
|
+
const params = [];
|
|
99
|
+
let paramIndex = 1;
|
|
100
|
+
if (type) {
|
|
101
|
+
where += ` AND type = $${paramIndex++}`;
|
|
102
|
+
params.push(type);
|
|
103
|
+
}
|
|
104
|
+
if (batchId) {
|
|
105
|
+
where += ` AND batch_id = $${paramIndex++}`;
|
|
106
|
+
params.push(batchId);
|
|
107
|
+
}
|
|
108
|
+
if (tags && tags.length > 0) {
|
|
109
|
+
where += ` AND tags ?| $${paramIndex++}`;
|
|
110
|
+
params.push(tags);
|
|
111
|
+
}
|
|
112
|
+
if (before) {
|
|
113
|
+
where += ` AND created_at < $${paramIndex++}`;
|
|
114
|
+
params.push(before);
|
|
115
|
+
}
|
|
116
|
+
if (after) {
|
|
117
|
+
where += ` AND created_at > $${paramIndex++}`;
|
|
118
|
+
params.push(after);
|
|
119
|
+
}
|
|
120
|
+
if (search) {
|
|
121
|
+
where += ` AND content::text ILIKE $${paramIndex++}`;
|
|
122
|
+
params.push(`%${search}%`);
|
|
123
|
+
}
|
|
124
|
+
const countResult = await this.pool.query(
|
|
125
|
+
`SELECT COUNT(*) as count FROM nodescope_entries WHERE ${where}`,
|
|
126
|
+
params
|
|
127
|
+
);
|
|
128
|
+
const total = parseInt(countResult.rows[0].count, 10);
|
|
129
|
+
const dataResult = await this.pool.query(
|
|
130
|
+
`SELECT * FROM nodescope_entries WHERE ${where} ORDER BY created_at DESC LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
|
|
131
|
+
[...params, limit, offset]
|
|
132
|
+
);
|
|
133
|
+
return {
|
|
134
|
+
data: dataResult.rows.map((row) => this.rowToEntry(row)),
|
|
135
|
+
total,
|
|
136
|
+
limit,
|
|
137
|
+
offset,
|
|
138
|
+
hasMore: offset + limit < total
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async findByBatch(batchId) {
|
|
142
|
+
const result = await this.pool.query(
|
|
143
|
+
"SELECT * FROM nodescope_entries WHERE batch_id = $1 ORDER BY created_at ASC",
|
|
144
|
+
[batchId]
|
|
145
|
+
);
|
|
146
|
+
return result.rows.map((row) => this.rowToEntry(row));
|
|
147
|
+
}
|
|
148
|
+
async prune(beforeDate) {
|
|
149
|
+
const result = await this.pool.query(
|
|
150
|
+
"DELETE FROM nodescope_entries WHERE created_at < $1",
|
|
151
|
+
[beforeDate]
|
|
152
|
+
);
|
|
153
|
+
return result.rowCount ?? 0;
|
|
154
|
+
}
|
|
155
|
+
async clear() {
|
|
156
|
+
await this.pool.query("DELETE FROM nodescope_entries");
|
|
157
|
+
}
|
|
158
|
+
async stats() {
|
|
159
|
+
const entriesByType = defaultEntryCounts();
|
|
160
|
+
const typeResult = await this.pool.query(
|
|
161
|
+
"SELECT type, COUNT(*) as count FROM nodescope_entries GROUP BY type"
|
|
162
|
+
);
|
|
163
|
+
for (const row of typeResult.rows) {
|
|
164
|
+
entriesByType[row.type] = parseInt(row.count, 10);
|
|
165
|
+
}
|
|
166
|
+
const totalResult = await this.pool.query(
|
|
167
|
+
"SELECT COUNT(*) as count FROM nodescope_entries"
|
|
168
|
+
);
|
|
169
|
+
const rangeResult = await this.pool.query(
|
|
170
|
+
"SELECT MIN(created_at) as oldest, MAX(created_at) as newest FROM nodescope_entries"
|
|
171
|
+
);
|
|
172
|
+
return {
|
|
173
|
+
totalEntries: parseInt(totalResult.rows[0].count, 10),
|
|
174
|
+
entriesByType,
|
|
175
|
+
oldestEntry: rangeResult.rows[0].oldest ? new Date(rangeResult.rows[0].oldest) : void 0,
|
|
176
|
+
newestEntry: rangeResult.rows[0].newest ? new Date(rangeResult.rows[0].newest) : void 0
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async close() {
|
|
180
|
+
await this.pool.end();
|
|
181
|
+
}
|
|
182
|
+
rowToEntry(row) {
|
|
183
|
+
return {
|
|
184
|
+
id: row.id,
|
|
185
|
+
batchId: row.batch_id,
|
|
186
|
+
type: row.type,
|
|
187
|
+
content: typeof row.content === "string" ? JSON.parse(row.content) : row.content,
|
|
188
|
+
tags: typeof row.tags === "string" ? JSON.parse(row.tags) : row.tags,
|
|
189
|
+
createdAt: new Date(row.created_at),
|
|
190
|
+
duration: row.duration ?? void 0,
|
|
191
|
+
memoryUsage: row.memory_usage ?? void 0
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export {
|
|
197
|
+
PostgreSQLStorage
|
|
198
|
+
};
|