From e970aa803ecab25afdf8cb4773a7fb3c499b247b Mon Sep 17 00:00:00 2001 From: Brendan Clement Date: Tue, 12 May 2026 15:54:26 -0700 Subject: [PATCH] fix(nodejs): error on unrecognized namespace mode/behavior --- nodejs/__test__/connection.test.ts | 23 +++++++++++++++ nodejs/src/connection.rs | 47 ++++++++++++++++++++---------- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/nodejs/__test__/connection.test.ts b/nodejs/__test__/connection.test.ts index 48d74e81a..f03796e9b 100644 --- a/nodejs/__test__/connection.test.ts +++ b/nodejs/__test__/connection.test.ts @@ -373,4 +373,27 @@ describe("namespaces", () => { await db.createNamespace(["dup"]); await expect(db.createNamespace(["dup"])).rejects.toThrow(); }); + + it("should reject an unrecognized createNamespace mode with a clear error", async () => { + await expect( + // biome-ignore lint/suspicious/noExplicitAny: deliberately bypass TS to test runtime validation + db.createNamespace(["x"], { mode: "frobnicate" as any }), + ).rejects.toThrow(/Invalid mode 'frobnicate'/); + }); + + it("should reject an unrecognized dropNamespace mode with a clear error", async () => { + await db.createNamespace(["x"]); + await expect( + // biome-ignore lint/suspicious/noExplicitAny: deliberately bypass TS to test runtime validation + db.dropNamespace(["x"], { mode: "frobnicate" as any }), + ).rejects.toThrow(/Invalid mode 'frobnicate'/); + }); + + it("should reject an unrecognized dropNamespace behavior with a clear error", async () => { + await db.createNamespace(["x"]); + await expect( + // biome-ignore lint/suspicious/noExplicitAny: deliberately bypass TS to test runtime validation + db.dropNamespace(["x"], { behavior: "frobnicate" as any }), + ).rejects.toThrow(/Invalid behavior 'frobnicate'/); + }); }); diff --git a/nodejs/src/connection.rs b/nodejs/src/connection.rs index af3b3d983..058b74f96 100644 --- a/nodejs/src/connection.rs +++ b/nodejs/src/connection.rs @@ -353,12 +353,17 @@ impl Connection { mode: Option, properties: Option>, ) -> napi::Result { - let mode_str = mode.and_then(|m| match m.to_lowercase().as_str() { - "create" => Some("Create".to_string()), - "exist_ok" => Some("ExistOk".to_string()), - "overwrite" => Some("Overwrite".to_string()), - _ => None, - }); + let mode_str = mode + .map(|m| match m.to_lowercase().as_str() { + "create" => Ok("Create".to_string()), + "exist_ok" => Ok("ExistOk".to_string()), + "overwrite" => Ok("Overwrite".to_string()), + _ => Err(napi::Error::from_reason(format!( + "Invalid mode '{}': expected one of 'create', 'exist_ok', 'overwrite'", + m + ))), + }) + .transpose()?; let req = CreateNamespaceRequest { id: Some(namespace_path), mode: mode_str, @@ -384,16 +389,26 @@ impl Connection { mode: Option, behavior: Option, ) -> napi::Result { - let mode_str = mode.and_then(|m| match m.to_lowercase().as_str() { - "skip" => Some("Skip".to_string()), - "fail" => Some("Fail".to_string()), - _ => None, - }); - let behavior_str = behavior.and_then(|b| match b.to_lowercase().as_str() { - "restrict" => Some("Restrict".to_string()), - "cascade" => Some("Cascade".to_string()), - _ => None, - }); + let mode_str = mode + .map(|m| match m.to_lowercase().as_str() { + "skip" => Ok("Skip".to_string()), + "fail" => Ok("Fail".to_string()), + _ => Err(napi::Error::from_reason(format!( + "Invalid mode '{}': expected one of 'skip', 'fail'", + m + ))), + }) + .transpose()?; + let behavior_str = behavior + .map(|b| match b.to_lowercase().as_str() { + "restrict" => Ok("Restrict".to_string()), + "cascade" => Ok("Cascade".to_string()), + _ => Err(napi::Error::from_reason(format!( + "Invalid behavior '{}': expected one of 'restrict', 'cascade'", + b + ))), + }) + .transpose()?; let req = DropNamespaceRequest { id: Some(namespace_path), mode: mode_str,