From a0e9a59ea936a1d00ea184d1481ef612dc776d56 Mon Sep 17 00:00:00 2001 From: Botik Date: Fri, 3 Apr 2026 13:28:52 +0300 Subject: [PATCH] fix(MongoClient): use mongodb-connection-string-url for host parsing (#58) --- .changeset/gold-files-sin.md | 5 ++ packages/effect-mongodb/package.json | 3 +- packages/effect-mongodb/src/MongoClient.ts | 13 ++++- .../effect-mongodb/test/MongoClient.test.ts | 55 +++++++++++++++++++ pnpm-lock.yaml | 37 +++++++++++++ 5 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 .changeset/gold-files-sin.md diff --git a/.changeset/gold-files-sin.md b/.changeset/gold-files-sin.md new file mode 100644 index 0000000..c03aa22 --- /dev/null +++ b/.changeset/gold-files-sin.md @@ -0,0 +1,5 @@ +--- +"effect-mongodb": patch +--- + +fix(MongoClient): update tests for ConnectionString host parsing (#58) diff --git a/packages/effect-mongodb/package.json b/packages/effect-mongodb/package.json index 1ad7569..450b11a 100644 --- a/packages/effect-mongodb/package.json +++ b/packages/effect-mongodb/package.json @@ -47,7 +47,8 @@ }, "peerDependencies": { "effect": "^3.10.14", - "mongodb": "^6.9.0" + "mongodb": "^6.9.0", + "mongodb-connection-string-url": "^7.0.1" }, "devDependencies": { "effect": "^3.10.14", diff --git a/packages/effect-mongodb/src/MongoClient.ts b/packages/effect-mongodb/src/MongoClient.ts index fde7234..8c02424 100644 --- a/packages/effect-mongodb/src/MongoClient.ts +++ b/packages/effect-mongodb/src/MongoClient.ts @@ -5,21 +5,30 @@ import * as Data from "effect/Data" import * as Effect from "effect/Effect" import * as F from "effect/Function" import type * as Scope from "effect/Scope" -import type { DbOptions, MongoClientOptions } from "mongodb" +import type { DbOptions, MongoClientOptions, MongoParseError } from "mongodb" import { MongoClient as MongoClient_ } from "mongodb" +import { ConnectionString } from "mongodb-connection-string-url" import * as Db from "./Db.js" import { mongoErrorOrDie } from "./internal/mongo-error.js" import * as MongoError from "./MongoError.js" export class MongoClient extends Data.TaggedClass("MongoClient")<{ client: MongoClient_ }> {} +const parseHosts = (url: string): Effect.Effect => + Effect.try({ try: () => new ConnectionString(url), catch: (e) => e as MongoParseError }) + export const connect = ( url: string, options?: MongoClientOptions ): Effect.Effect => Effect.promise(() => MongoClient_.connect(url, options)).pipe( Effect.map((client) => new MongoClient({ client })), - Effect.catchAllDefect(mongoErrorOrDie(errorSource([new URL(url).host], "connect"))) + Effect.catchAllDefect((e) => + parseHosts(url).pipe( + Effect.catchAll((e) => Effect.succeed({ hosts: [e.message] })), + Effect.flatMap((cs) => mongoErrorOrDie(errorSource(cs.hosts, "connect"))(e)) + ) + ) ) export const close: { diff --git a/packages/effect-mongodb/test/MongoClient.test.ts b/packages/effect-mongodb/test/MongoClient.test.ts index aeff989..a7719f9 100644 --- a/packages/effect-mongodb/test/MongoClient.test.ts +++ b/packages/effect-mongodb/test/MongoClient.test.ts @@ -30,4 +30,59 @@ describe("MongoClient", () => { expect(result.message).not.toContain("pwd") } }) + + test("parse hosts from connection string", async () => { + const getErrorMessage = async (url: string) => + F.pipe( + MongoClient.connect(url, { + directConnection: true, + serverSelectionTimeoutMS: 200 + }), + Effect.catchAll(Effect.succeed), + Effect.runPromise + ) + + const singleHostMsg = await getErrorMessage("mongodb://localhost:27017") + expect(singleHostMsg).toBeInstanceOf(MongoError.MongoError) + if (singleHostMsg instanceof MongoError.MongoError) { + expect(singleHostMsg.message).toContain("localhost:27017") + } + const multiHostMsg = await getErrorMessage("mongodb://host1:27017,host2:27017,host3:27017") + expect(multiHostMsg).toBeInstanceOf(MongoError.MongoError) + if (multiHostMsg instanceof MongoError.MongoError) { + expect(multiHostMsg.message).toContain("host1:27017") + expect(multiHostMsg.message).toContain("host2:27017") + expect(multiHostMsg.message).toContain("host3:27017") + } + const srvMsg = await getErrorMessage("mongodb+srv://cluster.example.com") + expect(srvMsg).toBeInstanceOf(MongoError.MongoError) + if (srvMsg instanceof MongoError.MongoError) { + expect(srvMsg.message).toContain("cluster.example.com") + } + const withAuthMsg = await getErrorMessage("mongodb://user:pass@host1:27017,host2:27017/db?replicaSet=rs0") + expect(withAuthMsg).toBeInstanceOf(MongoError.MongoError) + if (withAuthMsg instanceof MongoError.MongoError) { + expect(withAuthMsg.message).not.toContain("user") + expect(withAuthMsg.message).not.toContain("pass") + expect(withAuthMsg.message).toContain("host1:27017") + expect(withAuthMsg.message).toContain("host2:27017") + } + }) + + test("parse invalid hosts from connection string", async () => { + const result = await F.pipe( + MongoClient.connect("mongodb//localhost:27017", { + directConnection: true, + serverSelectionTimeoutMS: 200 + }), + Effect.catchAllDefect(Effect.succeed), + Effect.runPromise + ) + + // const singleHostMsg = await getErrorMessage("mongodb://localhost:27017") + expect(result).toBeInstanceOf(Error) + if (result instanceof Error) { + expect(result.message).toContain("hosts Invalid scheme") + } + }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d98ce3a..afb5c85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,6 +143,10 @@ importers: version: 2.1.9(@types/node@22.7.5) packages/effect-mongodb: + dependencies: + mongodb-connection-string-url: + specifier: ^7.0.1 + version: 7.0.1 devDependencies: '@types/node': specifier: ^22.5.4 @@ -1042,6 +1046,9 @@ packages: '@types/whatwg-url@11.0.5': resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + '@types/whatwg-url@13.0.0': + resolution: {integrity: sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -2683,6 +2690,10 @@ packages: mongodb-connection-string-url@3.0.1: resolution: {integrity: sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==} + mongodb-connection-string-url@7.0.1: + resolution: {integrity: sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==} + engines: {node: '>=20.19.0'} + mongodb@6.12.0: resolution: {integrity: sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==} engines: {node: '>=16.20.1'} @@ -3383,6 +3394,10 @@ packages: resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} engines: {node: '>=14'} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -3580,6 +3595,10 @@ packages: resolution: {integrity: sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==} engines: {node: '>=16'} + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4516,6 +4535,10 @@ snapshots: dependencies: '@types/webidl-conversions': 7.0.3 + '@types/whatwg-url@13.0.0': + dependencies: + '@types/webidl-conversions': 7.0.3 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.32': @@ -6419,6 +6442,11 @@ snapshots: '@types/whatwg-url': 11.0.5 whatwg-url: 13.0.0 + mongodb-connection-string-url@7.0.1: + dependencies: + '@types/whatwg-url': 13.0.0 + whatwg-url: 14.2.0 + mongodb@6.12.0: dependencies: '@mongodb-js/saslprep': 1.1.9 @@ -7144,6 +7172,10 @@ snapshots: dependencies: punycode: 2.3.1 + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + ts-api-utils@1.3.0(typescript@5.6.2): dependencies: typescript: 5.6.2 @@ -7363,6 +7395,11 @@ snapshots: tr46: 4.1.1 webidl-conversions: 7.0.0 + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3