@lensjs/core 1.0.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.
Files changed (84) hide show
  1. package/README.md +1 -0
  2. package/copy-front-build.cjs +16 -0
  3. package/package.json +40 -0
  4. package/src/abstracts/adapter.ts +41 -0
  5. package/src/abstracts/store.ts +36 -0
  6. package/src/context/container.ts +67 -0
  7. package/src/context/context.ts +9 -0
  8. package/src/core/api_controller.ts +116 -0
  9. package/src/core/lens.ts +147 -0
  10. package/src/core/watcher.ts +6 -0
  11. package/src/index.ts +11 -0
  12. package/src/stores/better_sqlite.ts +176 -0
  13. package/src/stores/index.ts +1 -0
  14. package/src/types/index.ts +103 -0
  15. package/src/ui/README.md +69 -0
  16. package/src/ui/bun.lock +750 -0
  17. package/src/ui/eslint.config.js +27 -0
  18. package/src/ui/index.html +13 -0
  19. package/src/ui/package-lock.json +2953 -0
  20. package/src/ui/package.json +40 -0
  21. package/src/ui/public/favicon.ico +0 -0
  22. package/src/ui/src/App.tsx +40 -0
  23. package/src/ui/src/components/DetailPanel.tsx +70 -0
  24. package/src/ui/src/components/JsonViewer.tsx +232 -0
  25. package/src/ui/src/components/LoadMore.tsx +25 -0
  26. package/src/ui/src/components/MethodBadge.tsx +19 -0
  27. package/src/ui/src/components/Modal.tsx +48 -0
  28. package/src/ui/src/components/StatusCode.tsx +20 -0
  29. package/src/ui/src/components/Table.tsx +127 -0
  30. package/src/ui/src/components/layout/DeleteButton.tsx +60 -0
  31. package/src/ui/src/components/layout/Footer.tsx +12 -0
  32. package/src/ui/src/components/layout/Header.tsx +40 -0
  33. package/src/ui/src/components/layout/Layout.tsx +49 -0
  34. package/src/ui/src/components/layout/LoadingScreen.tsx +14 -0
  35. package/src/ui/src/components/layout/Sidebar.tsx +67 -0
  36. package/src/ui/src/components/queryFormatters/MongoViewer.tsx +92 -0
  37. package/src/ui/src/components/queryFormatters/QueryViewer.tsx +18 -0
  38. package/src/ui/src/components/queryFormatters/SqlViewer.tsx +105 -0
  39. package/src/ui/src/components/table/NoData.tsx +26 -0
  40. package/src/ui/src/components/tabs/TabbedDataViewer.tsx +77 -0
  41. package/src/ui/src/containers/queries/QueriesContainer.tsx +21 -0
  42. package/src/ui/src/containers/queries/QueryDetailsContainer.tsx +15 -0
  43. package/src/ui/src/containers/requests/RequestDetailsContainer.tsx +16 -0
  44. package/src/ui/src/containers/requests/RequestsContainer.tsx +22 -0
  45. package/src/ui/src/hooks/useLensApi.ts +92 -0
  46. package/src/ui/src/hooks/useLoadMore.ts +48 -0
  47. package/src/ui/src/hooks/useQueries.ts +58 -0
  48. package/src/ui/src/hooks/useRequests.ts +79 -0
  49. package/src/ui/src/hooks/useTanstackApi.ts +126 -0
  50. package/src/ui/src/index.css +78 -0
  51. package/src/ui/src/interfaces/index.ts +10 -0
  52. package/src/ui/src/main.tsx +33 -0
  53. package/src/ui/src/router/Router.ts +11 -0
  54. package/src/ui/src/router/routes/Loading.tsx +5 -0
  55. package/src/ui/src/router/routes/index.tsx +85 -0
  56. package/src/ui/src/types/index.ts +95 -0
  57. package/src/ui/src/utils/api.ts +7 -0
  58. package/src/ui/src/utils/context.ts +24 -0
  59. package/src/ui/src/utils/date.ts +36 -0
  60. package/src/ui/src/views/queries/QueryDetails.tsx +58 -0
  61. package/src/ui/src/views/queries/QueryTable.tsx +21 -0
  62. package/src/ui/src/views/queries/columns.tsx +83 -0
  63. package/src/ui/src/views/requests/BasicRequestDetails.tsx +82 -0
  64. package/src/ui/src/views/requests/RequestDetails.tsx +70 -0
  65. package/src/ui/src/views/requests/RequetsTable.tsx +19 -0
  66. package/src/ui/src/views/requests/columns.tsx +62 -0
  67. package/src/ui/src/vite-env.d.ts +1 -0
  68. package/src/ui/tsconfig.app.json +27 -0
  69. package/src/ui/tsconfig.json +7 -0
  70. package/src/ui/tsconfig.node.json +25 -0
  71. package/src/ui/vite.config.ts +9 -0
  72. package/src/utils/event_emitter.ts +13 -0
  73. package/src/utils/index.ts +176 -0
  74. package/src/watchers/index.ts +2 -0
  75. package/src/watchers/query_watcher.ts +15 -0
  76. package/src/watchers/request_watcher.ts +27 -0
  77. package/tests/core/lens.test.ts +89 -0
  78. package/tests/stores/better_sqlite.test.ts +168 -0
  79. package/tests/utils/index.test.ts +182 -0
  80. package/tests/watchers/query_watcher.test.ts +35 -0
  81. package/tests/watchers/request_watcher.test.ts +59 -0
  82. package/tsconfig.json +3 -0
  83. package/tsup.config.ts +15 -0
  84. package/vitest.config.ts +9 -0
