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
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MongoBench

A modern, dark-mode-first MongoDB GUI for Windows. Built as a daily-driver alternative to MongoDB Compass.
A modern, dark-mode-first MongoDB GUI. Built as a daily-driver alternative to MongoDB Compass.

> Status: early development

Expand All @@ -11,12 +11,6 @@ A modern, dark-mode-first MongoDB GUI for Windows. Built as a daily-driver alter
- React 18, Zustand, TanStack Query
- Tailwind CSS + shadcn/ui
- Official `mongodb` Node.js driver, `bson` for EJSON (added in M1)
- electron-builder for Windows distribution (NSIS / MSI / portable)

## Requirements

- Node.js 20+
- Windows 10/11 (the v1 target; the codebase stays portable for later cross-platform builds)

## Scripts

Expand Down
33 changes: 13 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "mongobench",
"version": "1.0.0",
"private": true,
"description": "A modern, dark-mode-first MongoDB GUI for Windows.",
"description": "A modern, dark-mode-first MongoDB GUI.",
"author": "ByteExceptionM",
"license": "MIT",
"repository": {
Expand Down
10 changes: 10 additions & 0 deletions src/main/services/ConnectionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ export class ConnectionService {
return client
}

/**
* Per-connection toggle: when true, the explorer should only list
* databases / collections the authenticated user has privileges on.
* Reads from the in-memory repo cache, so it's effectively sync.
*/
async isAuthorizedOnly(id: string): Promise<boolean> {
const stored = await this.repo.getStored(id)
return stored?.authorizedOnly === true
}

private async materializeFromInput(input: ConnectionInput, existingId?: string): Promise<string> {
const formPassword =
input.password !== undefined && input.password.length > 0 ? input.password : undefined
Expand Down
37 changes: 27 additions & 10 deletions src/main/services/DatabaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ export class DatabaseService {

async listDatabases(connectionId: string): Promise<DatabaseInfo[]> {
const client = this.connections.getClient(connectionId)
const authOnly = await this.connections.isAuthorizedOnly(connectionId)
if (!authOnly) {
const result = (await client.db('admin').admin().listDatabases()) as {
databases: Array<{ name: string; sizeOnDisk?: number; empty?: boolean }>
}
return result.databases
}

const [result, allowed] = await Promise.all([
client.db('admin').admin().listDatabases({ authorizedDatabases: true }) as Promise<{
databases: Array<{ name: string; sizeOnDisk?: number; empty?: boolean }>
Expand All @@ -22,9 +30,10 @@ export class DatabaseService {

async listCollections(connectionId: string, db: string): Promise<CollectionInfo[]> {
const client = this.connections.getClient(connectionId)
const authOnly = await this.connections.isAuthorizedOnly(connectionId)
const cursor = client
.db(db)
.listCollections({}, { nameOnly: true, authorizedCollections: true })
.listCollections({}, { nameOnly: true, authorizedCollections: authOnly })
const items = await cursor.toArray()
return items.map((info) => ({
name: info.name as string,
Expand Down Expand Up @@ -118,9 +127,10 @@ export class DatabaseService {
async serverStats(connectionId: string): Promise<ServerStats> {
const client = this.connections.getClient(connectionId)
const admin = client.db('admin')
const authOnly = await this.connections.isAuthorizedOnly(connectionId)
const [status, dbs] = await Promise.all([
admin.command({ serverStatus: 1 }) as Promise<Record<string, unknown>>,
admin.admin().listDatabases({ authorizedDatabases: true }) as Promise<{
admin.admin().listDatabases(authOnly ? { authorizedDatabases: true } : {}) as Promise<{
databases: Array<{ name: string; sizeOnDisk?: number; empty?: boolean }>
totalSize?: number
}>
Expand Down Expand Up @@ -148,9 +158,9 @@ export class DatabaseService {
const opLat = status['opLatencies'] as Record<string, unknown> | undefined
const latencies = opLat
? {
reads: averageLatency(opLat['reads']),
writes: averageLatency(opLat['writes']),
commands: averageLatency(opLat['commands'])
reads: rawLatency(opLat['reads']),
writes: rawLatency(opLat['writes']),
commands: rawLatency(opLat['commands'])
}
: undefined

Expand Down Expand Up @@ -284,12 +294,19 @@ async function authorizedDatabases(client: MongoClient): Promise<Set<string> | '
}
}

function averageLatency(node: unknown): { avgMicros: number; ops: number } {
if (typeof node !== 'object' || node === null) return { avgMicros: 0, ops: 0 }
/**
* Pull the raw cumulative latency / ops counters out of the
* `opLatencies.{reads,writes,commands}` block. The renderer derives a
* *recent* per-op average by diffing consecutive samples — a cumulative
* ratio computed here would barely move once the server has been up.
*/
function rawLatency(node: unknown): { latencyMicros: number; ops: number } {
if (typeof node !== 'object' || node === null) return { latencyMicros: 0, ops: 0 }
const r = node as Record<string, unknown>
const latency = numberOr(r['latency'], 0)
const ops = numberOr(r['ops'], 0)
return { avgMicros: ops > 0 ? latency / ops : 0, ops }
return {
latencyMicros: numberOr(r['latency'], 0),
ops: numberOr(r['ops'], 0)
}
}

function tickets(node: unknown): { available: number; out: number; total: number } {
Expand Down
1 change: 1 addition & 0 deletions src/main/stores/ConnectionsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export class ConnectionsRepository {
...(input.readPreference !== undefined ? { readPreference: input.readPreference } : {}),
...(input.uuidEncoding !== undefined ? { uuidEncoding: input.uuidEncoding } : {}),
...(input.timezone !== undefined ? { timezone: input.timezone } : {}),
...(input.authorizedOnly !== undefined ? { authorizedOnly: input.authorizedOnly } : {}),
...(input.maxPoolSize !== undefined ? { maxPoolSize: input.maxPoolSize } : {}),
...(input.minPoolSize !== undefined ? { minPoolSize: input.minPoolSize } : {}),
...(input.connectTimeoutMS !== undefined ? { connectTimeoutMS: input.connectTimeoutMS } : {}),
Expand Down
Loading
Loading