From 79162bd46e1258d5eb8529cb31a74d20832522b9 Mon Sep 17 00:00:00 2001 From: Nycz-lab Date: Sun, 10 May 2026 23:04:30 +0200 Subject: [PATCH 1/4] chore: add aur instruction --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2096f1d..f4bf17a 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ Per-connection live view, sampled every 5 s, sliding 5-min history: | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Windows (x64)** | `MongoBench-Setup--x64.exe` from [Releases](https://github.com/ByteExceptionM/MongoBench/releases/latest) — NSIS installer, per-user, no admin | | **Linux (x64)** | `MongoBench--x86_64.AppImage` — `chmod +x` and run | -| **Arch / AUR** | PKGBUILD shipped under `packaging/arch/` | +| **Arch / AUR** | PKGBUILD shipped under `packaging/arch/` or via AUR: paru -S mongobench-git / yay -S mongobench-git | Builds are **unsigned**. On first launch on Windows you'll see SmartScreen — click "More info → Run anyway". From 7941180d952293a4e5943dec093e8d962757134d Mon Sep 17 00:00:00 2001 From: Nycz-lab Date: Sun, 10 May 2026 23:12:23 +0200 Subject: [PATCH 2/4] fix: style --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f4bf17a..2eeb1c3 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,7 @@ Per-connection live view, sampled every 5 s, sliding 5-min history: | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Windows (x64)** | `MongoBench-Setup--x64.exe` from [Releases](https://github.com/ByteExceptionM/MongoBench/releases/latest) — NSIS installer, per-user, no admin | | **Linux (x64)** | `MongoBench--x86_64.AppImage` — `chmod +x` and run | -| **Arch / AUR** | PKGBUILD shipped under `packaging/arch/` or via AUR: paru -S mongobench-git / yay -S mongobench-git | +| **Arch / AUR** | PKGBUILD shipped under `packaging/arch/` or via AUR: paru -S mongobench-git / yay -S mongobench-git | Builds are **unsigned**. On first launch on Windows you'll see SmartScreen — click "More info → Run anyway". From df8e8e9c675e6a5a202251723728a5ef0aff8a91 Mon Sep 17 00:00:00 2001 From: DasDarki Date: Wed, 24 Jun 2026 17:28:31 +0200 Subject: [PATCH 3/4] feat: add multi insert and sorting collections from a-z (#17) --- src/main/ipc/channels.ts | 1 + src/main/ipc/router.ts | 6 ++ src/main/services/DatabaseService.ts | 10 ++- src/main/services/QueryService.ts | 10 +++ src/preload/index.ts | 4 + .../document/DocumentEditorDialog.tsx | 41 ++++++--- .../src/features/explorer/CollectionLeaf.tsx | 2 +- src/renderer/src/lib/api.ts | 4 + src/renderer/src/lib/mongoQueryLang.test.ts | 84 ++++++++++++++++++- src/renderer/src/lib/mongoQueryLang.ts | 44 ++++++++++ src/shared/api.ts | 3 + src/shared/schemas.ts | 9 ++ src/shared/types.ts | 11 +++ 13 files changed, 212 insertions(+), 17 deletions(-) diff --git a/src/main/ipc/channels.ts b/src/main/ipc/channels.ts index 947b0b6..ffc37b0 100644 --- a/src/main/ipc/channels.ts +++ b/src/main/ipc/channels.ts @@ -37,6 +37,7 @@ export const Channels = { QueryCount: 'query:count', QueryReplaceOne: 'query:replaceOne', QueryInsertOne: 'query:insertOne', + QueryInsertMany: 'query:insertMany', QueryDeleteOne: 'query:deleteOne', QueryDeleteMany: 'query:deleteMany' } as const diff --git a/src/main/ipc/router.ts b/src/main/ipc/router.ts index d7223d4..1569945 100644 --- a/src/main/ipc/router.ts +++ b/src/main/ipc/router.ts @@ -21,6 +21,7 @@ import { DropUserSchema, FindRequestSchema, IndexesListSchema, + InsertManyRequestSchema, InsertOneRequestSchema, RenameCollectionSchema, ReorderConnectionsSchema, @@ -212,6 +213,11 @@ export function registerIpcHandlers(services: Services): void { withResult(InsertOneRequestSchema, (request) => queries.insertOne(request)) ) + ipcMain.handle( + Channels.QueryInsertMany, + withResult(InsertManyRequestSchema, (request) => queries.insertMany(request)) + ) + ipcMain.handle( Channels.QueryDeleteOne, withResult(DeleteOneRequestSchema, (request) => queries.deleteOne(request)) diff --git a/src/main/services/DatabaseService.ts b/src/main/services/DatabaseService.ts index 87ba5f6..ca7cd24 100644 --- a/src/main/services/DatabaseService.ts +++ b/src/main/services/DatabaseService.ts @@ -35,10 +35,12 @@ export class DatabaseService { .db(db) .listCollections({}, { nameOnly: true, authorizedCollections: authOnly }) const items = await cursor.toArray() - return items.map((info) => ({ - name: info.name as string, - type: (info.type as 'collection' | 'view' | undefined) ?? 'collection' - })) + return items + .map((info) => ({ + name: info.name as string, + type: (info.type as 'collection' | 'view' | undefined) ?? 'collection' + })) + .sort((a, b) => a.name.localeCompare(b.name)) } async collectionStats(connectionId: string, db: string, coll: string): Promise { diff --git a/src/main/services/QueryService.ts b/src/main/services/QueryService.ts index 9502ca3..31b27b6 100644 --- a/src/main/services/QueryService.ts +++ b/src/main/services/QueryService.ts @@ -10,6 +10,8 @@ import type { DocumentEnvelope, FindRequest, FindResponse, + InsertManyRequest, + InsertManyResponse, InsertOneRequest, InsertOneResponse, ReplaceOneRequest, @@ -111,6 +113,14 @@ export class QueryService { return { insertedId: toCanonicalString(result.insertedId) } } + async insertMany(req: InsertManyRequest): Promise { + const client = this.connections.getClient(req.connectionId) + const coll = client.db(req.db).collection(req.coll) + const documents = req.documents.map(parseDocument) + const result = await coll.insertMany(documents) + return { insertedIds: Object.values(result.insertedIds).map((id) => toCanonicalString(id)) } + } + /** * Bulk delete by `_id`. No per-document hash check — the renderer * collects an explicit confirmation before calling this, so a stale diff --git a/src/preload/index.ts b/src/preload/index.ts index 8426c87..c8fd8e1 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -25,6 +25,8 @@ import type { FindRequest, FindResponse, IndexInfo, + InsertManyRequest, + InsertManyResponse, InsertOneRequest, InsertOneResponse, RenameCollectionPayload, @@ -77,6 +79,8 @@ const api: Api = { replaceOne: (request: ReplaceOneRequest) => invoke('query:replaceOne', request), insertOne: (request: InsertOneRequest) => invoke('query:insertOne', request), + insertMany: (request: InsertManyRequest) => + invoke('query:insertMany', request), deleteOne: (request: DeleteOneRequest) => invoke('query:deleteOne', request), deleteMany: (request: DeleteManyRequest) => invoke('query:deleteMany', request) diff --git a/src/renderer/src/features/document/DocumentEditorDialog.tsx b/src/renderer/src/features/document/DocumentEditorDialog.tsx index d139023..057f074 100644 --- a/src/renderer/src/features/document/DocumentEditorDialog.tsx +++ b/src/renderer/src/features/document/DocumentEditorDialog.tsx @@ -13,7 +13,7 @@ import { } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { api, ApiError } from '@/lib/api' -import { parseMongoQuery } from '@/lib/mongoQueryLang' +import { parseMongoDocuments, parseMongoQuery } from '@/lib/mongoQueryLang' import { serializeMongoValue } from '@/lib/mongoQuerySerialize' import type { DocumentEnvelope, UuidEncoding } from '@shared/types' @@ -36,7 +36,7 @@ const TITLES: Record = { view: 'View document', edit: 'Edit document', duplicate: 'Duplicate document', - insert: 'Insert document' + insert: 'Insert documents' } const DESCRIPTIONS: Record = { @@ -44,7 +44,7 @@ const DESCRIPTIONS: Record = { edit: 'Edit using mongo shell syntax — ObjectId("…"), ISODate("…"), NumberLong("…"), UUID/JUUID, regex literals.', duplicate: 'A copy with the original _id removed. Save inserts a new document with a fresh _id.', insert: - 'New document — mongo shell syntax. Leave _id out and the server will assign a fresh ObjectId.' + 'Multiple documents, one after another newline, comma-separated or JSON array. Leave _id out for a fresh ObjectId.' } const INSERT_TEMPLATE = '{\n \n}' @@ -112,16 +112,26 @@ export function DocumentEditorDialog({ setValue(mode === 'duplicate' ? stripIdShell(rendered, uuidEncoding, timezone) : rendered) }, [envelope, mode, uuidEncoding, timezone]) - const compiled = useMemo(() => { - if (mode === 'view' || mode === null) return { ok: true as const, ejson: '' } + const compiled = useMemo< + { ok: true; ejson: string; documents: string[] } | { ok: false; error: string } + >(() => { + if (mode === 'view' || mode === null) return { ok: true, ejson: '', documents: [] } + if (mode === 'insert') { + const parsed = parseMongoDocuments(value) + if (!parsed.ok) return { ok: false, error: parsed.error } + if (parsed.documents.length === 0) return { ok: false, error: 'Enter at least one document' } + return { ok: true, ejson: '', documents: parsed.documents } + } const parsed = parseMongoQuery(value) - if (!parsed.ok) return { ok: false as const, error: parsed.error } + if (!parsed.ok) return { ok: false, error: parsed.error } if (parsed.value === null || typeof parsed.value !== 'object' || Array.isArray(parsed.value)) { - return { ok: false as const, error: 'Document must be an object' } + return { ok: false, error: 'Document must be an object' } } - return { ok: true as const, ejson: parsed.ejson } + return { ok: true, ejson: parsed.ejson, documents: [] } }, [value, mode]) + const manyCount = compiled.ok ? compiled.documents.length : 0 + const saveMutation = useMutation({ mutationFn: async () => { if (!mode) throw new Error('No editor mode') @@ -137,7 +147,10 @@ export function DocumentEditorDialog({ replacement: compiled.ejson }) } - if (mode === 'duplicate' || mode === 'insert') { + if (mode === 'insert') { + return api.query.insertMany({ connectionId, db, coll, documents: compiled.documents }) + } + if (mode === 'duplicate') { return api.query.insertOne({ connectionId, db, coll, document: compiled.ejson }) } throw new Error('Cannot save in view mode') @@ -150,7 +163,7 @@ export function DocumentEditorDialog({ ? 'Document updated' : mode === 'duplicate' ? 'Document duplicated' - : 'Document inserted' + : `Inserted ${manyCount} document${manyCount === 1 ? '' : 's'}` toast.success(successMessage) onClose() }, @@ -172,7 +185,13 @@ export function DocumentEditorDialog({ const parseError = compiled.ok ? null : compiled.error const isReadOnly = mode === 'view' const ctaLabel = - mode === 'edit' ? 'Save changes' : mode === 'duplicate' ? 'Insert duplicate' : 'Insert document' + mode === 'edit' + ? 'Save changes' + : mode === 'duplicate' + ? 'Insert duplicate' + : manyCount > 0 + ? `Insert ${manyCount} document${manyCount === 1 ? '' : 's'}` + : 'Insert documents' return ( diff --git a/src/renderer/src/features/explorer/CollectionLeaf.tsx b/src/renderer/src/features/explorer/CollectionLeaf.tsx index e0460a2..f66a00e 100644 --- a/src/renderer/src/features/explorer/CollectionLeaf.tsx +++ b/src/renderer/src/features/explorer/CollectionLeaf.tsx @@ -144,7 +144,7 @@ export function CollectionLeaf({ setDialog('insert')} disabled={type === 'view'}> - Insert document… + Insert documents… setDialog('indexes')} disabled={type === 'view'}> diff --git a/src/renderer/src/lib/api.ts b/src/renderer/src/lib/api.ts index d39b347..249c3b3 100644 --- a/src/renderer/src/lib/api.ts +++ b/src/renderer/src/lib/api.ts @@ -23,6 +23,8 @@ import type { FindRequest, FindResponse, IndexInfo, + InsertManyRequest, + InsertManyResponse, InsertOneRequest, InsertOneResponse, RenameCollectionPayload, @@ -102,6 +104,8 @@ export const api = { unwrap(window.api.query.replaceOne(request)), insertOne: (request: InsertOneRequest): Promise => unwrap(window.api.query.insertOne(request)), + insertMany: (request: InsertManyRequest): Promise => + unwrap(window.api.query.insertMany(request)), deleteOne: (request: DeleteOneRequest): Promise => unwrap(window.api.query.deleteOne(request)), deleteMany: (request: DeleteManyRequest): Promise => diff --git a/src/renderer/src/lib/mongoQueryLang.test.ts b/src/renderer/src/lib/mongoQueryLang.test.ts index 37c435c..b7d45ec 100644 --- a/src/renderer/src/lib/mongoQueryLang.test.ts +++ b/src/renderer/src/lib/mongoQueryLang.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { parseMongoQuery } from './mongoQueryLang' +import { parseMongoDocuments, parseMongoQuery } from './mongoQueryLang' describe('parseMongoQuery', () => { it('returns empty ejson for blank input', () => { @@ -143,3 +143,85 @@ describe('parseMongoQuery', () => { }) }) }) + +describe('parseMongoDocuments', () => { + it('returns no documents for blank input', () => { + const r = parseMongoDocuments(' ') + expect(r.ok).toBe(true) + if (r.ok) expect(r.documents).toEqual([]) + }) + + it('parses a single document', () => { + const r = parseMongoDocuments('{ name: "ada" }') + expect(r.ok).toBe(true) + if (r.ok) expect(r.documents.map((d) => JSON.parse(d))).toEqual([{ name: 'ada' }]) + }) + + it('parses newline-separated documents without an array', () => { + const r = parseMongoDocuments('{ a: 1 }\n{ b: 2 }\n{ c: 3 }') + expect(r.ok).toBe(true) + if (r.ok) expect(r.documents.map((d) => JSON.parse(d))).toEqual([{ a: 1 }, { b: 2 }, { c: 3 }]) + }) + + it('tolerates comma separators between documents', () => { + const r = parseMongoDocuments('{ a: 1 },\n{ b: 2 },') + expect(r.ok).toBe(true) + if (r.ok) expect(r.documents.map((d) => JSON.parse(d))).toEqual([{ a: 1 }, { b: 2 }]) + }) + + it('treats commas as fully optional separators in any position', () => { + const inputs = [ + '{ a: 1 }\n{ b: 2 }', + '{ a: 1 },\n{ b: 2 },', + '{ a: 1 },\n{ b: 2 }', + ',{ a: 1 },,{ b: 2 },,' + ] + for (const input of inputs) { + const r = parseMongoDocuments(input) + expect(r.ok).toBe(true) + if (r.ok) expect(r.documents.map((d) => JSON.parse(d))).toEqual([{ a: 1 }, { b: 2 }]) + } + }) + + it('rewrites shell helpers inside each document', () => { + const r = parseMongoDocuments( + '{ _id: ObjectId("507f1f77bcf86cd799439011") }\n{ at: ISODate("2024-01-01T00:00:00Z") }' + ) + expect(r.ok).toBe(true) + if (r.ok) + expect(r.documents.map((d) => JSON.parse(d))).toEqual([ + { _id: { $oid: '507f1f77bcf86cd799439011' } }, + { at: { $date: '2024-01-01T00:00:00Z' } } + ]) + }) + + it('flattens a top-level array of documents', () => { + const r = parseMongoDocuments('[{ a: 1 }, { b: 2 }]') + expect(r.ok).toBe(true) + if (r.ok) expect(r.documents.map((d) => JSON.parse(d))).toEqual([{ a: 1 }, { b: 2 }]) + }) + + it('mixes bare documents and arrays', () => { + const r = parseMongoDocuments('{ a: 1 }\n[{ b: 2 }, { c: 3 }]\n{ d: 4 }') + expect(r.ok).toBe(true) + if (r.ok) + expect(r.documents.map((d) => JSON.parse(d))).toEqual([ + { a: 1 }, + { b: 2 }, + { c: 3 }, + { d: 4 } + ]) + }) + + it('rejects a non-object element inside an array', () => { + const r = parseMongoDocuments('[{ a: 1 }, 42]') + expect(r.ok).toBe(false) + if (!r.ok) expect(r.error).toMatch(/must be an object/) + }) + + it('rejects a non-object document in the sequence', () => { + const r = parseMongoDocuments('{ a: 1 }\n42') + expect(r.ok).toBe(false) + if (!r.ok) expect(r.error).toMatch(/must be an object/) + }) +}) diff --git a/src/renderer/src/lib/mongoQueryLang.ts b/src/renderer/src/lib/mongoQueryLang.ts index d9579a9..93a2766 100644 --- a/src/renderer/src/lib/mongoQueryLang.ts +++ b/src/renderer/src/lib/mongoQueryLang.ts @@ -52,6 +52,50 @@ export function parseMongoQuery(input: string): ParseResult { } } +export type DocumentsParseSuccess = { ok: true; documents: string[] } +export type DocumentsParseResult = DocumentsParseSuccess | ParseFailure + +export function parseMongoDocuments(input: string): DocumentsParseResult { + if (input.trim().length === 0) { + return { ok: true, documents: [] } + } + try { + const tokens = tokenize(input) + const parser = new Parser(tokens, input) + const documents: string[] = [] + const skipCommas = (): void => { + let sep = parser.peek() + while (sep && sep.type === 'punct' && sep.value === ',') { + parser.advance() + sep = parser.peek() + } + } + const pushDocument = (value: unknown, offset: number): void => { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new ParseError('Each document must be an object', offset) + } + documents.push(JSON.stringify(value)) + } + skipCommas() + while (parser.peek()) { + const offset = parser.peek()!.offset + const value = parser.parseValue() + if (Array.isArray(value)) { + for (const item of value) pushDocument(item, offset) + } else { + pushDocument(value, offset) + } + skipCommas() + } + return { ok: true, documents } + } catch (e) { + if (e instanceof ParseError) { + return { ok: false, error: e.message, offset: e.offset } + } + return { ok: false, error: e instanceof Error ? e.message : String(e), offset: 0 } + } +} + class ParseError extends Error { constructor( message: string, diff --git a/src/shared/api.ts b/src/shared/api.ts index 5ff3a79..17445ba 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -22,6 +22,8 @@ import type { FindRequest, FindResponse, IndexInfo, + InsertManyRequest, + InsertManyResponse, InsertOneRequest, InsertOneResponse, RenameCollectionPayload, @@ -86,6 +88,7 @@ export type Api = { count: (request: CountRequest) => Promise> replaceOne: (request: ReplaceOneRequest) => Promise> insertOne: (request: InsertOneRequest) => Promise> + insertMany: (request: InsertManyRequest) => Promise> deleteOne: (request: DeleteOneRequest) => Promise> deleteMany: (request: DeleteManyRequest) => Promise> } diff --git a/src/shared/schemas.ts b/src/shared/schemas.ts index 338ef90..127b734 100644 --- a/src/shared/schemas.ts +++ b/src/shared/schemas.ts @@ -221,6 +221,15 @@ export const InsertOneRequestSchema = z }) .strict() +export const InsertManyRequestSchema = z + .object({ + connectionId: z.string().uuid(), + db: dbName, + coll: collName, + documents: z.array(documentString).min(1).max(10_000) + }) + .strict() + export const DeleteOneRequestSchema = z .object({ connectionId: z.string().uuid(), diff --git a/src/shared/types.ts b/src/shared/types.ts index e154a7b..9d1a86e 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -354,6 +354,17 @@ export type InsertOneResponse = { insertedId: string } +export type InsertManyRequest = { + connectionId: string + db: string + coll: string + documents: string[] +} + +export type InsertManyResponse = { + insertedIds: string[] +} + export type DeleteOneRequest = { connectionId: string db: string From 5e843ace8dd7130f2ae8e7c4df7398b1f25fe46c Mon Sep 17 00:00:00 2001 From: "masel.io" Date: Thu, 25 Jun 2026 11:27:31 +0200 Subject: [PATCH 4/4] chore: bump version to 1.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2bd8541..9b1b55d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mongobench", - "version": "1.2.0", + "version": "1.3.0", "private": true, "description": "A modern, dark-mode-first MongoDB GUI.", "author": "ByteExceptionM",