mongoku 2.0.2 → 2.0.3

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 (151) hide show
  1. package/Dockerfile +27 -0
  2. package/build/client/_app/immutable/chunks/{MHUppGzk.js → BAM9w9EL.js} +1 -1
  3. package/build/client/_app/immutable/chunks/BAM9w9EL.js.br +0 -0
  4. package/build/client/_app/immutable/chunks/BAM9w9EL.js.gz +0 -0
  5. package/build/client/_app/immutable/chunks/{CN-ecO3-.js → BMa204Dm.js} +1 -1
  6. package/build/client/_app/immutable/chunks/BMa204Dm.js.br +0 -0
  7. package/build/client/_app/immutable/chunks/BMa204Dm.js.gz +0 -0
  8. package/build/client/_app/immutable/chunks/{_2kcttvK.js → BdR-m9Ad.js} +1 -1
  9. package/build/client/_app/immutable/chunks/BdR-m9Ad.js.br +0 -0
  10. package/build/client/_app/immutable/chunks/BdR-m9Ad.js.gz +0 -0
  11. package/build/client/_app/immutable/chunks/BzAcxkRZ.js +4 -0
  12. package/build/client/_app/immutable/chunks/BzAcxkRZ.js.br +0 -0
  13. package/build/client/_app/immutable/chunks/BzAcxkRZ.js.gz +0 -0
  14. package/build/client/_app/immutable/chunks/{BFhvhM4X.js → CyQLXPZI.js} +1 -1
  15. package/build/client/_app/immutable/chunks/CyQLXPZI.js.br +0 -0
  16. package/build/client/_app/immutable/chunks/CyQLXPZI.js.gz +0 -0
  17. package/build/client/_app/immutable/chunks/{BdWVCPGW.js → D4VhtiDg.js} +1 -1
  18. package/build/client/_app/immutable/chunks/D4VhtiDg.js.br +0 -0
  19. package/build/client/_app/immutable/chunks/D4VhtiDg.js.gz +0 -0
  20. package/build/client/_app/immutable/chunks/{zXvB9_Mi.js → XYFbSe2V.js} +1 -1
  21. package/build/client/_app/immutable/chunks/XYFbSe2V.js.br +0 -0
  22. package/build/client/_app/immutable/chunks/XYFbSe2V.js.gz +0 -0
  23. package/build/client/_app/immutable/chunks/{CGIdus8b.js → uMNMODvc.js} +1 -1
  24. package/build/client/_app/immutable/chunks/uMNMODvc.js.br +0 -0
  25. package/build/client/_app/immutable/chunks/uMNMODvc.js.gz +0 -0
  26. package/build/client/_app/immutable/entry/{app.hGE78f-O.js → app.9nC_873E.js} +2 -2
  27. package/build/client/_app/immutable/entry/app.9nC_873E.js.br +0 -0
  28. package/build/client/_app/immutable/entry/app.9nC_873E.js.gz +0 -0
  29. package/build/client/_app/immutable/entry/start.Bn88Alw2.js +1 -0
  30. package/build/client/_app/immutable/entry/start.Bn88Alw2.js.br +2 -0
  31. package/build/client/_app/immutable/entry/start.Bn88Alw2.js.gz +0 -0
  32. package/build/client/_app/immutable/nodes/{0.DyBXVtnT.js → 0.COxTCtn2.js} +1 -1
  33. package/build/client/_app/immutable/nodes/0.COxTCtn2.js.br +0 -0
  34. package/build/client/_app/immutable/nodes/0.COxTCtn2.js.gz +0 -0
  35. package/build/client/_app/immutable/nodes/{1.FqB0jq88.js → 1.Bc8yPK_D.js} +1 -1
  36. package/build/client/_app/immutable/nodes/1.Bc8yPK_D.js.br +0 -0
  37. package/build/client/_app/immutable/nodes/1.Bc8yPK_D.js.gz +0 -0
  38. package/build/client/_app/immutable/nodes/{3.Bekn_8hM.js → 3.CI2GcqTf.js} +1 -1
  39. package/build/client/_app/immutable/nodes/3.CI2GcqTf.js.br +0 -0
  40. package/build/client/_app/immutable/nodes/3.CI2GcqTf.js.gz +0 -0
  41. package/build/client/_app/immutable/nodes/{4.DQfaAvJi.js → 4.ChSdW7ac.js} +1 -1
  42. package/build/client/_app/immutable/nodes/4.ChSdW7ac.js.br +0 -0
  43. package/build/client/_app/immutable/nodes/4.ChSdW7ac.js.gz +0 -0
  44. package/build/client/_app/immutable/nodes/{5.B1E6iW2R.js → 5.DaMML2go.js} +1 -1
  45. package/build/client/_app/immutable/nodes/5.DaMML2go.js.br +0 -0
  46. package/build/client/_app/immutable/nodes/5.DaMML2go.js.gz +0 -0
  47. package/build/client/_app/immutable/nodes/{6.28eZQkvz.js → 6.Dcq0qwvO.js} +1 -1
  48. package/build/client/_app/immutable/nodes/6.Dcq0qwvO.js.br +0 -0
  49. package/build/client/_app/immutable/nodes/6.Dcq0qwvO.js.gz +0 -0
  50. package/build/client/_app/immutable/nodes/{7.qpcLWZb7.js → 7.CU-ncPes.js} +1 -1
  51. package/build/client/_app/immutable/nodes/7.CU-ncPes.js.br +0 -0
  52. package/build/client/_app/immutable/nodes/7.CU-ncPes.js.gz +0 -0
  53. package/build/client/_app/version.json +1 -1
  54. package/build/client/_app/version.json.br +0 -0
  55. package/build/client/_app/version.json.gz +0 -0
  56. package/build/server/chunks/{0-m42kIUxj.js → 0-C1NyHW8A.js} +2 -2
  57. package/build/server/chunks/{0-m42kIUxj.js.map → 0-C1NyHW8A.js.map} +1 -1
  58. package/build/server/chunks/{1-uc74UVG3.js → 1-CThf4W5r.js} +2 -2
  59. package/build/server/chunks/{1-uc74UVG3.js.map → 1-CThf4W5r.js.map} +1 -1
  60. package/build/server/chunks/{3-Bi8teWON.js → 3-CJf0NbiV.js} +2 -2
  61. package/build/server/chunks/{3-Bi8teWON.js.map → 3-CJf0NbiV.js.map} +1 -1
  62. package/build/server/chunks/{4-u1WGAtFU.js → 4-Dfbpsagm.js} +2 -2
  63. package/build/server/chunks/{4-u1WGAtFU.js.map → 4-Dfbpsagm.js.map} +1 -1
  64. package/build/server/chunks/{5-BlGdcdjs.js → 5-DLB6GOjf.js} +2 -2
  65. package/build/server/chunks/{5-BlGdcdjs.js.map → 5-DLB6GOjf.js.map} +1 -1
  66. package/build/server/chunks/{6-YCp6xyCU.js → 6-DfCARDKO.js} +2 -2
  67. package/build/server/chunks/{6-YCp6xyCU.js.map → 6-DfCARDKO.js.map} +1 -1
  68. package/build/server/chunks/{7-ieA4k9K_.js → 7-B5o4OymX.js} +2 -2
  69. package/build/server/chunks/{7-ieA4k9K_.js.map → 7-B5o4OymX.js.map} +1 -1
  70. package/build/server/index.js +1 -1
  71. package/build/server/index.js.map +1 -1
  72. package/build/server/manifest.js +8 -8
  73. package/build/server/manifest.js.map +1 -1
  74. package/cli.ts +148 -0
  75. package/dist/cli.js +2 -3
  76. package/ecosystem.config.js +9 -0
  77. package/package.json +10 -2
  78. package/src/api/servers.remote.ts +98 -0
  79. package/src/app.css +228 -0
  80. package/src/app.d.ts +16 -0
  81. package/src/app.html +11 -0
  82. package/src/hooks.server.ts +34 -0
  83. package/src/lib/components/Breadcrumbs.svelte +133 -0
  84. package/src/lib/components/JsonValue.svelte +248 -0
  85. package/src/lib/components/Notifications.svelte +81 -0
  86. package/src/lib/components/Panel.svelte +37 -0
  87. package/src/lib/components/PrettyJson.svelte +187 -0
  88. package/src/lib/components/SearchBox.svelte +160 -0
  89. package/src/lib/components/TooltipTable.svelte +137 -0
  90. package/src/lib/server/HostsManager.ts +105 -0
  91. package/src/lib/server/JsonEncoder.ts +62 -0
  92. package/src/lib/server/mongo.ts +199 -0
  93. package/src/lib/stores/notifications.svelte.ts +45 -0
  94. package/src/lib/types.ts +56 -0
  95. package/src/lib/utils/filters.ts +25 -0
  96. package/src/lib/utils/jsonParser.ts +125 -0
  97. package/src/routes/+layout.server.ts +7 -0
  98. package/src/routes/+layout.svelte +27 -0
  99. package/src/routes/+page.server.ts +6 -0
  100. package/src/routes/servers/+page.server.ts +53 -0
  101. package/src/routes/servers/+page.svelte +196 -0
  102. package/src/routes/servers/[server]/databases/+page.server.ts +47 -0
  103. package/src/routes/servers/[server]/databases/+page.svelte +88 -0
  104. package/src/routes/servers/[server]/databases/[database]/collections/+page.server.ts +21 -0
  105. package/src/routes/servers/[server]/databases/[database]/collections/+page.svelte +110 -0
  106. package/src/routes/servers/[server]/databases/[database]/collections/[collection]/+page.server.ts +106 -0
  107. package/src/routes/servers/[server]/databases/[database]/collections/[collection]/+page.svelte +174 -0
  108. package/src/routes/servers/[server]/databases/[database]/collections/[collection]/documents/[document]/+page.server.ts +25 -0
  109. package/src/routes/servers/[server]/databases/[database]/collections/[collection]/documents/[document]/+page.svelte +90 -0
  110. package/src/tests/api/readonly.test.ts +89 -0
  111. package/src/tests/setup.ts +19 -0
  112. package/svelte.config.js +28 -0
  113. package/tsconfig.cli.json +15 -0
  114. package/tsconfig.json +19 -0
  115. package/vite.config.ts +7 -0
  116. package/build/client/_app/immutable/chunks/BFhvhM4X.js.br +0 -0
  117. package/build/client/_app/immutable/chunks/BFhvhM4X.js.gz +0 -0
  118. package/build/client/_app/immutable/chunks/BdWVCPGW.js.br +0 -0
  119. package/build/client/_app/immutable/chunks/BdWVCPGW.js.gz +0 -0
  120. package/build/client/_app/immutable/chunks/CGIdus8b.js.br +0 -0
  121. package/build/client/_app/immutable/chunks/CGIdus8b.js.gz +0 -0
  122. package/build/client/_app/immutable/chunks/CN-ecO3-.js.br +0 -0
  123. package/build/client/_app/immutable/chunks/CN-ecO3-.js.gz +0 -0
  124. package/build/client/_app/immutable/chunks/DB3PPjLu.js +0 -4
  125. package/build/client/_app/immutable/chunks/DB3PPjLu.js.br +0 -0
  126. package/build/client/_app/immutable/chunks/DB3PPjLu.js.gz +0 -0
  127. package/build/client/_app/immutable/chunks/MHUppGzk.js.br +0 -0
  128. package/build/client/_app/immutable/chunks/MHUppGzk.js.gz +0 -0
  129. package/build/client/_app/immutable/chunks/_2kcttvK.js.br +0 -0
  130. package/build/client/_app/immutable/chunks/_2kcttvK.js.gz +0 -0
  131. package/build/client/_app/immutable/chunks/zXvB9_Mi.js.br +0 -0
  132. package/build/client/_app/immutable/chunks/zXvB9_Mi.js.gz +0 -0
  133. package/build/client/_app/immutable/entry/app.hGE78f-O.js.br +0 -0
  134. package/build/client/_app/immutable/entry/app.hGE78f-O.js.gz +0 -0
  135. package/build/client/_app/immutable/entry/start._GE1Zd3d.js +0 -1
  136. package/build/client/_app/immutable/entry/start._GE1Zd3d.js.br +0 -2
  137. package/build/client/_app/immutable/entry/start._GE1Zd3d.js.gz +0 -0
  138. package/build/client/_app/immutable/nodes/0.DyBXVtnT.js.br +0 -0
  139. package/build/client/_app/immutable/nodes/0.DyBXVtnT.js.gz +0 -0
  140. package/build/client/_app/immutable/nodes/1.FqB0jq88.js.br +0 -2
  141. package/build/client/_app/immutable/nodes/1.FqB0jq88.js.gz +0 -0
  142. package/build/client/_app/immutable/nodes/3.Bekn_8hM.js.br +0 -0
  143. package/build/client/_app/immutable/nodes/3.Bekn_8hM.js.gz +0 -0
  144. package/build/client/_app/immutable/nodes/4.DQfaAvJi.js.br +0 -0
  145. package/build/client/_app/immutable/nodes/4.DQfaAvJi.js.gz +0 -0
  146. package/build/client/_app/immutable/nodes/5.B1E6iW2R.js.br +0 -0
  147. package/build/client/_app/immutable/nodes/5.B1E6iW2R.js.gz +0 -0
  148. package/build/client/_app/immutable/nodes/6.28eZQkvz.js.br +0 -0
  149. package/build/client/_app/immutable/nodes/6.28eZQkvz.js.gz +0 -0
  150. package/build/client/_app/immutable/nodes/7.qpcLWZb7.js.br +0 -0
  151. package/build/client/_app/immutable/nodes/7.qpcLWZb7.js.gz +0 -0
