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.
- package/Dockerfile +27 -0
- package/build/client/_app/immutable/chunks/{MHUppGzk.js → BAM9w9EL.js} +1 -1
- package/build/client/_app/immutable/chunks/BAM9w9EL.js.br +0 -0
- package/build/client/_app/immutable/chunks/BAM9w9EL.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CN-ecO3-.js → BMa204Dm.js} +1 -1
- package/build/client/_app/immutable/chunks/BMa204Dm.js.br +0 -0
- package/build/client/_app/immutable/chunks/BMa204Dm.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{_2kcttvK.js → BdR-m9Ad.js} +1 -1
- package/build/client/_app/immutable/chunks/BdR-m9Ad.js.br +0 -0
- package/build/client/_app/immutable/chunks/BdR-m9Ad.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BzAcxkRZ.js +4 -0
- package/build/client/_app/immutable/chunks/BzAcxkRZ.js.br +0 -0
- package/build/client/_app/immutable/chunks/BzAcxkRZ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BFhvhM4X.js → CyQLXPZI.js} +1 -1
- package/build/client/_app/immutable/chunks/CyQLXPZI.js.br +0 -0
- package/build/client/_app/immutable/chunks/CyQLXPZI.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BdWVCPGW.js → D4VhtiDg.js} +1 -1
- package/build/client/_app/immutable/chunks/D4VhtiDg.js.br +0 -0
- package/build/client/_app/immutable/chunks/D4VhtiDg.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{zXvB9_Mi.js → XYFbSe2V.js} +1 -1
- package/build/client/_app/immutable/chunks/XYFbSe2V.js.br +0 -0
- package/build/client/_app/immutable/chunks/XYFbSe2V.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{CGIdus8b.js → uMNMODvc.js} +1 -1
- package/build/client/_app/immutable/chunks/uMNMODvc.js.br +0 -0
- package/build/client/_app/immutable/chunks/uMNMODvc.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.hGE78f-O.js → app.9nC_873E.js} +2 -2
- package/build/client/_app/immutable/entry/app.9nC_873E.js.br +0 -0
- package/build/client/_app/immutable/entry/app.9nC_873E.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.Bn88Alw2.js +1 -0
- package/build/client/_app/immutable/entry/start.Bn88Alw2.js.br +2 -0
- package/build/client/_app/immutable/entry/start.Bn88Alw2.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.DyBXVtnT.js → 0.COxTCtn2.js} +1 -1
- package/build/client/_app/immutable/nodes/0.COxTCtn2.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.COxTCtn2.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.FqB0jq88.js → 1.Bc8yPK_D.js} +1 -1
- package/build/client/_app/immutable/nodes/1.Bc8yPK_D.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.Bc8yPK_D.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.Bekn_8hM.js → 3.CI2GcqTf.js} +1 -1
- package/build/client/_app/immutable/nodes/3.CI2GcqTf.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.CI2GcqTf.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{4.DQfaAvJi.js → 4.ChSdW7ac.js} +1 -1
- package/build/client/_app/immutable/nodes/4.ChSdW7ac.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.ChSdW7ac.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{5.B1E6iW2R.js → 5.DaMML2go.js} +1 -1
- package/build/client/_app/immutable/nodes/5.DaMML2go.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.DaMML2go.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.28eZQkvz.js → 6.Dcq0qwvO.js} +1 -1
- package/build/client/_app/immutable/nodes/6.Dcq0qwvO.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.Dcq0qwvO.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.qpcLWZb7.js → 7.CU-ncPes.js} +1 -1
- package/build/client/_app/immutable/nodes/7.CU-ncPes.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.CU-ncPes.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-m42kIUxj.js → 0-C1NyHW8A.js} +2 -2
- package/build/server/chunks/{0-m42kIUxj.js.map → 0-C1NyHW8A.js.map} +1 -1
- package/build/server/chunks/{1-uc74UVG3.js → 1-CThf4W5r.js} +2 -2
- package/build/server/chunks/{1-uc74UVG3.js.map → 1-CThf4W5r.js.map} +1 -1
- package/build/server/chunks/{3-Bi8teWON.js → 3-CJf0NbiV.js} +2 -2
- package/build/server/chunks/{3-Bi8teWON.js.map → 3-CJf0NbiV.js.map} +1 -1
- package/build/server/chunks/{4-u1WGAtFU.js → 4-Dfbpsagm.js} +2 -2
- package/build/server/chunks/{4-u1WGAtFU.js.map → 4-Dfbpsagm.js.map} +1 -1
- package/build/server/chunks/{5-BlGdcdjs.js → 5-DLB6GOjf.js} +2 -2
- package/build/server/chunks/{5-BlGdcdjs.js.map → 5-DLB6GOjf.js.map} +1 -1
- package/build/server/chunks/{6-YCp6xyCU.js → 6-DfCARDKO.js} +2 -2
- package/build/server/chunks/{6-YCp6xyCU.js.map → 6-DfCARDKO.js.map} +1 -1
- package/build/server/chunks/{7-ieA4k9K_.js → 7-B5o4OymX.js} +2 -2
- package/build/server/chunks/{7-ieA4k9K_.js.map → 7-B5o4OymX.js.map} +1 -1
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +8 -8
- package/build/server/manifest.js.map +1 -1
- package/cli.ts +148 -0
- package/dist/cli.js +2 -3
- package/ecosystem.config.js +9 -0
- package/package.json +10 -2
- package/src/api/servers.remote.ts +98 -0
- package/src/app.css +228 -0
- package/src/app.d.ts +16 -0
- package/src/app.html +11 -0
- package/src/hooks.server.ts +34 -0
- package/src/lib/components/Breadcrumbs.svelte +133 -0
- package/src/lib/components/JsonValue.svelte +248 -0
- package/src/lib/components/Notifications.svelte +81 -0
- package/src/lib/components/Panel.svelte +37 -0
- package/src/lib/components/PrettyJson.svelte +187 -0
- package/src/lib/components/SearchBox.svelte +160 -0
- package/src/lib/components/TooltipTable.svelte +137 -0
- package/src/lib/server/HostsManager.ts +105 -0
- package/src/lib/server/JsonEncoder.ts +62 -0
- package/src/lib/server/mongo.ts +199 -0
- package/src/lib/stores/notifications.svelte.ts +45 -0
- package/src/lib/types.ts +56 -0
- package/src/lib/utils/filters.ts +25 -0
- package/src/lib/utils/jsonParser.ts +125 -0
- package/src/routes/+layout.server.ts +7 -0
- package/src/routes/+layout.svelte +27 -0
- package/src/routes/+page.server.ts +6 -0
- package/src/routes/servers/+page.server.ts +53 -0
- package/src/routes/servers/+page.svelte +196 -0
- package/src/routes/servers/[server]/databases/+page.server.ts +47 -0
- package/src/routes/servers/[server]/databases/+page.svelte +88 -0
- package/src/routes/servers/[server]/databases/[database]/collections/+page.server.ts +21 -0
- package/src/routes/servers/[server]/databases/[database]/collections/+page.svelte +110 -0
- package/src/routes/servers/[server]/databases/[database]/collections/[collection]/+page.server.ts +106 -0
- package/src/routes/servers/[server]/databases/[database]/collections/[collection]/+page.svelte +174 -0
- package/src/routes/servers/[server]/databases/[database]/collections/[collection]/documents/[document]/+page.server.ts +25 -0
- package/src/routes/servers/[server]/databases/[database]/collections/[collection]/documents/[document]/+page.svelte +90 -0
- package/src/tests/api/readonly.test.ts +89 -0
- package/src/tests/setup.ts +19 -0
- package/svelte.config.js +28 -0
- package/tsconfig.cli.json +15 -0
- package/tsconfig.json +19 -0
- package/vite.config.ts +7 -0
- package/build/client/_app/immutable/chunks/BFhvhM4X.js.br +0 -0
- package/build/client/_app/immutable/chunks/BFhvhM4X.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BdWVCPGW.js.br +0 -0
- package/build/client/_app/immutable/chunks/BdWVCPGW.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CGIdus8b.js.br +0 -0
- package/build/client/_app/immutable/chunks/CGIdus8b.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CN-ecO3-.js.br +0 -0
- package/build/client/_app/immutable/chunks/CN-ecO3-.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DB3PPjLu.js +0 -4
- package/build/client/_app/immutable/chunks/DB3PPjLu.js.br +0 -0
- package/build/client/_app/immutable/chunks/DB3PPjLu.js.gz +0 -0
- package/build/client/_app/immutable/chunks/MHUppGzk.js.br +0 -0
- package/build/client/_app/immutable/chunks/MHUppGzk.js.gz +0 -0
- package/build/client/_app/immutable/chunks/_2kcttvK.js.br +0 -0
- package/build/client/_app/immutable/chunks/_2kcttvK.js.gz +0 -0
- package/build/client/_app/immutable/chunks/zXvB9_Mi.js.br +0 -0
- package/build/client/_app/immutable/chunks/zXvB9_Mi.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.hGE78f-O.js.br +0 -0
- package/build/client/_app/immutable/entry/app.hGE78f-O.js.gz +0 -0
- package/build/client/_app/immutable/entry/start._GE1Zd3d.js +0 -1
- package/build/client/_app/immutable/entry/start._GE1Zd3d.js.br +0 -2
- package/build/client/_app/immutable/entry/start._GE1Zd3d.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.DyBXVtnT.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.DyBXVtnT.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.FqB0jq88.js.br +0 -2
- package/build/client/_app/immutable/nodes/1.FqB0jq88.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.Bekn_8hM.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.Bekn_8hM.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.DQfaAvJi.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.DQfaAvJi.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.B1E6iW2R.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.B1E6iW2R.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.28eZQkvz.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.28eZQkvz.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.qpcLWZb7.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.qpcLWZb7.js.gz +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { resolve } from "$app/paths";
|
|
3
|
+
import Panel from "$lib/components/Panel.svelte";
|
|
4
|
+
import TooltipTable from "$lib/components/TooltipTable.svelte";
|
|
5
|
+
import { formatBytes, formatNumber } from "$lib/utils/filters";
|
|
6
|
+
import type { PageData } from "./$types";
|
|
7
|
+
|
|
8
|
+
let { data }: { data: PageData } = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<Panel title="{data.database} collections">
|
|
12
|
+
<table class="table">
|
|
13
|
+
<thead>
|
|
14
|
+
<tr>
|
|
15
|
+
<th>Name</th>
|
|
16
|
+
<th>Documents</th>
|
|
17
|
+
<th>Indexes</th>
|
|
18
|
+
<th>Size</th>
|
|
19
|
+
</tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody>
|
|
22
|
+
{#if data.collections && data.collections.length > 0}
|
|
23
|
+
{#each data.collections as collection (collection.name)}
|
|
24
|
+
<tr>
|
|
25
|
+
<td>
|
|
26
|
+
<a
|
|
27
|
+
href={resolve(
|
|
28
|
+
`/servers/${encodeURIComponent(data.server)}/databases/${encodeURIComponent(
|
|
29
|
+
data.database,
|
|
30
|
+
)}/collections/${encodeURIComponent(collection.name)}?query=${encodeURIComponent(
|
|
31
|
+
"{}",
|
|
32
|
+
)}&sort=&project=&skip=0&limit=20`,
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
{collection.name}
|
|
36
|
+
</a>
|
|
37
|
+
</td>
|
|
38
|
+
<td>
|
|
39
|
+
{#await collection.details}
|
|
40
|
+
<span class="text-gray-400">...</span>
|
|
41
|
+
{:then details}
|
|
42
|
+
{#if details}
|
|
43
|
+
{formatNumber(details.count)}
|
|
44
|
+
{:else}
|
|
45
|
+
<span title="Failed to load">❌</span>
|
|
46
|
+
{/if}
|
|
47
|
+
{/await}
|
|
48
|
+
</td>
|
|
49
|
+
<td>
|
|
50
|
+
{#await collection.details}
|
|
51
|
+
<span class="text-gray-400">...</span>
|
|
52
|
+
{:then details}
|
|
53
|
+
{#if details}
|
|
54
|
+
<TooltipTable
|
|
55
|
+
columns={[
|
|
56
|
+
{ header: "Index", key: "definition", align: "left" },
|
|
57
|
+
{ header: "Size", key: "size", align: "right" },
|
|
58
|
+
]}
|
|
59
|
+
rows={details.indexes.map((index) => ({
|
|
60
|
+
definition: index.key ? JSON.stringify(index.key, null, 1) : index.name,
|
|
61
|
+
size: formatBytes(index.size),
|
|
62
|
+
}))}
|
|
63
|
+
>
|
|
64
|
+
{formatNumber(details.nIndexes)}
|
|
65
|
+
</TooltipTable>
|
|
66
|
+
{:else}
|
|
67
|
+
<span title="Failed to load">❌</span>
|
|
68
|
+
{/if}
|
|
69
|
+
{/await}
|
|
70
|
+
</td>
|
|
71
|
+
<td>
|
|
72
|
+
{#await collection.details}
|
|
73
|
+
<span class="text-gray-400">...</span>
|
|
74
|
+
{:then details}
|
|
75
|
+
{#if details}
|
|
76
|
+
<TooltipTable
|
|
77
|
+
hideHeader
|
|
78
|
+
columns={[
|
|
79
|
+
{ header: "Label", key: "label", align: "left" },
|
|
80
|
+
{ header: "Value", key: "value", align: "right" },
|
|
81
|
+
]}
|
|
82
|
+
rows={[
|
|
83
|
+
{ label: "Average obj. size", value: details.avgObjSize },
|
|
84
|
+
{ label: "Data size", value: details.dataSize },
|
|
85
|
+
{ label: "Storage size", value: details.storageSize },
|
|
86
|
+
{ label: "Index size", value: details.totalIndexSize },
|
|
87
|
+
].map((row) => ({
|
|
88
|
+
...row,
|
|
89
|
+
value: typeof row.value === "number" ? formatBytes(row.value) : row.value,
|
|
90
|
+
}))}
|
|
91
|
+
>
|
|
92
|
+
{formatBytes(details.size)}
|
|
93
|
+
</TooltipTable>
|
|
94
|
+
{:else}
|
|
95
|
+
<span title="Failed to load">❌</span>
|
|
96
|
+
{/if}
|
|
97
|
+
{/await}
|
|
98
|
+
</td>
|
|
99
|
+
</tr>
|
|
100
|
+
{/each}
|
|
101
|
+
{:else}
|
|
102
|
+
<tr>
|
|
103
|
+
<td colspan="4">
|
|
104
|
+
<div class="text-center">No collections...</div>
|
|
105
|
+
</td>
|
|
106
|
+
</tr>
|
|
107
|
+
{/if}
|
|
108
|
+
</tbody>
|
|
109
|
+
</table>
|
|
110
|
+
</Panel>
|
package/src/routes/servers/[server]/databases/[database]/collections/[collection]/+page.server.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import JsonEncoder from "$lib/server/JsonEncoder";
|
|
2
|
+
import { getMongo } from "$lib/server/mongo";
|
|
3
|
+
import type { MongoDocument } from "$lib/types";
|
|
4
|
+
import { parseJSON } from "$lib/utils/jsonParser";
|
|
5
|
+
import { error } from "@sveltejs/kit";
|
|
6
|
+
import type { Document } from "mongodb";
|
|
7
|
+
import type { PageServerLoad } from "./$types";
|
|
8
|
+
|
|
9
|
+
export const load: PageServerLoad = async ({ params, url }) => {
|
|
10
|
+
const query = url.searchParams.get("query") || "{}";
|
|
11
|
+
const sort = url.searchParams.get("sort") || "{}";
|
|
12
|
+
const project = url.searchParams.get("project") || "{}";
|
|
13
|
+
const skip = parseInt(url.searchParams.get("skip") || "0", 10);
|
|
14
|
+
const limit = parseInt(url.searchParams.get("limit") || "20", 10);
|
|
15
|
+
|
|
16
|
+
// Parse JSON strings early for immediate errors
|
|
17
|
+
let queryDoc: unknown;
|
|
18
|
+
try {
|
|
19
|
+
queryDoc = parseJSON(query);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return error(400, `Invalid query: ${query} - ${err}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let sortDoc: unknown;
|
|
25
|
+
try {
|
|
26
|
+
sortDoc = parseJSON(sort);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return error(400, `Invalid sort: ${sort} - ${err}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let projectDoc: Document;
|
|
32
|
+
try {
|
|
33
|
+
projectDoc = parseJSON(project) as Document;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return error(400, `Invalid project: ${project} - ${err}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const mongo = await getMongo();
|
|
39
|
+
const collection = mongo.getCollection(params.server, params.database, params.collection);
|
|
40
|
+
|
|
41
|
+
if (!collection) {
|
|
42
|
+
return error(404, `Collection not found: ${params.server}.${params.database}.${params.collection}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Stream both promises - return error info instead of throwing
|
|
46
|
+
const resultsPromise = collection
|
|
47
|
+
.find(JsonEncoder.decode(queryDoc))
|
|
48
|
+
.project(projectDoc)
|
|
49
|
+
.sort(JsonEncoder.decode(sortDoc))
|
|
50
|
+
.limit(limit)
|
|
51
|
+
.skip(skip)
|
|
52
|
+
.map((obj) => JsonEncoder.encode(obj))
|
|
53
|
+
.toArray()
|
|
54
|
+
.then((results) => ({
|
|
55
|
+
data: results as MongoDocument[],
|
|
56
|
+
error: null as string | null,
|
|
57
|
+
}))
|
|
58
|
+
.catch((err) => {
|
|
59
|
+
console.error("Error fetching query results:", err);
|
|
60
|
+
return {
|
|
61
|
+
data: [] as MongoDocument[],
|
|
62
|
+
error: `Failed to fetch query results: ${err instanceof Error ? err.message : String(err)}`,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const countPromise = (async () => {
|
|
67
|
+
try {
|
|
68
|
+
if (queryDoc && Object.keys(queryDoc).length > 0) {
|
|
69
|
+
return {
|
|
70
|
+
data: await collection.countDocuments(JsonEncoder.decode(queryDoc), {
|
|
71
|
+
maxTimeMS: mongo.getCountTimeout(),
|
|
72
|
+
}),
|
|
73
|
+
error: null as string | null,
|
|
74
|
+
};
|
|
75
|
+
} else {
|
|
76
|
+
// fast count
|
|
77
|
+
return {
|
|
78
|
+
data: await collection.estimatedDocumentCount(),
|
|
79
|
+
error: null as string | null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error("Error counting documents:", err);
|
|
84
|
+
return {
|
|
85
|
+
data: 0,
|
|
86
|
+
error: `Failed to count documents: ${err instanceof Error ? err.message : String(err)}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
server: params.server,
|
|
93
|
+
database: params.database,
|
|
94
|
+
collection: params.collection,
|
|
95
|
+
// Stream these promises to the client
|
|
96
|
+
results: resultsPromise,
|
|
97
|
+
count: countPromise,
|
|
98
|
+
params: {
|
|
99
|
+
query,
|
|
100
|
+
sort,
|
|
101
|
+
project,
|
|
102
|
+
skip,
|
|
103
|
+
limit,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
};
|
package/src/routes/servers/[server]/databases/[database]/collections/[collection]/+page.svelte
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
deleteDocument as deleteDocumentCommand,
|
|
4
|
+
updateDocument as updateDocumentCommand,
|
|
5
|
+
} from "$api/servers.remote";
|
|
6
|
+
import { resolve } from "$app/paths";
|
|
7
|
+
import Panel from "$lib/components/Panel.svelte";
|
|
8
|
+
import PrettyJson from "$lib/components/PrettyJson.svelte";
|
|
9
|
+
import SearchBox from "$lib/components/SearchBox.svelte";
|
|
10
|
+
import { notificationStore } from "$lib/stores/notifications.svelte";
|
|
11
|
+
import type { MongoDocument, SearchParams } from "$lib/types";
|
|
12
|
+
import { formatNumber } from "$lib/utils/filters";
|
|
13
|
+
import { SvelteURLSearchParams } from "svelte/reactivity";
|
|
14
|
+
import type { PageData } from "./$types";
|
|
15
|
+
|
|
16
|
+
let { data }: { data: PageData } = $props();
|
|
17
|
+
|
|
18
|
+
let params = $state<SearchParams>({ ...data.params });
|
|
19
|
+
|
|
20
|
+
// Handle errors from streamed promises
|
|
21
|
+
$effect(() => {
|
|
22
|
+
// @ts-expect-error I just want to trigger the dependency
|
|
23
|
+
if (data.results) {
|
|
24
|
+
modifiedItems = null;
|
|
25
|
+
}
|
|
26
|
+
data.results.then((result) => {
|
|
27
|
+
if (result.error) {
|
|
28
|
+
notificationStore.notifyError(result.error);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
$effect(() => {
|
|
34
|
+
data.count.then((result) => {
|
|
35
|
+
if (result.error) {
|
|
36
|
+
notificationStore.notifyError(result.error);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let modifiedItems = $state<MongoDocument[] | null>(null);
|
|
42
|
+
let items = $derived(modifiedItems ? { data: modifiedItems, error: null } : data.results);
|
|
43
|
+
|
|
44
|
+
async function editDocument(_id: { $value?: string } | undefined, json: MongoDocument, items: MongoDocument[]) {
|
|
45
|
+
const partial = Boolean(
|
|
46
|
+
params.project && params.project !== "{}" && Object.keys(JSON.parse(params.project)).length > 0,
|
|
47
|
+
);
|
|
48
|
+
const newId = json?._id?.$value;
|
|
49
|
+
const oldId = _id?.$value;
|
|
50
|
+
|
|
51
|
+
if (newId !== oldId) {
|
|
52
|
+
notificationStore.notifyError("ObjectId changed. This is not supported, update canceled.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!oldId) return;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const result = await updateDocumentCommand({
|
|
60
|
+
server: data.server,
|
|
61
|
+
database: data.database,
|
|
62
|
+
collection: data.collection,
|
|
63
|
+
document: oldId,
|
|
64
|
+
value: json,
|
|
65
|
+
partial,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (result.ok) {
|
|
69
|
+
notificationStore.notifySuccess("Document updated successfully");
|
|
70
|
+
// Update the document in the list
|
|
71
|
+
const index = items.findIndex((item) => item._id?.$value === oldId);
|
|
72
|
+
if (index !== -1) {
|
|
73
|
+
items[index] = result.update;
|
|
74
|
+
}
|
|
75
|
+
modifiedItems = items;
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.log(error);
|
|
79
|
+
notificationStore.notifyError(error, "Failed to update document");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function removeDocument(_id: { $value?: string } | undefined, items: MongoDocument[]) {
|
|
84
|
+
const documentId = _id?.$value;
|
|
85
|
+
if (!documentId) return;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await deleteDocumentCommand({
|
|
89
|
+
server: data.server,
|
|
90
|
+
database: data.database,
|
|
91
|
+
collection: data.collection,
|
|
92
|
+
document: documentId,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
notificationStore.notifySuccess("Document removed successfully");
|
|
96
|
+
modifiedItems = items.filter((item) => item._id?.$value !== documentId);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
notificationStore.notifyError(error, "Failed to remove document");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildUrl(skip: number) {
|
|
103
|
+
const queryParams = new SvelteURLSearchParams();
|
|
104
|
+
queryParams.set("query", params.query || "{}");
|
|
105
|
+
queryParams.set("sort", params.sort || "");
|
|
106
|
+
queryParams.set("project", params.project || "");
|
|
107
|
+
queryParams.set("skip", String(skip));
|
|
108
|
+
queryParams.set("limit", String(params.limit));
|
|
109
|
+
|
|
110
|
+
return `/servers/${encodeURIComponent(data.server)}/databases/${encodeURIComponent(data.database)}/collections/${encodeURIComponent(data.collection)}?${queryParams.toString()}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const nextUrl = $derived(buildUrl(params.skip + params.limit));
|
|
114
|
+
const previousUrl = $derived(buildUrl(Math.max(0, params.skip - params.limit)));
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<SearchBox bind:params />
|
|
118
|
+
|
|
119
|
+
{#await items}
|
|
120
|
+
<Panel title="Loading documents...">
|
|
121
|
+
{#snippet actions()}
|
|
122
|
+
<span class="text-sm text-gray-500">Loading...</span>
|
|
123
|
+
{/snippet}
|
|
124
|
+
</Panel>
|
|
125
|
+
{:then resultsData}
|
|
126
|
+
{@const items = resultsData.data}
|
|
127
|
+
{#await data.count}
|
|
128
|
+
<Panel
|
|
129
|
+
title={items.length > 0
|
|
130
|
+
? `${formatNumber(data.params.skip + 1)} - ${formatNumber(data.params.skip + items.length)} Documents (counting...)`
|
|
131
|
+
: "No documents"}
|
|
132
|
+
>
|
|
133
|
+
{#snippet actions()}
|
|
134
|
+
{#if data.params.skip > 0}
|
|
135
|
+
<!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
|
|
136
|
+
<a href={resolve(previousUrl as any)} class="btn btn-default btn-sm -my-2">Previous</a>
|
|
137
|
+
{/if}
|
|
138
|
+
{/snippet}
|
|
139
|
+
</Panel>
|
|
140
|
+
{:then countData}
|
|
141
|
+
{@const count = countData.data}
|
|
142
|
+
{@const hasNext = data.params.skip + items.length < count}
|
|
143
|
+
{@const hasPrevious = data.params.skip > 0}
|
|
144
|
+
<Panel
|
|
145
|
+
title={count > 0
|
|
146
|
+
? `${formatNumber(data.params.skip + 1)} - ${formatNumber(data.params.skip + items.length)} of ${formatNumber(count)} Documents`
|
|
147
|
+
: "No documents"}
|
|
148
|
+
>
|
|
149
|
+
{#snippet actions()}
|
|
150
|
+
{#if hasPrevious}
|
|
151
|
+
<!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
|
|
152
|
+
<a href={resolve(previousUrl as any)} class="btn btn-default btn-sm -my-2">Previous</a>
|
|
153
|
+
{/if}
|
|
154
|
+
{#if hasNext}
|
|
155
|
+
<!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
|
|
156
|
+
<a href={resolve(nextUrl as any)} class="btn btn-default btn-sm -my-2">Next</a>
|
|
157
|
+
{/if}
|
|
158
|
+
{/snippet}
|
|
159
|
+
</Panel>
|
|
160
|
+
{/await}
|
|
161
|
+
|
|
162
|
+
{#each items as item (item._id?.$value)}
|
|
163
|
+
<PrettyJson
|
|
164
|
+
json={item}
|
|
165
|
+
autoCollapse={true}
|
|
166
|
+
readOnly={data.readOnly}
|
|
167
|
+
onedit={(json) => editDocument(item._id, json, items)}
|
|
168
|
+
onremove={() => removeDocument(item._id, items)}
|
|
169
|
+
server={data.server}
|
|
170
|
+
database={data.database}
|
|
171
|
+
collection={data.collection}
|
|
172
|
+
/>
|
|
173
|
+
{/each}
|
|
174
|
+
{/await}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import JsonEncoder from "$lib/server/JsonEncoder";
|
|
2
|
+
import { getMongo } from "$lib/server/mongo";
|
|
3
|
+
import { ObjectId } from "mongodb";
|
|
4
|
+
import type { PageServerLoad } from "./$types";
|
|
5
|
+
|
|
6
|
+
export const load: PageServerLoad = async ({ params }) => {
|
|
7
|
+
const mongo = await getMongo();
|
|
8
|
+
const collection = mongo.getCollection(params.server, params.database, params.collection);
|
|
9
|
+
|
|
10
|
+
let document = null;
|
|
11
|
+
if (collection) {
|
|
12
|
+
const obj = await collection.findOne({
|
|
13
|
+
_id: new ObjectId(params.document),
|
|
14
|
+
});
|
|
15
|
+
document = JsonEncoder.encode(obj);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
server: params.server,
|
|
20
|
+
database: params.database,
|
|
21
|
+
collection: params.collection,
|
|
22
|
+
documentId: params.document,
|
|
23
|
+
document,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
deleteDocument as deleteDocumentCommand,
|
|
4
|
+
updateDocument as updateDocumentCommand,
|
|
5
|
+
} from "$api/servers.remote";
|
|
6
|
+
import { goto } from "$app/navigation";
|
|
7
|
+
import { resolve } from "$app/paths";
|
|
8
|
+
import PrettyJson from "$lib/components/PrettyJson.svelte";
|
|
9
|
+
import { notificationStore } from "$lib/stores/notifications.svelte";
|
|
10
|
+
import type { MongoDocument } from "$lib/types";
|
|
11
|
+
import type { PageData } from "./$types";
|
|
12
|
+
|
|
13
|
+
let { data }: { data: PageData } = $props();
|
|
14
|
+
|
|
15
|
+
let loading = $state(false);
|
|
16
|
+
let item = $derived(data.document);
|
|
17
|
+
|
|
18
|
+
async function editDocument(json: MongoDocument) {
|
|
19
|
+
const newId = json?._id?.$value;
|
|
20
|
+
const oldId = item?._id?.$value;
|
|
21
|
+
|
|
22
|
+
if (newId !== oldId) {
|
|
23
|
+
notificationStore.notifyError("ObjectId changed. This is not supported, update canceled.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!oldId) return;
|
|
28
|
+
|
|
29
|
+
loading = true;
|
|
30
|
+
try {
|
|
31
|
+
const result = await updateDocumentCommand({
|
|
32
|
+
server: data.server,
|
|
33
|
+
database: data.database,
|
|
34
|
+
collection: data.collection,
|
|
35
|
+
document: oldId,
|
|
36
|
+
value: json,
|
|
37
|
+
partial: false,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (result.ok) {
|
|
41
|
+
notificationStore.notifySuccess("Document updated successfully");
|
|
42
|
+
item = result.update;
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
notificationStore.notifyError(error, "Failed to update document");
|
|
46
|
+
} finally {
|
|
47
|
+
loading = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function removeDocument() {
|
|
52
|
+
const documentId = item?._id?.$value;
|
|
53
|
+
if (!documentId) return;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await deleteDocumentCommand({
|
|
57
|
+
server: data.server,
|
|
58
|
+
database: data.database,
|
|
59
|
+
collection: data.collection,
|
|
60
|
+
document: documentId,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
notificationStore.notifySuccess("Document removed successfully");
|
|
64
|
+
// Navigate back to the collection explore page
|
|
65
|
+
goto(
|
|
66
|
+
resolve(
|
|
67
|
+
`/servers/${encodeURIComponent(data.server)}/databases/${encodeURIComponent(data.database)}/collections/${encodeURIComponent(data.collection)}?query=${encodeURIComponent("{}")}&sort=&project=&skip=0&limit=20`,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
notificationStore.notifyError(error, "Failed to remove document");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
{#if loading}
|
|
77
|
+
<div class="loading">Loading...</div>
|
|
78
|
+
{:else if item}
|
|
79
|
+
<PrettyJson
|
|
80
|
+
json={item}
|
|
81
|
+
readOnly={data.readOnly}
|
|
82
|
+
onedit={editDocument}
|
|
83
|
+
onremove={removeDocument}
|
|
84
|
+
server={data.server}
|
|
85
|
+
database={data.database}
|
|
86
|
+
collection={data.collection}
|
|
87
|
+
/>
|
|
88
|
+
{:else}
|
|
89
|
+
<div class="text-center">Document not found</div>
|
|
90
|
+
{/if}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
// Skipping for now, not sure how to handle tests with remote functions that throw errors
|
|
5
|
+
describe.skip("Read-Only Mode Remote Functions Tests", () => {
|
|
6
|
+
let originalEnv: string | undefined;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Store original env value
|
|
10
|
+
originalEnv = process.env.MONGOKU_READ_ONLY_MODE;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
// Restore original env value
|
|
15
|
+
if (originalEnv !== undefined) {
|
|
16
|
+
process.env.MONGOKU_READ_ONLY_MODE = originalEnv;
|
|
17
|
+
} else {
|
|
18
|
+
delete process.env.MONGOKU_READ_ONLY_MODE;
|
|
19
|
+
}
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("updateDocument", () => {
|
|
24
|
+
it("should throw error when trying to update a document in read-only mode", async () => {
|
|
25
|
+
process.env.MONGOKU_READ_ONLY_MODE = "true";
|
|
26
|
+
|
|
27
|
+
const { updateDocument } = await import("../../api/servers.remote");
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await updateDocument({
|
|
31
|
+
server: "test",
|
|
32
|
+
database: "testdb",
|
|
33
|
+
collection: "testcol",
|
|
34
|
+
document: "507f1f77bcf86cd799439011",
|
|
35
|
+
value: { name: "updated" },
|
|
36
|
+
partial: false,
|
|
37
|
+
});
|
|
38
|
+
expect.fail("Should have thrown an error");
|
|
39
|
+
} catch (error: any) {
|
|
40
|
+
expect(error.status).toBe(403);
|
|
41
|
+
expect(error.body.message).toContain("Read-only mode");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should allow updates when not in read-only mode", async () => {
|
|
46
|
+
delete process.env.MONGOKU_READ_ONLY_MODE;
|
|
47
|
+
|
|
48
|
+
const { updateDocument } = await import("../../api/servers.remote");
|
|
49
|
+
|
|
50
|
+
// Should work or fail for a different reason
|
|
51
|
+
try {
|
|
52
|
+
const response = await updateDocument({
|
|
53
|
+
server: "test",
|
|
54
|
+
database: "testdb",
|
|
55
|
+
collection: "testcol",
|
|
56
|
+
document: "507f1f77bcf86cd799439011",
|
|
57
|
+
value: { name: "updated" },
|
|
58
|
+
partial: false,
|
|
59
|
+
});
|
|
60
|
+
// Success case
|
|
61
|
+
expect(response.ok).toBe(true);
|
|
62
|
+
} catch (error: any) {
|
|
63
|
+
// If it fails, it shouldn't be because of read-only mode
|
|
64
|
+
expect(error.status).not.toBe(403);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("deleteDocument", () => {
|
|
70
|
+
it("should throw error when trying to delete a document in read-only mode", async () => {
|
|
71
|
+
process.env.MONGOKU_READ_ONLY_MODE = "true";
|
|
72
|
+
|
|
73
|
+
const { deleteDocument } = await import("../../api/servers.remote");
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await deleteDocument({
|
|
77
|
+
server: "test",
|
|
78
|
+
database: "testdb",
|
|
79
|
+
collection: "testcol",
|
|
80
|
+
document: "507f1f77bcf86cd799439011",
|
|
81
|
+
});
|
|
82
|
+
expect.fail("Should have thrown an error");
|
|
83
|
+
} catch (error: any) {
|
|
84
|
+
expect(error.status).toBe(403);
|
|
85
|
+
expect(error.body.message).toContain("Read-only mode");
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Mock the mongo module to avoid actual MongoDB connections during tests
|
|
4
|
+
vi.mock("$lib/server/mongo", () => ({
|
|
5
|
+
getMongo: vi.fn(() =>
|
|
6
|
+
Promise.resolve({
|
|
7
|
+
findOne: vi.fn(),
|
|
8
|
+
find: vi.fn(),
|
|
9
|
+
count: vi.fn(),
|
|
10
|
+
updateOne: vi.fn(),
|
|
11
|
+
removeOne: vi.fn(),
|
|
12
|
+
getServersJson: vi.fn(() => Promise.resolve([])),
|
|
13
|
+
getDatabasesJson: vi.fn(() => Promise.resolve([])),
|
|
14
|
+
getCollectionsJson: vi.fn(() => Promise.resolve([])),
|
|
15
|
+
addServer: vi.fn(),
|
|
16
|
+
removeServer: vi.fn(),
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
19
|
+
}));
|
package/svelte.config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import adapter from "@sveltejs/adapter-node";
|
|
2
|
+
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
|
3
|
+
|
|
4
|
+
/** @type {import('@sveltejs/kit').Config} */
|
|
5
|
+
const config = {
|
|
6
|
+
preprocess: vitePreprocess(),
|
|
7
|
+
|
|
8
|
+
kit: {
|
|
9
|
+
adapter: adapter({
|
|
10
|
+
out: "build",
|
|
11
|
+
envPrefix: "MONGOKU_SERVER_",
|
|
12
|
+
}),
|
|
13
|
+
experimental: {
|
|
14
|
+
remoteFunctions: true,
|
|
15
|
+
},
|
|
16
|
+
alias: {
|
|
17
|
+
$api: "src/api",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
compilerOptions: {
|
|
22
|
+
experimental: {
|
|
23
|
+
async: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default config;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"target": "es2022",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": ".",
|
|
9
|
+
"declaration": false,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["cli.ts"],
|
|
14
|
+
"exclude": ["node_modules", "src", "build", ".svelte-kit"]
|
|
15
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./.svelte-kit/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"checkJs": true,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"forceConsistentCasingInFileNames": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"moduleResolution": "bundler"
|
|
13
|
+
}
|
|
14
|
+
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
|
15
|
+
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
|
16
|
+
//
|
|
17
|
+
// To make changes to top-level options such as include and exclude, we recommend extending
|
|
18
|
+
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
|
19
|
+
}
|
package/vite.config.ts
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|