From a28b5d5280699ca4dffb85434446fa6a1d5ec12e Mon Sep 17 00:00:00 2001 From: Loc Nguyen <113083148+thailoc-dev@users.noreply.github.com> Date: Sun, 4 Oct 2026 15:48:09 +0700 Subject: [PATCH] fix(mongodb): label sampled date and ObjectId fields with BSON types --- apps/desktop/src/lib/mongo/mongoCompletion.ts | 3 + ...nectionStore.mongoCompletionFields.spec.ts | 74 +++++++++++++++++++ apps/desktop/src/stores/connectionStore.ts | 3 +- packages/app-tests/mongoCompletion.test.ts | 29 ++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/mongo/mongoCompletion.ts b/apps/desktop/src/lib/mongo/mongoCompletion.ts index ef2bdf2351..ca2c4c8063 100644 --- a/apps/desktop/src/lib/mongo/mongoCompletion.ts +++ b/apps/desktop/src/lib/mongo/mongoCompletion.ts @@ -2331,10 +2331,13 @@ function collectFieldTypes(value: unknown, prefix: string, out: Map { expect(cached).toEqual(fields); expect(api.mongoFindDocuments).toHaveBeenCalledOnce(); }); + + it("infers types from extended_documents when returned by mongoAggregateDocuments", async () => { + const api = { + mongoAggregateDocuments: vi.fn().mockResolvedValue({ + documents: [{ _id: "65f0c0ffee00000000000001", createdAt: "2026-01-01T00:00:00Z", ref: "65f0c0ffee0000000000abcd" }], + extended_documents: [{ _id: { $oid: "65f0c0ffee00000000000001" }, createdAt: { $date: "2026-01-01T00:00:00Z" }, ref: { $oid: "65f0c0ffee0000000000abcd" } }], + }), + mongoFindDocuments: vi.fn(), + }; + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => api); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + const config = mongoConnection(); + store.addEphemeralConnection(config); + + const fields = await store.listMongoCompletionFields(config.id, "app", "users"); + + expect(fields).toEqual([ + { name: "_id", type: "objectId" }, + { name: "createdAt", type: "date" }, + { name: "ref", type: "objectId" }, + ]); + }); + + it("infers types from extended_documents when returned by mongoFindDocuments fallback", async () => { + const api = { + mongoAggregateDocuments: vi.fn().mockRejectedValue(new Error("Aggregation not supported")), + mongoFindDocuments: vi.fn().mockResolvedValue({ + documents: [{ _id: "65f0c0ffee00000000000001", createdAt: "2026-01-01T00:00:00Z", ref: "65f0c0ffee0000000000abcd" }], + extended_documents: [{ _id: { $oid: "65f0c0ffee00000000000001" }, createdAt: { $date: "2026-01-01T00:00:00Z" }, ref: { $oid: "65f0c0ffee0000000000abcd" } }], + }), + }; + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => api); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + const config = mongoConnection(); + store.addEphemeralConnection(config); + + const fields = await store.listMongoCompletionFields(config.id, "app", "users"); + + expect(fields).toEqual([ + { name: "_id", type: "objectId" }, + { name: "createdAt", type: "date" }, + { name: "ref", type: "objectId" }, + ]); + }); + + it("falls back to documents when extended_documents is absent or length mismatches", async () => { + const api = { + mongoAggregateDocuments: vi.fn().mockResolvedValue({ + documents: [{ _id: "1", fallbackField: "val" }], + extended_documents: [], + }), + mongoFindDocuments: vi.fn(), + }; + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => api); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + const config = mongoConnection(); + store.addEphemeralConnection(config); + + const fields = await store.listMongoCompletionFields(config.id, "app", "users"); + + expect(fields).toEqual([ + { name: "_id", type: "string" }, + { name: "fallbackField", type: "string" }, + ]); + }); }); diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 7755fd1c49..205d36f023 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -8817,7 +8817,8 @@ export const useConnectionStore = defineStore("connection", () => { } catch { result = await api.mongoFindDocuments(connectionId, database, collection, 0, 100, "{}"); } - const fields = inferMongoCompletionFields(result.documents ?? []); + const sampled = result.extended_documents?.length === result.documents.length ? result.extended_documents : result.documents; + const fields = inferMongoCompletionFields(sampled ?? []); mongoCompletionFieldsCache.value[cacheKey] = fields; evictOldestCacheEntries(mongoCompletionFieldsCache.value, COMPLETION_CACHE_MAX); return fields; diff --git a/packages/app-tests/mongoCompletion.test.ts b/packages/app-tests/mongoCompletion.test.ts index ea97649602..702cfe5cc8 100644 --- a/packages/app-tests/mongoCompletion.test.ts +++ b/packages/app-tests/mongoCompletion.test.ts @@ -1779,6 +1779,35 @@ test("treats extended JSON wrappers as scalars, not subdocuments", () => { assert.ok(inferred.find((field) => field.name === "profile.email" && field.type === "string")); }); +test("infers BSON types from extended JSON values and legacy ISODate strings", () => { + const inferred = inferMongoCompletionFields([ + { + createdAt: { $date: "2026-01-01T00:00:00Z" }, + ref: { $oid: "65f0c0ffee0000000000abcd" }, + price: { $numberDecimal: "1.5" }, + }, + ]); + + for (const phantom of ["createdAt.$date", "ref.$oid", "price.$numberDecimal"]) { + assert.equal( + inferred.some((field) => field.name === phantom), + false, + phantom, + ); + } + + assert.equal(inferred.find((field) => field.name === "createdAt")?.type, "date"); + assert.equal(inferred.find((field) => field.name === "ref")?.type, "objectId"); + assert.equal(inferred.find((field) => field.name === "price")?.type, "decimal128"); + + const legacyInferred = inferMongoCompletionFields([ + { + createdAt: 'ISODate("2026-01-01T00:00:00Z")', + }, + ]); + assert.equal(legacyInferred.find((field) => field.name === "createdAt")?.type, "date"); +}); + test("infers dotted MongoDB fields from sampled documents", () => { const inferred = inferMongoCompletionFields([ { _id: "1", profile: { email: "a@example.com" }, tags: ["a"] },