@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.
- package/README.md +1 -0
- package/copy-front-build.cjs +16 -0
- package/package.json +40 -0
- package/src/abstracts/adapter.ts +41 -0
- package/src/abstracts/store.ts +36 -0
- package/src/context/container.ts +67 -0
- package/src/context/context.ts +9 -0
- package/src/core/api_controller.ts +116 -0
- package/src/core/lens.ts +147 -0
- package/src/core/watcher.ts +6 -0
- package/src/index.ts +11 -0
- package/src/stores/better_sqlite.ts +176 -0
- package/src/stores/index.ts +1 -0
- package/src/types/index.ts +103 -0
- package/src/ui/README.md +69 -0
- package/src/ui/bun.lock +750 -0
- package/src/ui/eslint.config.js +27 -0
- package/src/ui/index.html +13 -0
- package/src/ui/package-lock.json +2953 -0
- package/src/ui/package.json +40 -0
- package/src/ui/public/favicon.ico +0 -0
- package/src/ui/src/App.tsx +40 -0
- package/src/ui/src/components/DetailPanel.tsx +70 -0
- package/src/ui/src/components/JsonViewer.tsx +232 -0
- package/src/ui/src/components/LoadMore.tsx +25 -0
- package/src/ui/src/components/MethodBadge.tsx +19 -0
- package/src/ui/src/components/Modal.tsx +48 -0
- package/src/ui/src/components/StatusCode.tsx +20 -0
- package/src/ui/src/components/Table.tsx +127 -0
- package/src/ui/src/components/layout/DeleteButton.tsx +60 -0
- package/src/ui/src/components/layout/Footer.tsx +12 -0
- package/src/ui/src/components/layout/Header.tsx +40 -0
- package/src/ui/src/components/layout/Layout.tsx +49 -0
- package/src/ui/src/components/layout/LoadingScreen.tsx +14 -0
- package/src/ui/src/components/layout/Sidebar.tsx +67 -0
- package/src/ui/src/components/queryFormatters/MongoViewer.tsx +92 -0
- package/src/ui/src/components/queryFormatters/QueryViewer.tsx +18 -0
- package/src/ui/src/components/queryFormatters/SqlViewer.tsx +105 -0
- package/src/ui/src/components/table/NoData.tsx +26 -0
- package/src/ui/src/components/tabs/TabbedDataViewer.tsx +77 -0
- package/src/ui/src/containers/queries/QueriesContainer.tsx +21 -0
- package/src/ui/src/containers/queries/QueryDetailsContainer.tsx +15 -0
- package/src/ui/src/containers/requests/RequestDetailsContainer.tsx +16 -0
- package/src/ui/src/containers/requests/RequestsContainer.tsx +22 -0
- package/src/ui/src/hooks/useLensApi.ts +92 -0
- package/src/ui/src/hooks/useLoadMore.ts +48 -0
- package/src/ui/src/hooks/useQueries.ts +58 -0
- package/src/ui/src/hooks/useRequests.ts +79 -0
- package/src/ui/src/hooks/useTanstackApi.ts +126 -0
- package/src/ui/src/index.css +78 -0
- package/src/ui/src/interfaces/index.ts +10 -0
- package/src/ui/src/main.tsx +33 -0
- package/src/ui/src/router/Router.ts +11 -0
- package/src/ui/src/router/routes/Loading.tsx +5 -0
- package/src/ui/src/router/routes/index.tsx +85 -0
- package/src/ui/src/types/index.ts +95 -0
- package/src/ui/src/utils/api.ts +7 -0
- package/src/ui/src/utils/context.ts +24 -0
- package/src/ui/src/utils/date.ts +36 -0
- package/src/ui/src/views/queries/QueryDetails.tsx +58 -0
- package/src/ui/src/views/queries/QueryTable.tsx +21 -0
- package/src/ui/src/views/queries/columns.tsx +83 -0
- package/src/ui/src/views/requests/BasicRequestDetails.tsx +82 -0
- package/src/ui/src/views/requests/RequestDetails.tsx +70 -0
- package/src/ui/src/views/requests/RequetsTable.tsx +19 -0
- package/src/ui/src/views/requests/columns.tsx +62 -0
- package/src/ui/src/vite-env.d.ts +1 -0
- package/src/ui/tsconfig.app.json +27 -0
- package/src/ui/tsconfig.json +7 -0
- package/src/ui/tsconfig.node.json +25 -0
- package/src/ui/vite.config.ts +9 -0
- package/src/utils/event_emitter.ts +13 -0
- package/src/utils/index.ts +176 -0
- package/src/watchers/index.ts +2 -0
- package/src/watchers/query_watcher.ts +15 -0
- package/src/watchers/request_watcher.ts +27 -0
- package/tests/core/lens.test.ts +89 -0
- package/tests/stores/better_sqlite.test.ts +168 -0
- package/tests/utils/index.test.ts +182 -0
- package/tests/watchers/query_watcher.test.ts +35 -0
- package/tests/watchers/request_watcher.test.ts +59 -0
- package/tsconfig.json +3 -0
- package/tsup.config.ts +15 -0
- 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,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,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,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
|
+
}
|