@@ -0,0 +1,27 @@
1
+ import { getStore } from "../context/context";
2
+ import Watcher from "../core/watcher";
3
+ import { WatcherTypeEnum, type RequestEntry } from "../types/index";
4
+
5
+ export default class RequestWatcher extends Watcher {
6
+ name = WatcherTypeEnum.REQUEST;
7
+
8
+ async log(data: RequestEntry) {
9
+ await getStore().save({
10
+ id: data.request.id,
11
+ type: this.name,
12
+ minimal_data: {
13
+ id: data.request.id,
14
+ method: data.request.method,
15
+ path: data.request.path,
16
+ duration: data.request.duration,
17
+ createdAt: data.request.createdAt,
18
+ status: data.request.status,
19
+ },
20
+ data: {
21
+ ...data.request,
22
+ user: data.user,
23
+ response: data.response,
24
+ },
25
+ });
26
+ }
27
+ }
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import Lens from '../../src/core/lens';
3
+ import Store from '../../src/abstracts/store';
4
+ import Adapter from '../../src/abstracts/adapter';
5
+ import Watcher from '../../src/core/watcher';
6
+ import { WatcherTypeEnum } from '../../src/types';
7
+
8
+ // Mock implementations
9
+ class MockStore extends Store {
10
+ initialize = vi.fn();
11
+ save = vi.fn();
12
+ getAllRequests = vi.fn();
13
+ getAllQueries = vi.fn();
14
+ allByRequestId = vi.fn();
15
+ find = vi.fn();
16
+ truncate = vi.fn();
17
+ paginate = vi.fn();
18
+ count = vi.fn();
19
+ }
20
+
21
+ class MockAdapter extends Adapter {
22
+ setup = vi.fn();
23
+ registerRoutes = vi.fn();
24
+ serveUI = vi.fn();
25
+ override setWatchers = vi.fn().mockReturnThis();
26
+ }
27
+
28
+ class MockWatcher extends Watcher {
29
+ name = WatcherTypeEnum.QUERY;
30
+ log = vi.fn();
31
+ }
32
+
33
+ describe('Lens', () => {
34
+ let mockStore: MockStore;
35
+ let mockAdapter: MockAdapter;
36
+ let mockWatcher: MockWatcher;
37
+
38
+ beforeEach(() => {
39
+ mockStore = new MockStore();
40
+ mockAdapter = new MockAdapter();
41
+ mockWatcher = new MockWatcher();
42
+
43
+ // Reset static properties of Lens
44
+ Lens['watchers'].clear();
45
+ Lens['store'] = undefined as any;
46
+ Lens['adapter'] = undefined as any;
47
+ });
48
+
49
+ it('should set and get a store', async () => {
50
+ Lens.setStore(mockStore);
51
+ const store = await Lens.getStore();
52
+ expect(store).toBe(mockStore);
53
+ });
54
+
55
+ it('should set and get an adapter', () => {
56
+ Lens.setAdapter(mockAdapter);
57
+ const adapter = Lens.getAdapter();
58
+ expect(adapter).toBe(mockAdapter);
59
+ });
60
+
61
+ it('should throw an error if adapter is not set', () => {
62
+ expect(() => Lens.getAdapter()).toThrow('No adapter has been set');
63
+ });
64
+
65
+ it('should add a watcher', () => {
66
+ Lens.watch(mockWatcher);
67
+ expect(Lens['watchers'].get(WatcherTypeEnum.QUERY)).toBe(mockWatcher);
68
+ });
69
+
70
+ it('should start and configure the adapter', async () => {
71
+ Lens.setStore(mockStore);
72
+ Lens.setAdapter(mockAdapter);
73
+ Lens.watch(mockWatcher);
74
+
75
+ await Lens.start({ basePath: 'test', appName: 'TestApp', enabled: true });
76
+
77
+ expect(mockAdapter.setWatchers).toHaveBeenCalledWith([mockWatcher]);
78
+ expect(mockAdapter.setup).toHaveBeenCalled();
79
+ expect(mockAdapter.registerRoutes).toHaveBeenCalled();
80
+ expect(mockAdapter.serveUI).toHaveBeenCalled();
81
+ });
82
+
83
+ it('should not start if not enabled', async () => {
84
+ Lens.setAdapter(mockAdapter);
85
+ await Lens.start({ basePath: 'test', appName: 'TestApp', enabled: false });
86
+
87
+ expect(mockAdapter.setup).not.toHaveBeenCalled();
88
+ });
89
+ });
@@ -0,0 +1,168 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import BetterSqliteStore from "../../src/stores/better_sqlite";
3
+ import { WatcherTypeEnum } from "../../src/types";
4
+ import Database from "libsql";
5
+
6
+ class TestBetterSqliteStore extends BetterSqliteStore {
7
+ public override async initialize() {
8
+ this.connection = new Database(":memory:");
9
+ this.setupSchemaForTesting();
10
+ }
11
+
12
+ public setupSchemaForTesting() {
13
+ const createTable = `
14
+ CREATE TABLE IF NOT EXISTS lens_entries (
15
+ id TEXT PRIMARY KEY,
16
+ minimal_data TEXT,
17
+ data TEXT NOT NULL,
18
+ type TEXT NOT NULL,
19
+ created_at TEXT NOT NULL,
20
+ updated_at TEXT,
21
+ lens_entry_id TEXT NULL
22
+ );
23
+ `;
24
+
25
+ const createIndex = `
26
+ CREATE INDEX IF NOT EXISTS lens_entries_id_type_index
27
+ ON lens_entries (id, type);
28
+ `;
29
+
30
+ this.connection.exec(createTable);
31
+ this.connection.exec(createIndex);
32
+ }
33
+ }
34
+
35
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
36
+
37
+ describe("BetterSqliteStore", () => {
38
+ let store: TestBetterSqliteStore;
39
+
40
+ beforeEach(async () => {
41
+ store = new TestBetterSqliteStore();
42
+ await store.initialize();
43
+ });
44
+
45
+ it("should save and find an entry", async () => {
46
+ const entry = {
47
+ id: "test-entry-1",
48
+ type: WatcherTypeEnum.REQUEST,
49
+ data: { message: "hello" },
50
+ minimal_data: { message: "hello" },
51
+ };
52
+ await store.save(entry);
53
+
54
+ const count = await store.count(WatcherTypeEnum.REQUEST);
55
+ expect(count).toBe(1);
56
+ });
57
+
58
+ it("should truncate the database", async () => {
59
+ const entry = {
60
+ type: WatcherTypeEnum.REQUEST,
61
+ data: { message: "hello" },
62
+ };
63
+ await store.save(entry);
64
+ let count = await store.count(WatcherTypeEnum.REQUEST);
65
+ expect(count).toBe(1);
66
+
67
+ await store.truncate();
68
+ count = await store.count(WatcherTypeEnum.REQUEST);
69
+ expect(count).toBe(0);
70
+ });
71
+
72
+ it("should paginate requests", async () => {
73
+ for (let i = 0; i < 15; i++) {
74
+ await store.save({
75
+ type: WatcherTypeEnum.REQUEST,
76
+ data: { index: i },
77
+ minimal_data: { index: i },
78
+ });
79
+ await sleep(1);
80
+ }
81
+
82
+ const paginator = await store.getAllRequests({ page: 2, perPage: 5 });
83
+ expect(paginator.data.length).toBe(5);
84
+ expect(paginator.meta.total).toBe(15);
85
+ expect(paginator.meta.currentPage).toBe(2);
86
+ expect(paginator.meta.lastPage).toBe(3);
87
+ // Check that the full data is not included
88
+
89
+ // @ts-ignore
90
+ expect(paginator.data[0].data).toEqual({ index: 9 });
91
+ });
92
+
93
+ it("should paginate queries", async () => {
94
+ for (let i = 0; i < 15; i++) {
95
+ await store.save({
96
+ type: WatcherTypeEnum.QUERY,
97
+ data: { query: `SELECT ${i}` },
98
+ });
99
+ await sleep(1);
100
+ }
101
+
102
+ const paginator = await store.getAllQueries({ page: 1, perPage: 7 });
103
+ expect(paginator.data.length).toBe(7);
104
+ expect(paginator.meta.total).toBe(15);
105
+ expect(paginator.meta.currentPage).toBe(1);
106
+ expect(paginator.meta.lastPage).toBe(3);
107
+ paginator.data[0] &&
108
+ expect(paginator.data[0].data).toEqual({ query: "SELECT 14" });
109
+ });
110
+
111
+ it("should find an entry by id", async () => {
112
+ const entry = {
113
+ id: "test-id",
114
+ type: WatcherTypeEnum.REQUEST,
115
+ data: { message: "hello" },
116
+ };
117
+ await store.save(entry);
118
+
119
+ const foundEntry = await store.find(WatcherTypeEnum.REQUEST, "test-id");
120
+ expect(foundEntry).not.toBeNull();
121
+ expect(foundEntry?.id).toBe("test-id");
122
+ expect(foundEntry?.data).toEqual({ message: "hello" });
123
+ });
124
+
125
+ it("should return null if entry is not found", async () => {
126
+ const foundEntry = await store.find(
127
+ WatcherTypeEnum.REQUEST,
128
+ "non-existent-id",
129
+ );
130
+ expect(foundEntry).toBeNull();
131
+ });
132
+
133
+ it("should retrieve all entries by request id", async () => {
134
+ const requestId = "request-1";
135
+ await store.save({
136
+ id: requestId,
137
+ type: WatcherTypeEnum.REQUEST,
138
+ data: { url: "/users" },
139
+ });
140
+ await sleep(1);
141
+ await store.save({
142
+ requestId: requestId,
143
+ type: WatcherTypeEnum.QUERY,
144
+ data: { query: "SELECT * FROM users" },
145
+ });
146
+ await sleep(1);
147
+ await store.save({
148
+ requestId: requestId,
149
+ type: WatcherTypeEnum.QUERY,
150
+ data: { query: "SELECT * FROM posts" },
151
+ });
152
+ await sleep(1);
153
+ await store.save({
154
+ type: WatcherTypeEnum.REQUEST,
155
+ data: { url: "/products" },
156
+ });
157
+
158
+ const entries = await store.allByRequestId(
159
+ requestId,
160
+ WatcherTypeEnum.QUERY,
161
+ );
162
+ expect(entries.length).toBe(2);
163
+ entries[0] &&
164
+ expect(entries[0].data).toEqual({ query: "SELECT * FROM posts" });
165
+ entries[1] &&
166
+ expect(entries[1].data).toEqual({ query: "SELECT * FROM users" });
167
+ });
168
+ });
@@ -0,0 +1,182 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ interpolateQuery,
4
+ formatSqlQuery,
5
+ sqlDateTime,
6
+ isStaticFile,
7
+ stripBeforeAssetsPath,
8
+ prepareIgnoredPaths,
9
+ prettyHrTime,
10
+ } from "../../src/utils/index";
11
+ import { DateTime } from "luxon";
12
+
13
+ describe("utils", () => {
14
+ describe("interpolateQuery", () => {
15
+ // Array-based ?
16
+ it("should replace ? placeholders with array values", () => {
17
+ const query = "SELECT * FROM users WHERE id = ? AND name = ?";
18
+ const bindings = [1, "John Doe"];
19
+ const expected = "SELECT * FROM users WHERE id = 1 AND name = 'John Doe'";
20
+ expect(interpolateQuery(query, bindings)).toBe(expected);
21
+ });
22
+
23
+ it("should throw if not enough array bindings", () => {
24
+ const query = "SELECT * FROM users WHERE id = ? AND name = ?";
25
+ const bindings = [1];
26
+ expect(() => interpolateQuery(query, bindings)).toThrow(
27
+ "Not enough bindings for placeholders",
28
+ );
29
+ });
30
+
31
+ it("should handle null and undefined", () => {
32
+ const query = "SELECT * FROM users WHERE id = ? AND name = ?";
33
+ const bindings = [null, undefined];
34
+ const expected = "SELECT * FROM users WHERE id = NULL AND name = NULL";
35
+ expect(interpolateQuery(query, bindings)).toBe(expected);
36
+ });
37
+
38
+ it("should handle DateTime and Date objects", () => {
39
+ const query =
40
+ "SELECT * FROM users WHERE created_at > ? AND updated_at > ?";
41
+ const dateTime = DateTime.fromISO("2025-01-01T00:00:00.000Z");
42
+ const date = new Date("2025-01-02T00:00:00.000Z");
43
+ const bindings = [dateTime, date];
44
+ const expected = `SELECT * FROM users WHERE created_at > '${dateTime.toISO()}' AND updated_at > '${date.toISOString()}'`;
45
+ expect(interpolateQuery(query, bindings)).toBe(expected);
46
+ });
47
+
48
+ it("should handle arrays", () => {
49
+ const query = "SELECT * FROM users WHERE id IN (?)";
50
+ const bindings = [[1, 2, 3]];
51
+ const expected = "SELECT * FROM users WHERE id IN (1, 2, 3)";
52
+ expect(interpolateQuery(query, bindings)).toBe(expected);
53
+ });
54
+
55
+ it("should handle objects", () => {
56
+ const query = "SELECT * FROM users WHERE meta = ?";
57
+ const bindings = [{ key: "value" }];
58
+ const expected = `SELECT * FROM users WHERE meta = '${JSON.stringify({ key: "value" })}'`;
59
+ expect(interpolateQuery(query, bindings)).toBe(expected);
60
+ });
61
+
62
+ // Named / numeric placeholders
63
+ it("should replace $1 numeric placeholder", () => {
64
+ const query = "INSERT INTO users (id, name) VALUES (NULL, $1);";
65
+ const bindings = { $1: "John Doe" };
66
+ const expected =
67
+ "INSERT INTO users (id, name) VALUES (NULL, 'John Doe');";
68
+ expect(interpolateQuery(query, bindings)).toBe(expected);
69
+ });
70
+
71
+ it("should replace $name named placeholder", () => {
72
+ const query = "UPDATE users SET name = $name WHERE id = $id;";
73
+ const bindings = { name: "Alice", id: 42 };
74
+ const expected = "UPDATE users SET name = 'Alice' WHERE id = 42;";
75
+ expect(interpolateQuery(query, bindings)).toBe(expected);
76
+ });
77
+
78
+ it("should replace :name named placeholder", () => {
79
+ const query = "DELETE FROM users WHERE id = :id;";
80
+ const bindings = { id: 7 };
81
+ const expected = "DELETE FROM users WHERE id = 7;";
82
+ expect(interpolateQuery(query, bindings)).toBe(expected);
83
+ });
84
+
85
+ it("should throw if named placeholder is missing", () => {
86
+ const query = "SELECT * FROM users WHERE id = :id;";
87
+ const bindings = {};
88
+ expect(() => interpolateQuery(query, bindings)).toThrow(
89
+ "Missing binding for :id",
90
+ );
91
+ });
92
+ });
93
+
94
+ describe("formatSqlQuery", () => {
95
+ it("should format the SQL query", () => {
96
+ const query = "select * from users where id = 1";
97
+ const expected = "SELECT\n *\nFROM\n users\nWHERE\n id = 1";
98
+ expect(formatSqlQuery(query, "mysql")).toBe(expected);
99
+ });
100
+ });
101
+
102
+ describe("sqlDateTime", () => {
103
+ it("should return the current time in SQL format if no time is provided", () => {
104
+ const now = DateTime.now();
105
+ const expected = now.toSQL({ includeOffset: false });
106
+ // Since the exact time will differ, we check the format and length
107
+ expect(sqlDateTime()).toMatch(
108
+ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/,
109
+ );
110
+ expect(sqlDateTime()?.length).toBe(expected?.length);
111
+ });
112
+
113
+ it("should return the provided time in SQL format", () => {
114
+ const dateTime = DateTime.fromISO("2025-01-01T00:00:00.000Z", {
115
+ zone: "utc",
116
+ });
117
+ const expected = "2025-01-01 00:00:00.000";
118
+ expect(sqlDateTime(dateTime)).toBe(expected);
119
+ });
120
+ });
121
+
122
+ describe("isStaticFile", () => {
123
+ it('should return true if "assets" is in the params', () => {
124
+ expect(isStaticFile(["users", "1", "assets"])).toBe(true);
125
+ });
126
+
127
+ it('should return false if "assets" is not in the params', () => {
128
+ expect(isStaticFile(["users", "1", "profile"])).toBe(false);
129
+ });
130
+ });
131
+
132
+ describe("stripBeforeAssetsPath", () => {
133
+ it('should return the path from "assets" onwards', () => {
134
+ const url = "/users/1/assets/image.png";
135
+ const expected = "assets/image.png";
136
+ expect(stripBeforeAssetsPath(url)).toBe(expected);
137
+ });
138
+
139
+ it('should return the original url if "assets" is not present', () => {
140
+ const url = "/users/1/profile";
141
+ expect(stripBeforeAssetsPath(url)).toBe(url);
142
+ });
143
+ });
144
+
145
+ describe("prepareIgnoredPaths", () => {
146
+ it("should return the normalized path and the ignored paths", () => {
147
+ const path = "/api";
148
+ const ignoredPaths = [/^\/admin/];
149
+ const { normalizedPath, ignoredPaths: newIgnoredPaths } =
150
+ prepareIgnoredPaths(path, ignoredPaths);
151
+ expect(normalizedPath).toBe("api");
152
+ expect(newIgnoredPaths).toHaveLength(4);
153
+ newIgnoredPaths[0] &&
154
+ expect(newIgnoredPaths[0].toString()).toBe("/^\\/admin/");
155
+ newIgnoredPaths[1] &&
156
+ expect(newIgnoredPaths[1].toString()).toBe("/^\\/?api(\\/|$)/");
157
+ });
158
+ });
159
+
160
+ describe("prettyHrTime", () => {
161
+ it("should format high-resolution time in milliseconds", () => {
162
+ const hrtime: [number, number] = [0, 123456789]; // ~123.45 ms
163
+ expect(prettyHrTime(hrtime)).toBe("123 ms");
164
+ });
165
+
166
+ it("should format high-resolution time in seconds", () => {
167
+ const hrtime: [number, number] = [1, 500000000]; // 1.5s
168
+ expect(prettyHrTime(hrtime)).toBe("1.5 s");
169
+ });
170
+
171
+ it("should format high-resolution time verbosely", () => {
172
+ const hrtime: [number, number] = [0, 123456789]; // ~123.45 ms
173
+ expect(prettyHrTime(hrtime, true)).toBe("123.457 ms");
174
+
175
+ const secondsHrTime: [number, number] = [2, 123456789];
176
+ expect(prettyHrTime(secondsHrTime, true)).toBe("2.12s");
177
+
178
+ const minutesHrTime: [number, number] = [65, 123456789];
179
+ expect(prettyHrTime(minutesHrTime, true)).toBe("1m 5s");
180
+ });
181
+ });
182
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import QueryWatcher from '../../src/watchers/query_watcher';
3
+ import { WatcherTypeEnum, type QueryEntry } from '../../src/types';
4
+ import Store from '../../src/abstracts/store';
5
+
6
+ const mockStore = {
7
+ save: vi.fn(),
8
+ } as unknown as Store;
9
+
10
+ vi.mock('../../src/context/context', () => ({
11
+ getStore: () => mockStore,
12
+ }));
13
+
14
+ describe('QueryWatcher', () => {
15
+ it('should log a query entry to the store', async () => {
16
+ const watcher = new QueryWatcher();
17
+ const queryEntry: QueryEntry = {
18
+ data: {
19
+ type: 'sql',
20
+ duration: '10 ms',
21
+ query: 'SELECT * FROM users',
22
+ createdAt: '2025-01-01T00:00:00.000Z',
23
+ },
24
+ requestId: 'request-1',
25
+ };
26
+
27
+ await watcher.log(queryEntry);
28
+
29
+ expect(mockStore.save).toHaveBeenCalledWith({
30
+ type: WatcherTypeEnum.QUERY,
31
+ data: queryEntry.data,
32
+ requestId: 'request-1',
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import RequestWatcher from "../../src/watchers/request_watcher";
3
+ import { WatcherTypeEnum, type RequestEntry } from "../../src/types";
4
+ import Store from "../../src/abstracts/store";
5
+
6
+ const mockStore = {
7
+ save: vi.fn(),
8
+ } as unknown as Store;
9
+
10
+ vi.mock("../../src/context/context", () => ({
11
+ getStore: () => mockStore,
12
+ }));
13
+
14
+ describe("RequestWatcher", () => {
15
+ it("should log a request entry to the store", async () => {
16
+ const watcher = new RequestWatcher();
17
+ const requestEntry: RequestEntry = {
18
+ request: {
19
+ body: {},
20
+ id: "request-1",
21
+ method: "GET",
22
+ path: "/users",
23
+ duration: "200 ms",
24
+ createdAt: "2025-01-01T00:00:00.000Z",
25
+ status: 200,
26
+ ip: "127.0.0.1",
27
+ headers: {},
28
+ },
29
+ user: { id: "user-1", name: "John Doe", email: "john@example.com" },
30
+ response: {
31
+ json: {},
32
+ headers: {},
33
+ },
34
+ };
35
+
36
+ await watcher.log(requestEntry);
37
+
38
+ expect(mockStore.save).toHaveBeenCalledWith({
39
+ id: "request-1",
40
+ type: WatcherTypeEnum.REQUEST,
41
+ minimal_data: {
42
+ id: "request-1",
43
+ method: "GET",
44
+ path: "/users",
45
+ duration: '200 ms',
46
+ createdAt: "2025-01-01T00:00:00.000Z",
47
+ status: 200,
48
+ },
49
+ data: {
50
+ ...requestEntry.request,
51
+ user: { id: "user-1", name: "John Doe", email: "john@example.com" },
52
+ response: {
53
+ json: {},
54
+ headers: {},
55
+ },
56
+ },
57
+ });
58
+ });
59
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/**/*.ts", "!src/ui/**/*"],
5
+ outDir: "dist",
6
+ format: ["esm", "cjs"],
7
+ dts: true,
8
+ bundle: true,
9
+ target: "node18",
10
+ splitting: true,
11
+ clean: true,
12
+ shims: false,
13
+ // shims: true,
14
+ skipNodeModulesBundle: true,
15
+ });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['tests/**/*.test.ts'],
8
+ },
9
+ });