@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,58 @@
1
+ import { Link } from "react-router-dom";
2
+ import DetailPanel from "../../components/DetailPanel";
3
+ import TabbedDataViewer from "../../components/tabs/TabbedDataViewer";
4
+ import type { QueryEntry } from "../../types";
5
+ import { getRoutesPaths } from "../../router/routes";
6
+ import { useConfig } from "../../utils/context";
7
+ import { formatDateWithTimeAgo } from "../../utils/date";
8
+ import QueryViewer from "../../components/queryFormatters/QueryViewer";
9
+
10
+ export default function QueryDetails({ query }: { query: QueryEntry }) {
11
+ const detailItems = [
12
+ query.lens_entry_id && {
13
+ label: "Request",
14
+ value: (
15
+ <Link
16
+ to={`${getRoutesPaths(useConfig()).REQUESTS}/${query.lens_entry_id}`}
17
+ className="text-blue-600 hover:underline font-semibold"
18
+ >
19
+ View Request
20
+ </Link>
21
+ ),
22
+ className: "text-gray-900 dark:text-gray-100",
23
+ },
24
+ {
25
+ label: "Time",
26
+ value: <span>{formatDateWithTimeAgo(query.created_at)}</span>,
27
+ className: "text-gray-900 dark:text-gray-100",
28
+ },
29
+ {
30
+ label: "Duration",
31
+ value: <span>{query.data.duration}</span>,
32
+ className: "text-gray-900 dark:text-gray-100",
33
+ },
34
+ {
35
+ label: "Provider",
36
+ value: <span>{query.data.type}</span>,
37
+ className: "text-gray-900 dark:text-gray-100",
38
+ },
39
+ ].filter((item) => !!item);
40
+
41
+ return (
42
+ <div className="flex flex-col gap-4">
43
+ {" "}
44
+ <DetailPanel title="Request Details" items={detailItems} />
45
+ <TabbedDataViewer
46
+ tabs={[
47
+ {
48
+ id: "query",
49
+ data: ["query"],
50
+ label: "Query",
51
+ content: <QueryViewer queryPayload={query.data} />,
52
+ },
53
+ ]}
54
+ defaultActiveTab="query"
55
+ />
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1,21 @@
1
+ import { LoadMoreButton } from "../../components/LoadMore";
2
+ import Table from "../../components/Table";
3
+ import type { HasMoreType, QueryTableRow } from "../../types";
4
+ import getColumns from "./columns";
5
+
6
+ const QueriesTable = ({
7
+ hasMoreObject,
8
+ }: {
9
+ hasMoreObject: HasMoreType<QueryTableRow>;
10
+ }) => {
11
+ return (
12
+ <div className="w-full">
13
+ <div className="overflow-x-auto">
14
+ <Table columns={getColumns()} data={hasMoreObject.data} />
15
+ </div>
16
+ <LoadMoreButton paginatedPage={hasMoreObject} />
17
+ </div>
18
+ );
19
+ };
20
+
21
+ export default QueriesTable;
@@ -0,0 +1,83 @@
1
+ import { CircleArrowRightIcon } from "lucide-react";
2
+ import type { JSX } from "react";
3
+ import { Link } from "react-router-dom";
4
+ import type { TableColumn } from "../../components/Table";
5
+ import { getRoutesPaths } from "../../router/routes";
6
+ import type { QueryTableRow } from "../../types";
7
+ import { useConfig } from "../../utils/context";
8
+ import { humanDifferentDate } from "../../utils/date";
9
+
10
+ function highlightSQL(query: string): JSX.Element {
11
+ return (
12
+ <span className="text-red-600 dark:text-red-400 font-mono">{query}</span>
13
+ );
14
+ }
15
+
16
+ const getColumns = (): TableColumn<QueryTableRow>[] => {
17
+ const paths = getRoutesPaths(useConfig());
18
+
19
+ return [
20
+ {
21
+ name: "Query",
22
+ render: (row) => (
23
+ <div className="col-span-5">
24
+ <code
25
+ className="text-sm font-mono text-slate-800 dark:text-slate-300 leading-relaxed line-clamp-1"
26
+ title={row.data.query}
27
+ >
28
+ {highlightSQL(row.data.query)}
29
+ </code>
30
+ </div>
31
+ ),
32
+ },
33
+ {
34
+ name: "Duration",
35
+ render: (row) => (
36
+ <div className="col-span-1 text-right">
37
+ <span className="text-sm text-slate-600 dark:text-slate-400 font-mono">
38
+ {row.data.duration}
39
+ </span>
40
+ </div>
41
+ ),
42
+ },
43
+ {
44
+ name: "Provider",
45
+ render: (row) => {
46
+ return (
47
+ <div className="col-span-2 text-right">
48
+ <span className="text-sm text-slate-600 dark:text-slate-400 font-mono">
49
+ {row.data.type}
50
+ </span>
51
+ </div>
52
+ );
53
+ },
54
+ },
55
+ {
56
+ name: "Happened",
57
+ render: (row) => {
58
+ const { label, exact } = humanDifferentDate(row.data.createdAt);
59
+ return (
60
+ <span className="text-nowrap" title={exact}>
61
+ {label}
62
+ </span>
63
+ );
64
+ },
65
+ position: "end",
66
+ class: "min-w-32",
67
+ },
68
+ {
69
+ name: "Actions",
70
+ render: (row) => (
71
+ <Link
72
+ to={`${paths.QUERIES}/${row.id}`}
73
+ className="transition-colors duration-100 hover:text-white"
74
+ >
75
+ <CircleArrowRightIcon size={20} />
76
+ </Link>
77
+ ),
78
+ position: "end",
79
+ },
80
+ ];
81
+ };
82
+
83
+ export default getColumns;
@@ -0,0 +1,82 @@
1
+ import { useMemo } from "react";
2
+ import DetailPanel, { type DetailItem } from "../../components/DetailPanel";
3
+ import type { OneRequest } from "../../types";
4
+ import { formatDateWithTimeAgo} from "../../utils/date";
5
+ import MethodBadge from "../../components/MethodBadge";
6
+ import StatusCode from "../../components/StatusCode";
7
+
8
+ const BasicRequestDetails = ({ request }: { request: OneRequest }) => {
9
+ const formattedTime = useMemo(() => {
10
+ return formatDateWithTimeAgo(request?.data?.createdAt);
11
+ }, [request?.data?.createdAt]);
12
+
13
+ if (!request || !request.data) {
14
+ return (
15
+ <DetailPanel
16
+ title="Request Details"
17
+ items={[]}
18
+ emptyMessage="No request data available"
19
+ />
20
+ );
21
+ }
22
+
23
+ const getHostname = () => {
24
+ return request?.data?.headers?.host || "Unknown";
25
+ };
26
+
27
+ const detailItems = useMemo(
28
+ (): DetailItem[] => [
29
+ {
30
+ label: "Time",
31
+ value: formattedTime,
32
+ className: "text-gray-900 dark:text-gray-100",
33
+ },
34
+ {
35
+ label: "Hostname",
36
+ value: getHostname(),
37
+ className: "text-gray-900 dark:text-gray-100 font-mono",
38
+ },
39
+ {
40
+ label: "Method",
41
+ value: request.data.method ? (
42
+ <MethodBadge method={request.data.method} />
43
+ ) : (
44
+ "Unknown"
45
+ ),
46
+ },
47
+ {
48
+ label: "Request ID",
49
+ value: request.data.id || "N/A",
50
+ className: "text-gray-700 dark:text-gray-300 font-mono text-sm",
51
+ },
52
+ {
53
+ label: "Path",
54
+ value: request.data.path || "N/A",
55
+ className: "text-gray-900 dark:text-gray-100 font-mono",
56
+ },
57
+ {
58
+ label: "Status",
59
+ value: request.data.status ? (
60
+ <StatusCode status={request.data.status} />
61
+ ) : (
62
+ "N/A"
63
+ ),
64
+ },
65
+ {
66
+ label: "Duration",
67
+ value: request.data.duration || "N/A",
68
+ className: "text-gray-900 dark:text-gray-100 font-medium",
69
+ },
70
+ {
71
+ label: "IP Address",
72
+ value: request.data.ip || "N/A",
73
+ className: "text-gray-900 dark:text-gray-100 font-mono",
74
+ },
75
+ ],
76
+ [request.data, formattedTime],
77
+ );
78
+
79
+ return <DetailPanel title="Request Details" items={detailItems} />;
80
+ };
81
+
82
+ export default BasicRequestDetails;
@@ -0,0 +1,70 @@
1
+ import DetailPanel from "../../components/DetailPanel";
2
+ import TabbedDataViewer from "../../components/tabs/TabbedDataViewer";
3
+ import type { OneRequest } from "../../types";
4
+ import BasicRequestDetails from "./BasicRequestDetails";
5
+
6
+ const RequestDetails = ({ request }: { request: OneRequest }) => {
7
+ const dynamicTabs = [
8
+ {
9
+ id: "payload",
10
+ label: "Payload",
11
+ data: request.data.body,
12
+ },
13
+ {
14
+ id: "headers",
15
+ label: "Headers",
16
+ data: request.data.headers,
17
+ },
18
+ ];
19
+
20
+ const responseTabs = [
21
+ {
22
+ id: "response-body",
23
+ label: "Body",
24
+ data: request.data.response.json,
25
+ },
26
+ {
27
+ id: "response-headers",
28
+ label: "Headers",
29
+ data: request.data.response.headers,
30
+ },
31
+ ];
32
+
33
+ return (
34
+ <div className="flex flex-col gap-3">
35
+ <BasicRequestDetails request={request} />
36
+ {request.data.user && (
37
+ <DetailPanel
38
+ title="User"
39
+ items={[
40
+ {
41
+ label: "ID",
42
+ value: request?.data?.user?.id,
43
+ },
44
+ {
45
+ label: "Email",
46
+ value: request?.data?.user?.email,
47
+ },
48
+ {
49
+ label: "Name",
50
+ value: request?.data?.user?.name,
51
+ },
52
+ ]}
53
+ />
54
+ )}
55
+
56
+ <TabbedDataViewer
57
+ tabs={dynamicTabs}
58
+ title="Request Data"
59
+ defaultActiveTab="payload"
60
+ />
61
+ <TabbedDataViewer
62
+ tabs={responseTabs}
63
+ title="Response Data"
64
+ defaultActiveTab="response-body"
65
+ />
66
+ </div>
67
+ );
68
+ };
69
+
70
+ export default RequestDetails;
@@ -0,0 +1,19 @@
1
+ import { LoadMoreButton } from "../../components/LoadMore";
2
+ import Table from "../../components/Table";
3
+ import type { HasMoreType, RequestTableRow } from "../../types";
4
+ import getColumns from "./columns";
5
+
6
+ const RequestTable = ({
7
+ hasMoreObject,
8
+ }: {
9
+ hasMoreObject: HasMoreType<RequestTableRow>;
10
+ }) => {
11
+ return (
12
+ <div>
13
+ <Table columns={getColumns()} data={hasMoreObject.data} />
14
+ <LoadMoreButton paginatedPage={hasMoreObject} />
15
+ </div>
16
+ );
17
+ };
18
+
19
+ export default RequestTable;
@@ -0,0 +1,62 @@
1
+ import { CircleArrowRightIcon } from "lucide-react";
2
+ import MethodBadge from "../../components/MethodBadge";
3
+ import StatusCode from "../../components/StatusCode";
4
+ import { getRoutesPaths } from "../../router/routes";
5
+ import type { RequestTableRow } from "../../types";
6
+ import { useConfig } from "../../utils/context";
7
+ import { humanDifferentDate } from "../../utils/date";
8
+ import type { TableColumn } from "../../components/Table";
9
+ import { Link } from "react-router-dom";
10
+
11
+ const getColumns = (): TableColumn<RequestTableRow>[] => {
12
+ const paths = getRoutesPaths(useConfig());
13
+
14
+ return [
15
+ {
16
+ name: "Verb",
17
+ render: (row) => <MethodBadge method={row.data.method} />,
18
+ },
19
+ {
20
+ name: "Path",
21
+ render: (row) => (
22
+ <Link
23
+ to={`${paths.REQUESTS}/${row.id}`}
24
+ className="line-clamp-2 max-w-80 min-w-40 text-base text-blue-600 dark:text-neutral-200 hover:underline"
25
+ >
26
+ {row.data.path}
27
+ </Link>
28
+ ),
29
+ },
30
+ {
31
+ name: "Status",
32
+ render: (row) => <StatusCode status={row.data.status} />,
33
+ },
34
+ {
35
+ name: "Duration",
36
+ value: (row) => row.data.duration,
37
+ },
38
+ {
39
+ name: "Happened",
40
+ render: (row) => {
41
+ const { label, exact } = humanDifferentDate(row.data.createdAt);
42
+ return <span title={exact}>{label}</span>;
43
+ },
44
+ position: "end",
45
+ class: "min-w-32",
46
+ },
47
+ {
48
+ name: "Actions",
49
+ render: (row) => (
50
+ <Link
51
+ to={`${paths.REQUESTS}/${row.id}`}
52
+ className="transition-colors duration-100 hover:text-white"
53
+ >
54
+ <CircleArrowRightIcon size={20} />
55
+ </Link>
56
+ ),
57
+ position: "end",
58
+ },
59
+ ];
60
+ };
61
+
62
+ export default getColumns;
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "strict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "erasableSyntaxOnly": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "noUncheckedSideEffectImports": true
25
+ },
26
+ "include": ["src"]
27
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "erasableSyntaxOnly": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedSideEffectImports": true
23
+ },
24
+ "include": ["vite.config.ts"]
25
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react-swc";
3
+ import tailwindcss from "@tailwindcss/vite";
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ base: "./",
8
+ plugins: [react(), tailwindcss()],
9
+ });
@@ -0,0 +1,13 @@
1
+ import { AsyncResource } from "node:async_hooks";
2
+ import { QueryEntry } from "../types";
3
+ import Emittery from "emittery";
4
+
5
+ interface CoreEvents {
6
+ query: { query: QueryEntry["data"] };
7
+ }
8
+
9
+ export const createEmittery = <T extends Record<string, any>>() => {
10
+ return new Emittery<T>();
11
+ };
12
+ export const lensResource = new AsyncResource("lens-emitter");
13
+ export const lensEmitter = new Emittery<CoreEvents>();
@@ -0,0 +1,176 @@
1
+ import { DateTime } from "luxon";
2
+ import { format, SqlLanguage } from "sql-formatter";
3
+ import { randomUUID } from "crypto";
4
+ import { fileURLToPath } from "url";
5
+ import path from "path/posix";
6
+
7
+ export const generateRandomUuid = () => {
8
+ return randomUUID();
9
+ };
10
+
11
+ type Bindings = any[] | Record<string, any>;
12
+
13
+ /**
14
+ * Interpolates SQL query placeholders with actual values.
15
+ * Supports:
16
+ * - ? (array-based)
17
+ * - $1, $name (numeric or named)
18
+ * - :name (named)
19
+ */
20
+ export function interpolateQuery(query: string, bindings: Bindings): string {
21
+ // Helper to convert a value into a safe SQL literal
22
+ const formatValue = (value: any): string => {
23
+ if (value === null || value === undefined) return "NULL";
24
+ if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
25
+ if (value instanceof DateTime) return `'${value.toISO()}'`;
26
+ if (value instanceof Date) return `'${value.toISOString()}'`;
27
+ if (Array.isArray(value))
28
+ return value
29
+ .map((v) => (typeof v === "string" ? `'${v.replace(/'/g, "''")}'` : v))
30
+ .join(", ");
31
+ if (typeof value === "object")
32
+ return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
33
+ return value.toString();
34
+ };
35
+
36
+ // Case 1: Array-based bindings for '?' placeholders
37
+ if (Array.isArray(bindings)) {
38
+ let i = 0;
39
+ return query.replace(/\?/g, () => {
40
+ if (i >= bindings.length)
41
+ throw new Error("Not enough bindings for placeholders");
42
+ return formatValue(bindings[i++]);
43
+ });
44
+ }
45
+
46
+ // Case 2: Named or numeric placeholders ($1, $name, :name)
47
+ return query.replace(/(\$|\:)(\w+)/g, (match, prefix, keyOrIndex) => {
48
+ let value;
49
+
50
+ if (prefix === "$" && /^\d+$/.test(keyOrIndex)) {
51
+ // Numeric placeholder: $1, $2, ...
52
+ const index = parseInt(keyOrIndex, 10) - 1;
53
+ const keys = Object.keys(bindings);
54
+ if (index < 0 || index >= keys.length)
55
+ throw new Error(`Missing binding for ${match}`);
56
+ // @ts-expect-error
57
+ value = bindings[keys[index]];
58
+ } else {
59
+ // Named placeholder: $name or :name
60
+ if (!(keyOrIndex in bindings))
61
+ throw new Error(`Missing binding for ${match}`);
62
+ value = bindings[keyOrIndex];
63
+ }
64
+
65
+ return formatValue(value);
66
+ });
67
+ }
68
+
69
+ export const formatSqlQuery = (query: string, language: SqlLanguage) => {
70
+ return format(query, {
71
+ language,
72
+ dataTypeCase: "upper",
73
+ keywordCase: "upper",
74
+ functionCase: "upper",
75
+ });
76
+ };
77
+
78
+ export function now() {
79
+ return DateTime.now().setZone("utc");
80
+ }
81
+
82
+ export function nowISO() {
83
+ return now().toISO({ includeOffset: false }) as string;
84
+ }
85
+
86
+ export function sqlDateTime(dateTime?: DateTime | null) {
87
+ const time = dateTime ?? now();
88
+
89
+ return time.toSQL({ includeOffset: false });
90
+ }
91
+
92
+ export function convertToUTC(dateTime: string) {
93
+ return DateTime.fromISO(dateTime)
94
+ .setZone("utc")
95
+ .toISO({ includeOffset: false }) as string;
96
+ }
97
+
98
+ export function getMeta(metaUrl?: string): {
99
+ __filename: string;
100
+ __dirname: string;
101
+ } {
102
+ const isESM = typeof __dirname === "undefined";
103
+
104
+ if (isESM) {
105
+ if (!metaUrl) {
106
+ throw new Error("In ESM, you must pass import.meta.url to getMeta()");
107
+ }
108
+
109
+ const __filename = fileURLToPath(metaUrl);
110
+ const __dirname = path.dirname(__filename);
111
+ return { __filename, __dirname };
112
+ } else {
113
+ // @ts-ignore - available only in CJS
114
+ return { __filename, __dirname };
115
+ }
116
+ }
117
+
118
+ export function isStaticFile(params: string[]) {
119
+ return params.includes("assets");
120
+ }
121
+
122
+ export function stripBeforeAssetsPath(url: string) {
123
+ const match = url.match(/assets.*/);
124
+ return match ? match[0] : url;
125
+ }
126
+
127
+ export function prepareIgnoredPaths(path: string, ignoredPaths: RegExp[]) {
128
+ const normalizedPath = path.replace(/^\/+|\/+$/g, "");
129
+ ignoredPaths = [
130
+ ...ignoredPaths,
131
+ new RegExp(`^\/?${normalizedPath}(\/|$)`),
132
+ /^\/?lens-config$/,
133
+ /^\/\.well-known\//,
134
+ ];
135
+
136
+ return { ignoredPaths, normalizedPath };
137
+ }
138
+
139
+ export function shouldIgnoreCurrentPath(
140
+ path: string,
141
+ ignoredPaths: RegExp[],
142
+ onlyPaths: RegExp[],
143
+ ) {
144
+ if (onlyPaths.length > 0) {
145
+ return !onlyPaths.some((pattern) => pattern.test(path));
146
+ }
147
+
148
+ return ignoredPaths.some((pattern) => pattern.test(path));
149
+ }
150
+
151
+ export function prettyHrTime(
152
+ hrtime: [number, number],
153
+ verbose = false,
154
+ ): string {
155
+ const seconds = hrtime[0];
156
+ const nanoseconds = hrtime[1];
157
+ const ms = seconds * 1000 + nanoseconds / 1e6;
158
+
159
+ if (verbose) {
160
+ if (seconds > 60) {
161
+ const minutes = Math.floor(seconds / 60);
162
+ const remainingSeconds = seconds % 60;
163
+ return `${minutes}m ${remainingSeconds}s`;
164
+ }
165
+ if (seconds >= 1) {
166
+ return `${seconds}.${Math.floor(nanoseconds / 1e7)}s`;
167
+ }
168
+ return `${ms.toFixed(3)} ms`;
169
+ }
170
+
171
+ if (ms < 1000) {
172
+ return `${ms.toFixed(0)} ms`;
173
+ }
174
+
175
+ return `${(ms / 1000).toFixed(1)} s`;
176
+ }
@@ -0,0 +1,2 @@
1
+ export { default as QueryWatcher } from "./query_watcher";
2
+ export { default as RequestWatcher } from "./request_watcher";
@@ -0,0 +1,15 @@
1
+ import { getStore } from "../context/context";
2
+ import Watcher from "../core/watcher";
3
+ import { WatcherTypeEnum, type QueryEntry } from "../types/index";
4
+
5
+ export default class QueryWatcher extends Watcher {
6
+ name = WatcherTypeEnum.QUERY;
7
+
8
+ async log(entry: QueryEntry) {
9
+ await getStore().save({
10
+ type: this.name,
11
+ data: entry.data,
12
+ requestId: entry.requestId ?? "",
13
+ });
14
+ }
15
+ }