fix(mongodb): label sampled date and ObjectId fields with BSON types

This commit is contained in:
Loc Nguyen
2026-10-04 16:48:09 +08:00
committed by GitHub
parent ee2573f7db
commit a28b5d5280
4 changed files with 108 additions and 1 deletions
@@ -2331,10 +2331,13 @@ function collectFieldTypes(value: unknown, prefix: string, out: Map<string, Set<
}
}
const LEGACY_ISODATE_PATTERN = /^ISODate\("[^"]*"\)$/;
function describeMongoValueType(value: unknown): string {
if (value == null) return "null";
if (Array.isArray(value)) return "array";
if (value instanceof Date) return "date";
if (typeof value === "string" && LEGACY_ISODATE_PATTERN.test(value)) return "date";
return mongoExtendedJsonValueType(value) ?? (typeof value === "object" ? "object" : typeof value);
}
@@ -102,4 +102,78 @@ describe("connectionStore listMongoCompletionFields", () => {
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" },
]);
});
});
+2 -1
View File
@@ -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;
+29
View File
@@ -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"] },