Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/tauri/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import WelcomeTourDialog from "./ui/WelcomeTourDialog.vue";
import AboutDialog from "./ui/AboutDialog.vue";
import NewFolderDialog from "./ui/NewFolderDialog.vue";
import NewConnectionDialog, {
type MongoConnectPayload,
type MysqlConnectPayload,
type PostgresConnectPayload,
type RedisConnectPayload,
Expand Down Expand Up @@ -131,6 +132,22 @@ async function onConnectRedisFromDialog(payload: RedisConnectPayload) {
}
}

async function onConnectMongoFromDialog(payload: MongoConnectPayload) {
try {
await connections.connectMongo({
name: payload.name,
host: payload.host,
port: payload.port,
database: payload.database,
username: payload.username || null,
password: payload.password || null,
authSource: payload.authSource || "admin",
});
} catch {
// status already set in store
}
}

function onNewFolder() {
newFolderOpen.value = true;
}
Expand Down Expand Up @@ -317,6 +334,7 @@ onUnmounted(() => {
@connect-postgres="onConnectPostgresFromDialog"
@connect-mysql="onConnectMysqlFromDialog"
@connect-redis="onConnectRedisFromDialog"
@connect-mongo="onConnectMongoFromDialog"
/>
</div>
</template>
Expand Down
86 changes: 86 additions & 0 deletions apps/tauri/src/api/mongo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { invoke } from "@tauri-apps/api/core";
import type { ConnectionProfile } from "./connection";

export type MongoFindResult = {
documents: unknown[];
truncated: boolean;
};

export async function dbConnectMongo(opts: {
host: string;
port: number;
database: string;
username?: string | null;
password?: string | null;
authSource?: string | null;
id?: string | null;
}): Promise<string> {
return invoke<string>("db_connect_mongo", {
host: opts.host,
port: opts.port,
database: opts.database,
username: opts.username ?? null,
password: opts.password ?? null,
authSource: opts.authSource ?? null,
id: opts.id ?? null,
});
}

export async function profilesConnectMongo(opts: {
name: string;
host: string;
port: number;
database: string;
username?: string | null;
password?: string | null;
authSource?: string | null;
folderId?: string | null;
id?: string | null;
}): Promise<ConnectionProfile> {
return invoke<ConnectionProfile>("profiles_connect_mongo", {
request: {
name: opts.name,
host: opts.host,
port: opts.port,
database: opts.database,
username: opts.username ?? null,
password: opts.password ?? null,
authSource: opts.authSource ?? null,
folderId: opts.folderId ?? null,
id: opts.id ?? null,
},
});
}

export async function mongoPing(id: string): Promise<string> {
return invoke<string>("mongo_ping", { id });
}

export async function mongoListDatabases(id: string): Promise<string[]> {
return invoke<string[]>("mongo_list_databases", { id });
}

export async function mongoListCollections(
id: string,
database: string,
): Promise<string[]> {
return invoke<string[]>("mongo_list_collections", { id, database });
}

export async function mongoFind(
id: string,
opts: {
database: string;
collection: string;
filterJson?: string | null;
limit?: number | null;
},
): Promise<MongoFindResult> {
return invoke<MongoFindResult>("mongo_find", {
id,
database: opts.database,
collection: opts.collection,
filterJson: opts.filterJson ?? null,
limit: opts.limit ?? null,
});
}
115 changes: 113 additions & 2 deletions apps/tauri/src/stores/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
profilesUpsert,
type ConnectionProfile,
} from "../api/connection";
import { profilesConnectMongo } from "../api/mongo";
import { profilesConnectRedis } from "../api/redis";
import { DEMO_CONNECTION } from "../mock/demoCatalog";
import { useFoldersStore } from "./folders";
Expand All @@ -19,7 +20,7 @@ import { useShellStatusStore } from "./shellStatus";

export type LiveConnection = {
id: string;
driver: "sqlite" | "postgres" | "mysql" | "redis";
driver: "sqlite" | "postgres" | "mysql" | "redis" | "mongo";
name: string;
/** SQLite file path. */
path?: string;
Expand Down Expand Up @@ -49,7 +50,13 @@ function endpointLabel(conn: LiveConnection): string {
const host = conn.host ?? "localhost";
const port =
conn.port ??
(conn.driver === "mysql" ? 3306 : conn.driver === "redis" ? 6379 : 5432);
(conn.driver === "mysql"
? 3306
: conn.driver === "redis"
? 6379
: conn.driver === "mongo"
? 27017
: 5432);
const db = conn.database ?? (conn.driver === "redis" ? "0" : "");
return `${host}:${port}/${db}`;
}
Expand Down Expand Up @@ -100,6 +107,21 @@ function fromProfile(p: ConnectionProfile, connected = false): LiveConnection |
connected,
};
}
if (p.driver === "mongo" || p.driver === "mongodb") {
const host = p.host?.trim();
if (!host) return null;
return {
id: p.id,
driver: "mongo",
name: p.name,
host,
port: p.port != null ? Number(p.port) : 27017,
database: p.database_name?.trim() || "querya",
user: p.username?.trim() || undefined,
folderId: p.folder_id ?? null,
connected,
};
}
return null;
}

