Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/gold-files-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect-mongodb": patch
---

fix(MongoClient): update tests for ConnectionString host parsing (#58)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be "Fix MongoClient.connect connection string parsing for error reporting"

3 changes: 2 additions & 1 deletion packages/effect-mongodb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 11 additions & 2 deletions packages/effect-mongodb/src/MongoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid installing another dependency just for error reporting.
This would be a breaking change for every user of the library, which is not ideal.

I would like to find other ways to parse the connection string, maybe there is something built-in in mongodb package? Or otherwise we can also consider an empty array for hosts as you always have the cause error in the MongoError.cause

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<ConnectionString, MongoParseError> =>
Effect.try({ try: () => new ConnectionString(url), catch: (e) => e as MongoParseError })

export const connect = (
url: string,
options?: MongoClientOptions
): Effect.Effect<MongoClient, MongoError.MongoError> =>
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: {
Expand Down
55 changes: 55 additions & 0 deletions packages/effect-mongodb/test/MongoClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
})
})
37 changes: 37 additions & 0 deletions pnpm-lock.yaml

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