@ngn-net/nestjs-telescope 0.1.6 → 0.1.7
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 +42 -34
- package/dist/constants.d.ts +12 -3
- package/dist/constants.js +14 -5
- package/dist/controllers/telescope.controller.d.ts +5 -2
- package/dist/controllers/telescope.controller.js +41 -13
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/interfaces/telescope-options.interface.d.ts +9 -3
- package/dist/storage/entities/telescope-entry.entity.js +34 -7
- package/dist/storage/telescope-repository.service.d.ts +17 -7
- package/dist/storage/telescope-repository.service.js +42 -17
- package/dist/telescope.module.d.ts +6 -2
- package/dist/telescope.module.js +148 -44
- package/dist/telescope.service.d.ts +21 -1
- package/dist/telescope.service.js +69 -23
- package/dist/ui/index.html +1635 -0
- package/dist/watchers/cache.watcher.d.ts +5 -4
- package/dist/watchers/cache.watcher.js +50 -34
- package/dist/watchers/event.watcher.d.ts +7 -3
- package/dist/watchers/event.watcher.js +22 -6
- package/dist/watchers/exception.watcher.js +7 -1
- package/dist/watchers/http-request.watcher.d.ts +3 -1
- package/dist/watchers/http-request.watcher.js +25 -8
- package/dist/watchers/log.watcher.d.ts +1 -0
- package/dist/watchers/log.watcher.js +22 -17
- package/dist/watchers/mail.watcher.d.ts +1 -1
- package/dist/watchers/mail.watcher.js +28 -56
- package/dist/watchers/query.watcher.d.ts +6 -3
- package/dist/watchers/query.watcher.js +36 -8
- package/dist/watchers/queue.watcher.d.ts +2 -4
- package/dist/watchers/queue.watcher.js +38 -32
- package/dist/watchers/redis.watcher.d.ts +3 -1
- package/dist/watchers/redis.watcher.js +16 -4
- package/dist/watchers/schedule.watcher.d.ts +8 -3
- package/dist/watchers/schedule.watcher.js +25 -11
- package/package.json +51 -19
- package/ui/index.html +602 -235
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ All telemetry records are persisted to a database table and accessible via a mod
|
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
25
25
|
|
|
26
|
-
Install the package
|
|
26
|
+
Install the package:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
npm install @ngn-net/nestjs-telescope @nestjs/typeorm typeorm @nestjs/jwt @nestjs/passport passport-jwt
|
|
@@ -33,6 +33,38 @@ Ensure you have TypeORM initialized in your NestJS application.
|
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
|
+
## Optional & Peer Dependencies
|
|
37
|
+
|
|
38
|
+
All watchers are dynamically activated. If your application does not use certain modules, Telescope will safely boot without them. Simply install the peer dependencies for the watchers you want to enable:
|
|
39
|
+
|
|
40
|
+
| Watcher | Required Dependency | Enable Entry Type |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| **Cache Watcher** | `@nestjs/cache-manager` & `cache-manager` | `EntryType.CACHE` |
|
|
43
|
+
| **Queue Watcher** | `@nestjs/bullmq` & `bullmq` | `EntryType.JOB` |
|
|
44
|
+
| **Event Watcher** | `@nestjs/event-emitter` | `EntryType.EVENT` |
|
|
45
|
+
| **Schedule Watcher** | `@nestjs/schedule` | `EntryType.SCHEDULED_TASK` |
|
|
46
|
+
| **Mail Watcher** | `nodemailer` & `@types/nodemailer` | `EntryType.MAIL` |
|
|
47
|
+
| **Redis Watcher** | `ioredis` | `EntryType.REDIS` |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Configuration Reference
|
|
52
|
+
|
|
53
|
+
You can pass configuration options into `TelescopeModule.forRoot(options)`:
|
|
54
|
+
|
|
55
|
+
| Option | Type | Default | Description |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `path` | `string` | `'telescope'` | Base path for the UI and API. |
|
|
58
|
+
| `jwtSecret` | `string` | `'telescope-change-me'` | JWT secret used to sign UI authentication tokens. |
|
|
59
|
+
| `password` | `string` | `'password'` | Password for dashboard login (can also be set via `TELESCOPE_PASSWORD` env). |
|
|
60
|
+
| `maxEntries` | `number` | `1000` | Maximum number of entries to retain in the database before pruning. |
|
|
61
|
+
| `enabledEntryTypes` | `EntryType[]` | `all` | Specific entry types to record. If undefined, all types are recorded. |
|
|
62
|
+
| `ignorePaths` | `string[]` | `[]` | Specific request path prefixes to ignore (e.g. `['/health', '/metrics']`). |
|
|
63
|
+
| `ignoreCommands` | `string[]` | `[]` | Specific Redis command names to ignore (e.g. `['ping', 'info']`). |
|
|
64
|
+
| `uiMiddleware` | `any[]` | `[]` | Custom middleware to apply to Telescope routes. |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
36
68
|
## Usage
|
|
37
69
|
|
|
38
70
|
Import the `TelescopeModule` in your root `AppModule`:
|
|
@@ -45,54 +77,29 @@ import { TelescopeModule, EntryType } from '@ngn-net/nestjs-telescope';
|
|
|
45
77
|
@Module({
|
|
46
78
|
imports: [
|
|
47
79
|
TypeOrmModule.forRoot({
|
|
48
|
-
type: '
|
|
49
|
-
|
|
50
|
-
port: 5432,
|
|
51
|
-
username: 'db_user',
|
|
52
|
-
password: 'db_password',
|
|
53
|
-
database: 'app_db',
|
|
80
|
+
type: 'sqlite', // or postgres, mysql, etc.
|
|
81
|
+
database: 'app.db',
|
|
54
82
|
autoLoadEntities: true,
|
|
55
|
-
synchronize: true, // Telescope
|
|
83
|
+
synchronize: true, // Telescope automatically registers its own entity
|
|
56
84
|
}),
|
|
57
85
|
TelescopeModule.forRoot({
|
|
58
86
|
path: 'telescope', // Access at /telescope
|
|
59
|
-
|
|
60
|
-
maxEntries:
|
|
61
|
-
|
|
62
|
-
EntryType.REQUEST,
|
|
63
|
-
EntryType.QUERY,
|
|
64
|
-
EntryType.CACHE,
|
|
65
|
-
EntryType.JOB,
|
|
66
|
-
EntryType.EVENT,
|
|
67
|
-
EntryType.LOG,
|
|
68
|
-
EntryType.EXCEPTION,
|
|
69
|
-
EntryType.SCHEDULED_TASK,
|
|
70
|
-
EntryType.REDIS,
|
|
71
|
-
EntryType.MAIL,
|
|
72
|
-
],
|
|
87
|
+
password: 'my-secure-password',
|
|
88
|
+
maxEntries: 500,
|
|
89
|
+
ignorePaths: ['/health-check'],
|
|
73
90
|
}),
|
|
74
91
|
],
|
|
75
92
|
})
|
|
76
93
|
export class AppModule {}
|
|
77
94
|
```
|
|
78
95
|
|
|
79
|
-
### Authentication Configuration
|
|
80
|
-
|
|
81
|
-
To secure the Telescope dashboard, configure the dashboard access password in your environment variables:
|
|
82
|
-
|
|
83
|
-
```env
|
|
84
|
-
TELESCOPE_PASSWORD=my-secure-password
|
|
85
|
-
TELESCOPE_JWT_SECRET=super-secret-token
|
|
86
|
-
```
|
|
87
|
-
*If not set, the password defaults to `password`.*
|
|
88
|
-
|
|
89
96
|
---
|
|
90
97
|
|
|
91
98
|
## Dashboard Access
|
|
92
99
|
|
|
93
100
|
1. Run your NestJS application.
|
|
94
|
-
2. Open your browser and navigate to `http://localhost:3000/telescope
|
|
95
|
-
3. Enter the password
|
|
101
|
+
2. Open your browser and navigate to `http://localhost:3000/telescope` (or your configured path).
|
|
102
|
+
3. Enter the configured password (or defaults to `password`).
|
|
96
103
|
4. Monitor requests, logs, queries, and errors in real-time.
|
|
97
104
|
|
|
98
105
|
---
|
|
@@ -108,3 +115,4 @@ To run Jest tests:
|
|
|
108
115
|
```bash
|
|
109
116
|
npm run test
|
|
110
117
|
```
|
|
118
|
+
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const
|
|
3
|
-
|
|
1
|
+
/** Injection token for telescope configuration options */
|
|
2
|
+
export declare const TELESCOPE_OPTIONS = "TELESCOPE_OPTIONS";
|
|
3
|
+
/** Default base path for the Telescope UI & API */
|
|
4
|
+
export declare const DEFAULT_TELESCOPE_PATH = "telescope";
|
|
5
|
+
/** Default JWT secret (should always be overridden in production) */
|
|
6
|
+
export declare const DEFAULT_JWT_SECRET = "telescope-change-me";
|
|
7
|
+
/** Default maximum number of entries to retain */
|
|
8
|
+
export declare const DEFAULT_MAX_ENTRIES = 1000;
|
|
9
|
+
/** Default password for UI authentication */
|
|
10
|
+
export declare const DEFAULT_PASSWORD = "password";
|
|
11
|
+
/** Minimum interval (ms) between prune operations */
|
|
12
|
+
export declare const PRUNE_THROTTLE_MS = 60000;
|
package/dist/constants.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TELESCOPE_DB_CONNECTION = exports.TELESCOPE_JWT_SECRET = exports.TELESCOPE_PATH = void 0;
|
|
4
2
|
// src/constants.ts
|
|
5
|
-
exports
|
|
6
|
-
exports.
|
|
7
|
-
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.PRUNE_THROTTLE_MS = exports.DEFAULT_PASSWORD = exports.DEFAULT_MAX_ENTRIES = exports.DEFAULT_JWT_SECRET = exports.DEFAULT_TELESCOPE_PATH = exports.TELESCOPE_OPTIONS = void 0;
|
|
5
|
+
/** Injection token for telescope configuration options */
|
|
6
|
+
exports.TELESCOPE_OPTIONS = 'TELESCOPE_OPTIONS';
|
|
7
|
+
/** Default base path for the Telescope UI & API */
|
|
8
|
+
exports.DEFAULT_TELESCOPE_PATH = 'telescope';
|
|
9
|
+
/** Default JWT secret (should always be overridden in production) */
|
|
10
|
+
exports.DEFAULT_JWT_SECRET = 'telescope-change-me';
|
|
11
|
+
/** Default maximum number of entries to retain */
|
|
12
|
+
exports.DEFAULT_MAX_ENTRIES = 1000;
|
|
13
|
+
/** Default password for UI authentication */
|
|
14
|
+
exports.DEFAULT_PASSWORD = 'password';
|
|
15
|
+
/** Minimum interval (ms) between prune operations */
|
|
16
|
+
exports.PRUNE_THROTTLE_MS = 60_000;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { TelescopeRepository } from '../storage/telescope-repository.service';
|
|
2
2
|
import { JwtService } from '@nestjs/jwt';
|
|
3
|
+
import { TelescopeOptions } from '../interfaces/telescope-options.interface';
|
|
3
4
|
import { Request, Response } from 'express';
|
|
4
5
|
export declare class TelescopeController {
|
|
5
6
|
private readonly repo;
|
|
6
7
|
private readonly jwtService;
|
|
7
|
-
|
|
8
|
+
private readonly options;
|
|
9
|
+
constructor(repo: TelescopeRepository, jwtService: JwtService, options: TelescopeOptions);
|
|
8
10
|
login(body: any): Promise<{
|
|
9
11
|
token: string;
|
|
10
12
|
}>;
|
|
11
|
-
|
|
13
|
+
getStats(): Promise<Record<string, number>>;
|
|
14
|
+
getEntries(type?: string, search?: string, page?: string, perPage?: string, limit?: string): Promise<import("../storage/telescope-repository.service").PaginatedResult>;
|
|
12
15
|
getEntry(uuid: string): Promise<import("..").TelescopeEntry>;
|
|
13
16
|
clearEntries(): Promise<{
|
|
14
17
|
success: boolean;
|
|
@@ -24,12 +24,14 @@ const fs_1 = require("fs");
|
|
|
24
24
|
let TelescopeController = class TelescopeController {
|
|
25
25
|
repo;
|
|
26
26
|
jwtService;
|
|
27
|
-
|
|
27
|
+
options;
|
|
28
|
+
constructor(repo, jwtService, options) {
|
|
28
29
|
this.repo = repo;
|
|
29
30
|
this.jwtService = jwtService;
|
|
31
|
+
this.options = options;
|
|
30
32
|
}
|
|
31
33
|
async login(body) {
|
|
32
|
-
const password = process.env.TELESCOPE_PASSWORD || 'password';
|
|
34
|
+
const password = this.options?.password || process.env.TELESCOPE_PASSWORD || 'password';
|
|
33
35
|
if (body.password === password) {
|
|
34
36
|
const payload = { username: 'telescope' };
|
|
35
37
|
return {
|
|
@@ -38,9 +40,19 @@ let TelescopeController = class TelescopeController {
|
|
|
38
40
|
}
|
|
39
41
|
throw new common_1.UnauthorizedException('Invalid credentials');
|
|
40
42
|
}
|
|
41
|
-
async
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
async getStats() {
|
|
44
|
+
return await this.repo.countByType();
|
|
45
|
+
}
|
|
46
|
+
async getEntries(type, search, page, perPage, limit) {
|
|
47
|
+
const pageNum = page ? parseInt(page, 10) : 1;
|
|
48
|
+
const limitNum = limit ? parseInt(limit, 10) : undefined;
|
|
49
|
+
const perPageNum = perPage ? parseInt(perPage, 10) : (limitNum || 50);
|
|
50
|
+
return await this.repo.queryEntries({
|
|
51
|
+
type,
|
|
52
|
+
search,
|
|
53
|
+
page: pageNum,
|
|
54
|
+
limit: perPageNum,
|
|
55
|
+
});
|
|
44
56
|
}
|
|
45
57
|
async getEntry(uuid) {
|
|
46
58
|
const entry = await this.repo.findByUuid(uuid);
|
|
@@ -56,9 +68,10 @@ let TelescopeController = class TelescopeController {
|
|
|
56
68
|
// Fallback wildcard to serve static assets and UI dashboard
|
|
57
69
|
serveStatic(req, res) {
|
|
58
70
|
const rawPath = req.path;
|
|
59
|
-
|
|
71
|
+
const basePath = this.options?.path || constants_1.DEFAULT_TELESCOPE_PATH;
|
|
72
|
+
// Strip basePath prefix to find the asset inside the ui directory
|
|
60
73
|
// e.g. /telescope/assets/main.js -> /assets/main.js
|
|
61
|
-
let assetPath = rawPath.replace(new RegExp(`^/${
|
|
74
|
+
let assetPath = rawPath.replace(new RegExp(`^/${basePath}`), '');
|
|
62
75
|
if (assetPath.startsWith('/')) {
|
|
63
76
|
assetPath = assetPath.substring(1);
|
|
64
77
|
}
|
|
@@ -66,12 +79,17 @@ let TelescopeController = class TelescopeController {
|
|
|
66
79
|
if (!assetPath || !assetPath.includes('.')) {
|
|
67
80
|
assetPath = 'index.html';
|
|
68
81
|
}
|
|
69
|
-
|
|
82
|
+
// Locate UI directory in either source (dev) or dist (prod) mode
|
|
83
|
+
let uiDir = (0, path_1.join)(__dirname, '../../ui');
|
|
84
|
+
if (!(0, fs_1.existsSync)((0, path_1.join)(uiDir, 'index.html'))) {
|
|
85
|
+
uiDir = (0, path_1.join)(__dirname, '../ui');
|
|
86
|
+
}
|
|
87
|
+
const filePath = (0, path_1.join)(uiDir, assetPath);
|
|
70
88
|
if ((0, fs_1.existsSync)(filePath)) {
|
|
71
89
|
return res.sendFile(filePath);
|
|
72
90
|
}
|
|
73
91
|
// Fall back to index.html
|
|
74
|
-
const fallbackPath = (0, path_1.join)(
|
|
92
|
+
const fallbackPath = (0, path_1.join)(uiDir, 'index.html');
|
|
75
93
|
if ((0, fs_1.existsSync)(fallbackPath)) {
|
|
76
94
|
return res.sendFile(fallbackPath);
|
|
77
95
|
}
|
|
@@ -86,14 +104,23 @@ __decorate([
|
|
|
86
104
|
__metadata("design:paramtypes", [Object]),
|
|
87
105
|
__metadata("design:returntype", Promise)
|
|
88
106
|
], TelescopeController.prototype, "login", null);
|
|
107
|
+
__decorate([
|
|
108
|
+
(0, common_1.UseGuards)(telescope_jwt_guard_1.JwtAuthGuard),
|
|
109
|
+
(0, common_1.Get)('api/stats'),
|
|
110
|
+
__metadata("design:type", Function),
|
|
111
|
+
__metadata("design:paramtypes", []),
|
|
112
|
+
__metadata("design:returntype", Promise)
|
|
113
|
+
], TelescopeController.prototype, "getStats", null);
|
|
89
114
|
__decorate([
|
|
90
115
|
(0, common_1.UseGuards)(telescope_jwt_guard_1.JwtAuthGuard),
|
|
91
116
|
(0, common_1.Get)('api/entries'),
|
|
92
117
|
__param(0, (0, common_1.Query)('type')),
|
|
93
118
|
__param(1, (0, common_1.Query)('search')),
|
|
94
|
-
__param(2, (0, common_1.Query)('
|
|
119
|
+
__param(2, (0, common_1.Query)('page')),
|
|
120
|
+
__param(3, (0, common_1.Query)('perPage')),
|
|
121
|
+
__param(4, (0, common_1.Query)('limit')),
|
|
95
122
|
__metadata("design:type", Function),
|
|
96
|
-
__metadata("design:paramtypes", [String, String, String]),
|
|
123
|
+
__metadata("design:paramtypes", [String, String, String, String, String]),
|
|
97
124
|
__metadata("design:returntype", Promise)
|
|
98
125
|
], TelescopeController.prototype, "getEntries", null);
|
|
99
126
|
__decorate([
|
|
@@ -120,7 +147,8 @@ __decorate([
|
|
|
120
147
|
__metadata("design:returntype", void 0)
|
|
121
148
|
], TelescopeController.prototype, "serveStatic", null);
|
|
122
149
|
exports.TelescopeController = TelescopeController = __decorate([
|
|
123
|
-
(0, common_1.Controller)(
|
|
150
|
+
(0, common_1.Controller)(),
|
|
151
|
+
__param(2, (0, common_1.Inject)(constants_1.TELESCOPE_OPTIONS)),
|
|
124
152
|
__metadata("design:paramtypes", [telescope_repository_service_1.TelescopeRepository,
|
|
125
|
-
jwt_1.JwtService])
|
|
153
|
+
jwt_1.JwtService, Object])
|
|
126
154
|
], TelescopeController);
|
package/dist/index.d.ts
CHANGED
|
@@ -6,3 +6,14 @@ export * from './enums/entry-type.enum';
|
|
|
6
6
|
export * from './interfaces/telescope-options.interface';
|
|
7
7
|
export * from './interfaces/entry.interface';
|
|
8
8
|
export * from './guards/telescope-jwt.guard';
|
|
9
|
+
export * from './constants';
|
|
10
|
+
export * from './watchers/http-request.watcher';
|
|
11
|
+
export * from './watchers/query.watcher';
|
|
12
|
+
export * from './watchers/cache.watcher';
|
|
13
|
+
export * from './watchers/queue.watcher';
|
|
14
|
+
export * from './watchers/event.watcher';
|
|
15
|
+
export * from './watchers/mail.watcher';
|
|
16
|
+
export * from './watchers/log.watcher';
|
|
17
|
+
export * from './watchers/exception.watcher';
|
|
18
|
+
export * from './watchers/schedule.watcher';
|
|
19
|
+
export * from './watchers/redis.watcher';
|
package/dist/index.js
CHANGED
|
@@ -23,3 +23,15 @@ __exportStar(require("./enums/entry-type.enum"), exports);
|
|
|
23
23
|
__exportStar(require("./interfaces/telescope-options.interface"), exports);
|
|
24
24
|
__exportStar(require("./interfaces/entry.interface"), exports);
|
|
25
25
|
__exportStar(require("./guards/telescope-jwt.guard"), exports);
|
|
26
|
+
__exportStar(require("./constants"), exports);
|
|
27
|
+
// Export all watchers for custom usage/extension
|
|
28
|
+
__exportStar(require("./watchers/http-request.watcher"), exports);
|
|
29
|
+
__exportStar(require("./watchers/query.watcher"), exports);
|
|
30
|
+
__exportStar(require("./watchers/cache.watcher"), exports);
|
|
31
|
+
__exportStar(require("./watchers/queue.watcher"), exports);
|
|
32
|
+
__exportStar(require("./watchers/event.watcher"), exports);
|
|
33
|
+
__exportStar(require("./watchers/mail.watcher"), exports);
|
|
34
|
+
__exportStar(require("./watchers/log.watcher"), exports);
|
|
35
|
+
__exportStar(require("./watchers/exception.watcher"), exports);
|
|
36
|
+
__exportStar(require("./watchers/schedule.watcher"), exports);
|
|
37
|
+
__exportStar(require("./watchers/redis.watcher"), exports);
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { EntryType } from '../enums/entry-type.enum';
|
|
2
2
|
export interface TelescopeOptions {
|
|
3
|
-
/** Base path for UI (default:
|
|
3
|
+
/** Base path for UI and API (default: 'telescope') */
|
|
4
4
|
path?: string;
|
|
5
5
|
/** JWT secret used for UI authentication */
|
|
6
6
|
jwtSecret?: string;
|
|
7
|
-
/**
|
|
7
|
+
/** Password for dashboard login (default: 'password', or env TELESCOPE_PASSWORD) */
|
|
8
|
+
password?: string;
|
|
9
|
+
/** Which entry types to enable (default: all) */
|
|
8
10
|
enabledEntryTypes?: EntryType[];
|
|
9
|
-
/** Max number of entries to retain (
|
|
11
|
+
/** Max number of entries to retain in the database (default: 1000) */
|
|
10
12
|
maxEntries?: number;
|
|
13
|
+
/** Request paths to ignore (e.g., ['/health', '/telescope']) */
|
|
14
|
+
ignorePaths?: string[];
|
|
15
|
+
/** Redis commands to ignore (e.g., ['info', 'ping']) */
|
|
16
|
+
ignoreCommands?: string[];
|
|
11
17
|
/** Additional middleware to apply to UI routes */
|
|
12
18
|
uiMiddleware?: any[];
|
|
13
19
|
}
|
|
@@ -13,6 +13,30 @@ exports.TelescopeEntry = void 0;
|
|
|
13
13
|
// src/storage/entities/telescope-entry.entity.ts
|
|
14
14
|
const typeorm_1 = require("typeorm");
|
|
15
15
|
const entry_type_enum_1 = require("../../enums/entry-type.enum");
|
|
16
|
+
/**
|
|
17
|
+
* Custom ValueTransformer that serializes objects to JSON strings and back.
|
|
18
|
+
* This allows the `content` column to work on all databases (SQLite, MySQL, PostgreSQL)
|
|
19
|
+
* by storing JSON as plain text instead of requiring a `jsonb` column type.
|
|
20
|
+
*/
|
|
21
|
+
const JsonTransformer = {
|
|
22
|
+
to(value) {
|
|
23
|
+
if (value === null || value === undefined)
|
|
24
|
+
return '{}';
|
|
25
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
26
|
+
},
|
|
27
|
+
from(value) {
|
|
28
|
+
if (value === null || value === undefined)
|
|
29
|
+
return {};
|
|
30
|
+
if (typeof value === 'object')
|
|
31
|
+
return value; // Already parsed (some drivers do this)
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(value);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
16
40
|
let TelescopeEntry = class TelescopeEntry {
|
|
17
41
|
id;
|
|
18
42
|
uuid;
|
|
@@ -28,29 +52,32 @@ __decorate([
|
|
|
28
52
|
__metadata("design:type", Number)
|
|
29
53
|
], TelescopeEntry.prototype, "id", void 0);
|
|
30
54
|
__decorate([
|
|
31
|
-
(0, typeorm_1.Column)({ type: '
|
|
55
|
+
(0, typeorm_1.Column)({ type: 'varchar', length: 64 }),
|
|
56
|
+
(0, typeorm_1.Index)({ unique: true }),
|
|
32
57
|
__metadata("design:type", String)
|
|
33
58
|
], TelescopeEntry.prototype, "uuid", void 0);
|
|
34
59
|
__decorate([
|
|
35
|
-
(0, typeorm_1.Column)({ nullable: true }),
|
|
60
|
+
(0, typeorm_1.Column)({ type: 'varchar', length: 64, nullable: true }),
|
|
36
61
|
__metadata("design:type", String)
|
|
37
62
|
], TelescopeEntry.prototype, "batchId", void 0);
|
|
38
63
|
__decorate([
|
|
39
|
-
(0, typeorm_1.Column)({ type: '
|
|
64
|
+
(0, typeorm_1.Column)({ type: 'varchar', length: 50 }),
|
|
40
65
|
__metadata("design:type", String)
|
|
41
66
|
], TelescopeEntry.prototype, "type", void 0);
|
|
42
67
|
__decorate([
|
|
43
|
-
(0, typeorm_1.Column)({ type: '
|
|
68
|
+
(0, typeorm_1.Column)({ type: 'text', transformer: JsonTransformer }),
|
|
44
69
|
__metadata("design:type", Object)
|
|
45
70
|
], TelescopeEntry.prototype, "content", void 0);
|
|
46
71
|
__decorate([
|
|
47
|
-
(0, typeorm_1.CreateDateColumn)(
|
|
72
|
+
(0, typeorm_1.CreateDateColumn)(),
|
|
48
73
|
__metadata("design:type", Date)
|
|
49
74
|
], TelescopeEntry.prototype, "recordedAt", void 0);
|
|
50
75
|
__decorate([
|
|
51
|
-
(0, typeorm_1.Column)({ nullable: true }),
|
|
76
|
+
(0, typeorm_1.Column)({ type: 'varchar', length: 64, nullable: true }),
|
|
52
77
|
__metadata("design:type", String)
|
|
53
78
|
], TelescopeEntry.prototype, "familyHash", void 0);
|
|
54
79
|
exports.TelescopeEntry = TelescopeEntry = __decorate([
|
|
55
|
-
(0, typeorm_1.Entity)({ name: 'telescope_entries' })
|
|
80
|
+
(0, typeorm_1.Entity)({ name: 'telescope_entries' }),
|
|
81
|
+
(0, typeorm_1.Index)(['type']),
|
|
82
|
+
(0, typeorm_1.Index)(['recordedAt'])
|
|
56
83
|
], TelescopeEntry);
|
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
import { Repository
|
|
1
|
+
import { Repository } from 'typeorm';
|
|
2
2
|
import { TelescopeEntry } from './entities/telescope-entry.entity';
|
|
3
3
|
import { EntryType } from '../enums/entry-type.enum';
|
|
4
|
+
export interface QueryFilters {
|
|
5
|
+
type?: string;
|
|
6
|
+
search?: string;
|
|
7
|
+
limit?: number;
|
|
8
|
+
page?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface PaginatedResult {
|
|
11
|
+
data: TelescopeEntry[];
|
|
12
|
+
total: number;
|
|
13
|
+
page: number;
|
|
14
|
+
perPage: number;
|
|
15
|
+
totalPages: number;
|
|
16
|
+
}
|
|
4
17
|
export declare class TelescopeRepository {
|
|
5
18
|
private readonly entryRepo;
|
|
6
19
|
constructor(entryRepo: Repository<TelescopeEntry>);
|
|
@@ -8,11 +21,8 @@ export declare class TelescopeRepository {
|
|
|
8
21
|
findAll(limit?: number): Promise<TelescopeEntry[]>;
|
|
9
22
|
findByType(type: EntryType, limit?: number): Promise<TelescopeEntry[]>;
|
|
10
23
|
prune(maxEntries: number): Promise<void>;
|
|
11
|
-
queryEntries(filters:
|
|
12
|
-
type?: string;
|
|
13
|
-
search?: string;
|
|
14
|
-
limit?: number;
|
|
15
|
-
}): Promise<TelescopeEntry[]>;
|
|
24
|
+
queryEntries(filters: QueryFilters): Promise<PaginatedResult>;
|
|
16
25
|
findByUuid(uuid: string): Promise<TelescopeEntry | null>;
|
|
17
|
-
clearAll(): Promise<DeleteResult>;
|
|
26
|
+
clearAll(): Promise<import("typeorm").DeleteResult>;
|
|
27
|
+
countByType(): Promise<Record<string, number>>;
|
|
18
28
|
}
|
|
@@ -38,29 +38,41 @@ let TelescopeRepository = class TelescopeRepository {
|
|
|
38
38
|
if (count <= maxEntries)
|
|
39
39
|
return;
|
|
40
40
|
const excess = count - maxEntries;
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
41
|
+
// Use a single query to find and delete the oldest entries
|
|
42
|
+
const oldest = await this.entryRepo.find({
|
|
43
|
+
order: { recordedAt: 'ASC' },
|
|
44
|
+
take: excess,
|
|
45
|
+
select: ['id'],
|
|
46
|
+
});
|
|
47
|
+
if (oldest.length > 0) {
|
|
48
|
+
const ids = oldest.map((e) => e.id);
|
|
49
|
+
await this.entryRepo.delete(ids);
|
|
50
|
+
}
|
|
44
51
|
}
|
|
45
52
|
async queryEntries(filters) {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
const perPage = Math.min(filters.limit || 50, 200);
|
|
54
|
+
const page = Math.max(filters.page || 1, 1);
|
|
55
|
+
const skip = (page - 1) * perPage;
|
|
56
|
+
const qb = this.entryRepo.createQueryBuilder('entry');
|
|
57
|
+
qb.orderBy('entry.recordedAt', 'DESC');
|
|
51
58
|
if (filters.type) {
|
|
52
|
-
|
|
59
|
+
qb.andWhere('entry.type = :type', { type: filters.type });
|
|
53
60
|
}
|
|
54
|
-
|
|
61
|
+
// For search, do in-memory filtering since LIKE on JSON text varies per DB
|
|
55
62
|
if (filters.search) {
|
|
56
|
-
const searchLower = filters.search.toLowerCase()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const contentMatch = JSON.stringify(entry.content).toLowerCase().includes(searchLower);
|
|
60
|
-
return uuidMatch || contentMatch;
|
|
61
|
-
});
|
|
63
|
+
const searchLower = `%${filters.search.toLowerCase()}%`;
|
|
64
|
+
// Try database-level search first (works on most DBs)
|
|
65
|
+
qb.andWhere('(LOWER(entry.uuid) LIKE :search OR LOWER(entry.content) LIKE :search)', { search: searchLower });
|
|
62
66
|
}
|
|
63
|
-
|
|
67
|
+
const total = await qb.getCount();
|
|
68
|
+
const data = await qb.skip(skip).take(perPage).getMany();
|
|
69
|
+
return {
|
|
70
|
+
data,
|
|
71
|
+
total,
|
|
72
|
+
page,
|
|
73
|
+
perPage,
|
|
74
|
+
totalPages: Math.ceil(total / perPage),
|
|
75
|
+
};
|
|
64
76
|
}
|
|
65
77
|
async findByUuid(uuid) {
|
|
66
78
|
return await this.entryRepo.findOne({ where: { uuid } });
|
|
@@ -68,6 +80,19 @@ let TelescopeRepository = class TelescopeRepository {
|
|
|
68
80
|
async clearAll() {
|
|
69
81
|
return await this.entryRepo.delete({});
|
|
70
82
|
}
|
|
83
|
+
async countByType() {
|
|
84
|
+
const results = await this.entryRepo
|
|
85
|
+
.createQueryBuilder('entry')
|
|
86
|
+
.select('entry.type', 'type')
|
|
87
|
+
.addSelect('COUNT(*)', 'count')
|
|
88
|
+
.groupBy('entry.type')
|
|
89
|
+
.getRawMany();
|
|
90
|
+
const counts = {};
|
|
91
|
+
for (const row of results) {
|
|
92
|
+
counts[row.type] = parseInt(row.count, 10);
|
|
93
|
+
}
|
|
94
|
+
return counts;
|
|
95
|
+
}
|
|
71
96
|
};
|
|
72
97
|
exports.TelescopeRepository = TelescopeRepository;
|
|
73
98
|
exports.TelescopeRepository = TelescopeRepository = __decorate([
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { DynamicModule } from '@nestjs/common';
|
|
2
|
-
|
|
1
|
+
import { DynamicModule, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { ModuleRef } from '@nestjs/core';
|
|
3
|
+
export declare class TelescopeModule implements OnModuleInit {
|
|
4
|
+
private readonly moduleRef;
|
|
5
|
+
constructor(moduleRef: ModuleRef);
|
|
6
|
+
onModuleInit(): void;
|
|
3
7
|
static forRoot(options?: any): DynamicModule;
|
|
4
8
|
}
|