@@ -0,0 +1,160 @@
1
+ <script lang="ts">
2
+ import { goto } from "$app/navigation";
3
+ import { resolve } from "$app/paths";
4
+ import { page } from "$app/state";
5
+ import type { SearchParams } from "$lib/types";
6
+
7
+ interface Props {
8
+ params: SearchParams;
9
+ }
10
+
11
+ let { params = $bindable() }: Props = $props();
12
+
13
+ // Show optional fields - start with all hidden
14
+ let showOptionalFields = $state(
15
+ params.sort !== "{}" || params.project !== "{}" || params.skip !== 0 || params.limit !== 20,
16
+ );
17
+
18
+ let counter = $state(Math.random());
19
+
20
+ let queryInput: HTMLInputElement | undefined;
21
+
22
+ $effect(() => {
23
+ if (queryInput) {
24
+ queryInput.setSelectionRange(1, 1 /* queryInput.value.length - 1 */);
25
+ queryInput.focus();
26
+ }
27
+ });
28
+
29
+ async function submit(event: SubmitEvent) {
30
+ event.preventDefault();
31
+ counter++;
32
+ const formData = new FormData(form);
33
+ await goto(
34
+ resolve(
35
+ (page.url.pathname +
36
+ "?" +
37
+ [...formData.entries()]
38
+ .map((e) => encodeURIComponent(e[0]) + "=" + encodeURIComponent(e[1] as string))
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ .join("&")) as any,
41
+ ),
42
+ {
43
+ keepFocus: true,
44
+ },
45
+ );
46
+ }
47
+
48
+ let form: HTMLFormElement | undefined;
49
+ </script>
50
+
51
+ <form class="flex items-stretch w-full" method="GET" action="?" onsubmit={submit} bind:this={form}>
52
+ <!-- Parameters group -->
53
+ <div class="flex-grow">
54
+ <!-- Query input (always shown) -->
55
+ <div class="flex items-stretch w-full h-10">
56
+ <div
57
+ class="min-w-[100px] flex justify-center items-center border border-[var(--color-4)] {!showOptionalFields
58
+ ? 'border-b rounded-bl-md'
59
+ : 'border-b-0'} bg-[var(--color-1)] rounded-tl-md"
60
+ >
61
+ Query:
62
+ </div>
63
+ <input
64
+ type="text"
65
+ bind:this={queryInput}
66
+ bind:value={params.query}
67
+ placeholder={"{}"}
68
+ name="query"
69
+ class="flex-grow border-0 bg-[var(--color-3)] pl-2.5 font-mono"
70
+ />
71
+ </div>
72
+
73
+ <input type="hidden" value={counter} name="v" />
74
+ <!-- Sort input -->
75
+ {#if showOptionalFields}
76
+ <div class="flex items-stretch w-full h-10">
77
+ <div
78
+ class="min-w-[100px] flex justify-center items-center border border-[var(--color-4)] border-b-0 bg-[var(--color-1)]"
79
+ >
80
+ Sort:
81
+ </div>
82
+ <input
83
+ type="text"
84
+ bind:value={params.sort}
85
+ name="sort"
86
+ placeholder={"{}"}
87
+ class="flex-grow border-0 border-t border-[var(--color-4)] bg-[var(--color-3)] pl-2.5 font-mono"
88
+ />
89
+ </div>
90
+
91
+ <!-- Skip input -->
92
+ <div class="flex items-stretch w-full h-10">
93
+ <div
94
+ class="min-w-[100px] flex justify-center items-center border border-[var(--color-4)] border-b-0 bg-[var(--color-1)]"
95
+ >
96
+ Skip:
97
+ </div>
98
+ <input
99
+ type="number"
100
+ bind:value={params.skip}
101
+ name="skip"
102
+ min="0"
103
+ class="flex-grow border-0 border-t border-[var(--color-4)] bg-[var(--color-3)] pl-2.5 font-mono"
104
+ />
105
+ </div>
106
+
107
+ <!-- Limit input -->
108
+ <div class="flex items-stretch w-full h-10">
109
+ <div
110
+ class="min-w-[100px] flex justify-center items-center border border-[var(--color-4)] border-b-0 bg-[var(--color-1)]"
111
+ >
112
+ Limit:
113
+ </div>
114
+ <input
115
+ type="number"
116
+ bind:value={params.limit}
117
+ name="limit"
118
+ min="1"
119
+ class="flex-grow border-0 border-t border-[var(--color-4)] bg-[var(--color-3)] pl-2.5 font-mono"
120
+ />
121
+ </div>
122
+
123
+ <!-- Project input -->
124
+ <div class="flex items-stretch w-full h-10">
125
+ <div
126
+ class="min-w-[100px] flex justify-center items-center border border-[var(--color-4)] border-b bg-[var(--color-1)] rounded-bl-md"
127
+ >
128
+ Project:
129
+ </div>
130
+ <input
131
+ type="text"
132
+ bind:value={params.project}
133
+ name="project"
134
+ placeholder={"{}"}
135
+ class="flex-grow border-0 border-t border-[var(--color-4)] bg-[var(--color-3)] pl-2.5 font-mono"
136
+ />
137
+ </div>
138
+ {/if}
139
+ </div>
140
+
141
+ <!-- Toggle optional fields button -->
142
+ <button
143
+ class="btn btn-default !w-12 !rounded-none !border-r-0 text-2xl leading-none font-bold !py-1.5"
144
+ type="button"
145
+ onclick={() => {
146
+ showOptionalFields = !showOptionalFields;
147
+ }}
148
+ >
149
+ {showOptionalFields ? "−" : "+"}
150
+ </button>
151
+
152
+ <!-- Search button -->
153
+ <button class="btn btn-success !rounded-l-none !rounded-r-md font-bold !py-1.5" type="submit"> GO! </button>
154
+ </form>
155
+
156
+ <style lang="postcss">
157
+ input {
158
+ border-radius: 0;
159
+ }
160
+ </style>
@@ -0,0 +1,137 @@
1
+ <script lang="ts">
2
+ import { tick, type Snippet } from "svelte";
3
+
4
+ interface TableColumn {
5
+ header: string;
6
+ key: string;
7
+ align?: "left" | "right" | "center";
8
+ }
9
+
10
+ interface TableRow {
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ let {
15
+ columns,
16
+ rows,
17
+ children,
18
+ hideHeader,
19
+ }: {
20
+ columns: TableColumn[];
21
+ rows: TableRow[];
22
+ children: Snippet;
23
+ hideHeader?: boolean;
24
+ } = $props();
25
+
26
+ let showTooltip = $state(false);
27
+ let tooltipElement = $state<HTMLDivElement>();
28
+ let containerElement = $state<HTMLDivElement>();
29
+
30
+ let tooltipPosition = $state({
31
+ left: "",
32
+ right: "",
33
+ top: "",
34
+ bottom: "",
35
+ marginTop: "",
36
+ marginBottom: "",
37
+ });
38
+
39
+ function handleMouseEnter() {
40
+ showTooltip = true;
41
+ }
42
+
43
+ function handleMouseLeave() {
44
+ showTooltip = false;
45
+ }
46
+
47
+ $effect(() => {
48
+ if (showTooltip && tooltipElement && containerElement) {
49
+ tick().then(() => {
50
+ if (!containerElement || !tooltipElement) return;
51
+
52
+ const tooltipRect = tooltipElement.getBoundingClientRect();
53
+ const viewportWidth = window.innerWidth;
54
+ const viewportHeight = window.innerHeight;
55
+
56
+ // Reset positioning
57
+ tooltipPosition = {
58
+ left: "",
59
+ right: "",
60
+ top: "",
61
+ bottom: "",
62
+ marginTop: "",
63
+ marginBottom: "",
64
+ };
65
+
66
+ // Position horizontally
67
+ if (tooltipRect.right > viewportWidth) {
68
+ tooltipPosition.right = "0";
69
+ } else {
70
+ tooltipPosition.left = "0";
71
+ }
72
+
73
+ // Position vertically
74
+ if (tooltipRect.bottom > viewportHeight) {
75
+ tooltipPosition.bottom = "100%";
76
+ tooltipPosition.marginBottom = "5px";
77
+ } else {
78
+ tooltipPosition.top = "100%";
79
+ tooltipPosition.marginTop = "5px";
80
+ }
81
+ });
82
+ }
83
+ });
84
+ </script>
85
+
86
+ <div class="relative inline-block" bind:this={containerElement}>
87
+ <button class="dotted text-center" onmouseenter={handleMouseEnter} onmouseleave={handleMouseLeave}>
88
+ {@render children?.()}
89
+ </button>
90
+ {#if showTooltip}
91
+ <div
92
+ class="absolute bg-[var(--color-2)] border border-[var(--color-3)] rounded p-0 z-[1000] whitespace-nowrap shadow-lg"
93
+ bind:this={tooltipElement}
94
+ style:left={tooltipPosition.left}
95
+ style:right={tooltipPosition.right}
96
+ style:top={tooltipPosition.top}
97
+ style:bottom={tooltipPosition.bottom}
98
+ style:margin-top={tooltipPosition.marginTop}
99
+ style:margin-bottom={tooltipPosition.marginBottom}
100
+ >
101
+ <table class="w-full border-collapse text-base font-medium">
102
+ {#if !hideHeader}
103
+ <thead>
104
+ <tr>
105
+ {#each columns as column, index (index)}
106
+ <th
107
+ class="px-2 py-1 border-b border-[var(--color-3)] bg-[var(--color-4)] font-bold"
108
+ class:text-left={column.align === "left" || !column.align}
109
+ class:text-right={column.align === "right"}
110
+ class:text-center={column.align === "center"}
111
+ >
112
+ {column.header}
113
+ </th>
114
+ {/each}
115
+ </tr>
116
+ </thead>
117
+ {/if}
118
+ <tbody>
119
+ {#each rows as row, index (index)}
120
+ <tr>
121
+ {#each columns as column, index (index)}
122
+ <td
123
+ class="px-2 py-1 border-b border-[var(--color-3)]"
124
+ class:text-left={column.align === "left" || !column.align}
125
+ class:text-right={column.align === "right"}
126
+ class:text-center={column.align === "center"}
127
+ >
128
+ {row[column.key]}
129
+ </td>
130
+ {/each}
131
+ </tr>
132
+ {/each}
133
+ </tbody>
134
+ </table>
135
+ </div>
136
+ {/if}
137
+ </div>
@@ -0,0 +1,105 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+
5
+ export interface Host {
6
+ path: string;
7
+ _id: string;
8
+ }
9
+
10
+ const DEFAULT_HOSTS = process.env.MONGOKU_DEFAULT_HOST
11
+ ? process.env.MONGOKU_DEFAULT_HOST.split(";")
12
+ : ["localhost:27017"];
13
+ const DATABASE_FILE = process.env.MONGOKU_DATABASE_FILE || path.join(os.homedir(), ".mongoku.db");
14
+
15
+ export class HostsManager {
16
+ private _hosts: Map<string, string> = new Map(); // path -> _id
17
+
18
+ async load() {
19
+ let first = false;
20
+ try {
21
+ await fs.promises.stat(DATABASE_FILE);
22
+ } catch {
23
+ first = true;
24
+ }
25
+
26
+ if (!first) {
27
+ await this._loadFromFile();
28
+ }
29
+
30
+ if (first || this._hosts.size === 0) {
31
+ // Initialize with default hosts
32
+ for (const hostname of DEFAULT_HOSTS) {
33
+ this._hosts.set(hostname, this._generateId());
34
+ }
35
+ await this._saveToFile();
36
+ }
37
+ }
38
+
39
+ private async _loadFromFile(): Promise<void> {
40
+ const content = await fs.promises.readFile(DATABASE_FILE, "utf8");
41
+ const lines = content
42
+ .trim()
43
+ .split("\n")
44
+ .filter((line) => line.trim());
45
+
46
+ const newHosts = new Map<string, string>();
47
+ for (const line of lines) {
48
+ const host = JSON.parse(line);
49
+ if (host && typeof host.path === "string") {
50
+ // Use existing _id if available, generate new one if not
51
+ const id = host._id || this._generateId();
52
+ newHosts.set(host.path, id);
53
+ }
54
+ }
55
+ this._hosts = newHosts;
56
+ }
57
+
58
+ private async _saveToFile(): Promise<void> {
59
+ const lines = Array.from(this._hosts).map(([hostPath, id]) => JSON.stringify({ path: hostPath, _id: id }));
60
+ await fs.promises.writeFile(DATABASE_FILE, lines.join("\n") + "\n", "utf8");
61
+ }
62
+
63
+ private _generateId(): string {
64
+ // Generate a NeDB-compatible ID (16 characters, alphanumeric)
65
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
66
+ let result = "";
67
+ for (let i = 0; i < 16; i++) {
68
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
69
+ }
70
+ return result;
71
+ }
72
+
73
+ async getHosts(): Promise<Host[]> {
74
+ return Array.from(this._hosts).map(([hostPath, id]) => ({ path: hostPath, _id: id }));
75
+ }
76
+
77
+ async add(hostPath: string): Promise<string> {
78
+ // Use existing ID if host already exists, generate new one if not
79
+ let id = this._hosts.get(hostPath);
80
+ if (!id) {
81
+ id = this._generateId();
82
+ this._hosts.set(hostPath, id);
83
+ }
84
+ await this._saveToFile();
85
+ return id;
86
+ }
87
+
88
+ async remove(hostPath: string): Promise<void> {
89
+ // Remove exact matches and regex pattern matches
90
+ const toRemove = Array.from(this._hosts.keys()).filter((existingPath) => {
91
+ try {
92
+ const regex = new RegExp(hostPath);
93
+ return existingPath === hostPath || regex.test(existingPath);
94
+ } catch {
95
+ // If hostPath is not a valid regex, just do exact match
96
+ return existingPath === hostPath;
97
+ }
98
+ });
99
+
100
+ for (const host of toRemove) {
101
+ this._hosts.delete(host);
102
+ }
103
+ await this._saveToFile();
104
+ }
105
+ }
@@ -0,0 +1,62 @@
1
+ import { ObjectId } from "mongodb";
2
+
3
+ export default class JsonEncoder {
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ static encode(obj: any): any {
6
+ if (obj instanceof ObjectId) {
7
+ return {
8
+ $type: "ObjectId",
9
+ $value: obj.toHexString(),
10
+ $date: obj.getTimestamp().getTime(),
11
+ };
12
+ }
13
+ if (obj instanceof Date) {
14
+ return {
15
+ $type: "Date",
16
+ $value: obj.toISOString(),
17
+ };
18
+ }
19
+ if (obj instanceof RegExp) {
20
+ return {
21
+ $type: "RegExp",
22
+ $value: {
23
+ $pattern: obj.source,
24
+ $flags: obj.flags,
25
+ },
26
+ };
27
+ }
28
+ if (Array.isArray(obj)) {
29
+ return [...obj.map(JsonEncoder.encode)];
30
+ }
31
+ if (obj && typeof obj === "object") {
32
+ for (const [key, value] of Object.entries(obj)) {
33
+ obj[key] = JsonEncoder.encode(value);
34
+ }
35
+ }
36
+
37
+ return obj;
38
+ }
39
+
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ static decode(obj: any): any {
42
+ if (obj && obj.$type === "ObjectId") {
43
+ return ObjectId.createFromHexString(obj.$value);
44
+ }
45
+ if (obj && obj.$type === "Date") {
46
+ return new Date(obj.$value);
47
+ }
48
+ if (obj && obj.$type === "RegExp") {
49
+ return new RegExp(obj.$value.$pattern, obj.$value.$flags);
50
+ }
51
+ if (Array.isArray(obj)) {
52
+ return [...obj.map(JsonEncoder.decode)];
53
+ }
54
+ if (obj && typeof obj === "object") {
55
+ for (const [key, value] of Object.entries(obj)) {
56
+ obj[key] = JsonEncoder.decode(value);
57
+ }
58
+ }
59
+
60
+ return obj;
61
+ }
62
+ }
@@ -0,0 +1,199 @@
1
+ import type { CollectionJSON } from "$lib/types";
2
+ import { MongoClient, type Collection } from "mongodb";
3
+ import { URL } from "url";
4
+ import { HostsManager } from "./HostsManager";
5
+
6
+ export async function getCollectionJson(
7
+ collection: Collection,
8
+ type?: "view" | "timeseries" | "collection" | string,
9
+ ): Promise<CollectionJSON> {
10
+ const stats = {
11
+ size: 0,
12
+ count: 0,
13
+ avgObjSize: 0,
14
+ storageSize: 0,
15
+ capped: false,
16
+ nindexes: 0,
17
+ totalIndexSize: 0,
18
+ indexSizes: {},
19
+ };
20
+
21
+ if (type !== "view") {
22
+ const agg = (await collection
23
+ .aggregate([
24
+ {
25
+ $collStats: {
26
+ storageStats: {},
27
+ count: {},
28
+ },
29
+ },
30
+ ])
31
+ .next()
32
+ .catch(() => null)) as {
33
+ storageStats: {
34
+ size: number;
35
+ count: number;
36
+ storageSize: number;
37
+ capped: boolean;
38
+ nindexes: number;
39
+ totalIndexSize: number;
40
+ indexSizes: Record<string, number>;
41
+ };
42
+ } | null;
43
+
44
+ if (agg) {
45
+ stats.size = agg.storageStats.size;
46
+ stats.count = agg.storageStats.count;
47
+ stats.avgObjSize = Math.round(agg.storageStats.size / agg.storageStats.count);
48
+ stats.storageSize = agg.storageStats.storageSize;
49
+ stats.capped = agg.storageStats.capped;
50
+ stats.nindexes = agg.storageStats.nindexes;
51
+ stats.totalIndexSize = agg.storageStats.totalIndexSize;
52
+ stats.indexSizes = agg.storageStats.indexSizes;
53
+ }
54
+ }
55
+
56
+ // Get index definitions
57
+ let indexes: Array<{ name: string; key?: Record<string, number>; size: number }> = [];
58
+ if (type !== "view") {
59
+ try {
60
+ const indexList = await collection.listIndexes().toArray();
61
+ indexes = indexList.map((index: { name: string; key: Record<string, number> }) => ({
62
+ name: index.name,
63
+ key: index.key,
64
+ size: (stats.indexSizes as Record<string, number>)[index.name] || 0,
65
+ }));
66
+ } catch {
67
+ // If we can't get index details, fall back to indexSizes
68
+ indexes = Object.entries(stats.indexSizes).map(([name, size]) => ({
69
+ name,
70
+ size: size as number,
71
+ }));
72
+ }
73
+ }
74
+
75
+ return {
76
+ name: collection.collectionName,
77
+ size: (stats.storageSize ?? 0) + (stats.totalIndexSize ?? 0),
78
+ dataSize: stats.size,
79
+ count: stats.count,
80
+ avgObjSize: stats.avgObjSize ?? 0,
81
+ storageSize: stats.storageSize ?? 0,
82
+ capped: stats.capped,
83
+ nIndexes: stats.nindexes,
84
+ totalIndexSize: stats.totalIndexSize ?? 0,
85
+ indexSizes: stats.indexSizes,
86
+ indexes,
87
+ };
88
+ }
89
+
90
+ class MongoConnections {
91
+ /**
92
+ * Todo: better system where we can have mutiple servers with same hostname, and labels for each server that
93
+ * would be displayed in the UI instead of the hostname.
94
+ */
95
+ private clients: Map<string, MongoClient> = new Map();
96
+ private clientIds: Map<string, string> = new Map(); // hostname -> _id
97
+ private hostsManager: HostsManager;
98
+ private countTimeout = parseInt(process.env.MONGOKU_COUNT_TIMEOUT!, 10) || 5000;
99
+
100
+ constructor() {
101
+ this.hostsManager = new HostsManager();
102
+ }
103
+
104
+ async initialize() {
105
+ await this.hostsManager.load();
106
+ const hosts = await this.hostsManager.getHosts();
107
+
108
+ // Create MongoClient instances without connecting (lazy connection)
109
+ for (const host of hosts) {
110
+ const urlStr = host.path.startsWith("mongodb") ? host.path : `mongodb://${host.path}`;
111
+ try {
112
+ const url = new URL(urlStr);
113
+ const hostname = url.host || host.path;
114
+
115
+ if (!this.clients.has(hostname)) {
116
+ const client = new MongoClient(urlStr);
117
+ this.clients.set(hostname, client);
118
+ this.clientIds.set(hostname, host._id);
119
+ }
120
+ } catch (err) {
121
+ console.error(`Failed to parse URL for host ${host.path}:`, err);
122
+ }
123
+ }
124
+ }
125
+
126
+ getClient(name: string): MongoClient {
127
+ const client = this.clients.get(name) || this.clients.get(`${name}:27017`);
128
+ if (!client) {
129
+ throw new Error("Server does not exist");
130
+ }
131
+ return client;
132
+ }
133
+
134
+ listClients(): Array<{ name: string; _id: string; client: MongoClient }> {
135
+ return Array.from(this.clients.entries())
136
+ .filter(([, client]) => client instanceof MongoClient)
137
+ .map(([name, client]) => ({
138
+ name,
139
+ _id: this.clientIds.get(name) || "",
140
+ client: client as MongoClient,
141
+ }));
142
+ }
143
+
144
+ getCollection(serverName: string, databaseName: string, collectionName: string) {
145
+ const client = this.getClient(serverName);
146
+ const db = client.db(databaseName);
147
+ return db.collection(collectionName);
148
+ }
149
+
150
+ getCountTimeout() {
151
+ return this.countTimeout;
152
+ }
153
+
154
+ async addServer(hostPath: string) {
155
+ const id = await this.hostsManager.add(hostPath);
156
+
157
+ // Add the new server client
158
+ const urlStr = hostPath.startsWith("mongodb") ? hostPath : `mongodb://${hostPath}`;
159
+ try {
160
+ const url = new URL(urlStr);
161
+ const hostname = url.host || hostPath;
162
+
163
+ if (!this.clients.has(hostname)) {
164
+ const client = new MongoClient(urlStr);
165
+ this.clients.set(hostname, client);
166
+ this.clientIds.set(hostname, id);
167
+ }
168
+ } catch (err) {
169
+ console.error(`Failed to parse URL for host ${hostPath}:`, err);
170
+ throw err;
171
+ }
172
+ }
173
+
174
+ async removeServer(name: string) {
175
+ await this.hostsManager.remove(name);
176
+ this.clients.delete(name);
177
+ this.clientIds.delete(name);
178
+ }
179
+ }
180
+
181
+ // Singleton instance
182
+ let mongoConnections: MongoConnections | null = null;
183
+ let initPromise: Promise<MongoConnections> | null = null;
184
+
185
+ export async function getMongo(): Promise<MongoConnections> {
186
+ if (!mongoConnections) {
187
+ if (!initPromise) {
188
+ initPromise = (async () => {
189
+ mongoConnections = new MongoConnections();
190
+ await mongoConnections.initialize();
191
+ return mongoConnections;
192
+ })();
193
+ }
194
+ return initPromise;
195
+ }
196
+ return mongoConnections;
197
+ }
198
+
199
+ export { mongoConnections };
@@ -0,0 +1,45 @@
1
+ import z from "zod";
2
+
3
+ interface Notification {
4
+ id: number;
5
+ message: string;
6
+ type: "success" | "error" | "info";
7
+ }
8
+
9
+ let notifications = $state<Notification[]>([]);
10
+ let nextId = 1;
11
+
12
+ export const notificationStore = {
13
+ get items() {
14
+ return notifications;
15
+ },
16
+
17
+ notify(message: string, type: "success" | "error" | "info" = "info") {
18
+ const id = nextId++;
19
+ notifications = [...notifications, { id, message, type }];
20
+
21
+ setTimeout(() => {
22
+ this.remove(id);
23
+ }, 5000);
24
+ },
25
+
26
+ async notifyError(message: string | unknown, fallbackMessage?: string) {
27
+ const finalMessage =
28
+ typeof message === "string"
29
+ ? message
30
+ : (z.object({ message: z.string() }).safeParse(message).data?.message ??
31
+ z.object({ body: z.object({ message: z.string() }), status: z.number() }).safeParse(message).data?.body
32
+ ?.message ??
33
+ fallbackMessage ??
34
+ "An unexpected error occurred");
35
+ this.notify(finalMessage, "error");
36
+ },
37
+
38
+ notifySuccess(message: string) {
39
+ this.notify(message, "success");
40
+ },
41
+
42
+ remove(id: number) {
43
+ notifications = notifications.filter((n) => n.id !== id);
44
+ },
45
+ };