Expand Down Expand Up @@ -135,6 +157,21 @@ function toProfile(conn: LiveConnection): ConnectionProfile {
updated_at: now,
};
}
if (conn.driver === "mongo") {
return {
id: conn.id,
driver: "mongo",
name: conn.name,
host: conn.host ?? null,
port: conn.port ?? 27017,
username: conn.user ?? null,
database_name: conn.database ?? "querya",
folder_id: conn.folderId ?? null,
sort_order: 0,
created_at: now,
updated_at: now,
};
}
return {
id: conn.id,
driver: "sqlite",
Expand Down Expand Up @@ -344,6 +381,66 @@ export const useConnectionsStore = defineStore("connections", () => {
}
}

async function connectMongo(opts: {
name: string;
host: string;
port: number;
database: string;
username?: string | null;
password?: string | null;
authSource?: string | null;
folderId?: string | null;
id?: string | null;
}) {
const status = useShellStatusStore();
const nav = useShellNavStore();
const name =
opts.name.trim() ||
`${opts.host}:${opts.port}/${opts.database}`;
status.setQueryMetrics({
busy: true,
message: `Connecting Mongo ${opts.host}:${opts.port}…`,
});
try {
const profile = await profilesConnectMongo({
name,
host: opts.host,
port: opts.port,
database: opts.database,
username: opts.username ?? null,
password: opts.password ?? null,
authSource: opts.authSource ?? null,
folderId: opts.folderId ?? null,
id: opts.id ?? null,
});
if (opts.password) sessionPasswords.set(profile.id, opts.password);
const conn: LiveConnection = {
id: profile.id,
driver: "mongo",
name: profile.name,
host: profile.host ?? opts.host,
port: profile.port != null ? Number(profile.port) : opts.port,
database: profile.database_name ?? opts.database,
user: profile.username ?? opts.username ?? undefined,
folderId: profile.folder_id ?? opts.folderId ?? null,
connected: true,
};
upsert(conn);
nav.setExpanded(conn.id, true);
nav.selectConnection(conn.id);
status.setConnection(conn.name, endpointLabel(conn));
status.setQueryMetrics({
busy: false,
message: `Connected · ${conn.name}`,
});
return conn;
} catch (err) {
const msg = formatConnectionError(err);
status.setQueryMetrics({ busy: false, message: msg });
throw err;
}
}

async function connectServerDriver(
driver: "postgres" | "mysql",
opts: {
Expand Down Expand Up @@ -442,6 +539,19 @@ export const useConnectionsStore = defineStore("connections", () => {
id: conn.id,
});
}
if (conn.driver === "mongo") {
return connectMongo({
name: conn.name,
host: conn.host ?? "localhost",
port: conn.port ?? 27017,
database: conn.database ?? "querya",
username: conn.user ?? null,
password,
authSource: "admin",
folderId: conn.folderId,
id: conn.id,
});
}
const opts = {
name: conn.name,
host: conn.host ?? "localhost",
Expand Down Expand Up @@ -574,6 +684,7 @@ export const useConnectionsStore = defineStore("connections", () => {
connectPostgres,
connectMysql,
connectRedis,
connectMongo,
ensureConnected,
disconnect,
removeConnection,
Expand Down
14 changes: 11 additions & 3 deletions apps/tauri/src/stores/objectTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,20 @@ export const useObjectTreeStore = defineStore("objectTree", () => {
return useConnectionsStore().byId.get(id)?.driver === "redis";
}

function isMongo(id: string): boolean {
return useConnectionsStore().byId.get(id)?.driver === "mongo";
}

function isNosqlBrowser(id: string): boolean {
return isRedis(id) || isMongo(id);
}

async function fetchChildren(
connId: string,
parentId: string | null,
): Promise<SqlObjectNode[]> {
if (isMock(connId)) return demoListChildren(parentId);
if (isRedis(connId)) return [];
if (isNosqlBrowser(connId)) return [];
return dbListChildren(connId, parentId);
}

Expand All @@ -144,8 +152,8 @@ export const useObjectTreeStore = defineStore("objectTree", () => {
st.error = null;
try {
await useConnectionsStore().ensureConnected(connId);
// Redis uses RedisBrowserView — no SQL object tree.
if (isRedis(connId)) {
// Redis/Mongo use dedicated browser panes — no SQL object tree.
if (isNosqlBrowser(connId)) {
st.roots = [];
st.loaded = true;
return;
Expand Down
Loading
Loading