This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 8722cafee feat(bindings/nodejs): Add DeleteOptions support for new 
options API (#6349)
8722cafee is described below

commit 8722cafee760c946bf88e8de9e80128e7ed5ccce
Author: Kingsword <kingswor...@gmail.com>
AuthorDate: Fri Jul 11 15:46:43 2025 +0800

    feat(bindings/nodejs): Add DeleteOptions support for new options API (#6349)
---
 bindings/nodejs/generated.d.ts                     |  9 ++-
 bindings/nodejs/src/capability.rs                  |  6 ++
 bindings/nodejs/src/lib.rs                         | 21 +++--
 bindings/nodejs/src/options.rs                     | 14 ++++
 .../tests/suites/asyncDeleteOptions.suite.mjs      | 92 ++++++++++++++++++++++
 bindings/nodejs/tests/suites/index.mjs             |  4 +
 .../tests/suites/syncDeleteOptions.suite.mjs       | 92 ++++++++++++++++++++++
 7 files changed, 231 insertions(+), 7 deletions(-)

diff --git a/bindings/nodejs/generated.d.ts b/bindings/nodejs/generated.d.ts
index c442c09fc..82d4ad1e5 100644
--- a/bindings/nodejs/generated.d.ts
+++ b/bindings/nodejs/generated.d.ts
@@ -269,6 +269,9 @@ export interface ListOptions {
   */
   deleted?: boolean
 }
+export interface DeleteOptions {
+  version?: string
+}
 export const enum EntryMode {
   /** FILE means the path has data to read. */
   FILE = 0,
@@ -400,6 +403,8 @@ export class Capability {
   get createDir(): boolean
   /** If operator supports delete. */
   get delete(): boolean
+  /** If operator supports delete by version. */
+  get deleteWithVersion(): boolean
   /** If operator supports copy. */
   get copy(): boolean
   /** If operator supports rename. */
@@ -644,7 +649,7 @@ export class Operator {
    * await op.delete("test");
    * ```
    */
-  delete(path: string): Promise<void>
+  delete(path: string, options?: DeleteOptions | undefined | null): 
Promise<void>
   /**
    * Delete the given path synchronously.
    *
@@ -653,7 +658,7 @@ export class Operator {
    * op.deleteSync("test");
    * ```
    */
-  deleteSync(path: string): void
+  deleteSync(path: string, options?: DeleteOptions | undefined | null): void
   /**
    * Remove given paths.
    *
diff --git a/bindings/nodejs/src/capability.rs 
b/bindings/nodejs/src/capability.rs
index 4033895bc..e77a5787a 100644
--- a/bindings/nodejs/src/capability.rs
+++ b/bindings/nodejs/src/capability.rs
@@ -221,6 +221,12 @@ impl Capability {
         self.0.delete
     }
 
+    /// If operator supports delete by version.
+    #[napi(getter)]
+    pub fn delete_with_version(&self) -> bool {
+        self.0.delete_with_version
+    }
+
     /// If operator supports copy.
     #[napi(getter)]
     pub fn copy(&self) -> bool {
diff --git a/bindings/nodejs/src/lib.rs b/bindings/nodejs/src/lib.rs
index e9e0c25ba..cc862194d 100644
--- a/bindings/nodejs/src/lib.rs
+++ b/bindings/nodejs/src/lib.rs
@@ -27,7 +27,7 @@ use std::time::Duration;
 use futures::AsyncReadExt;
 use futures::TryStreamExt;
 use napi::bindgen_prelude::*;
-use opendal::options::{ListOptions, ReadOptions, ReaderOptions, StatOptions};
+use opendal::options::{DeleteOptions, ListOptions, ReadOptions, ReaderOptions, 
StatOptions};
 
 mod capability;
 mod options;
@@ -458,8 +458,16 @@ impl Operator {
     /// await op.delete("test");
     /// ```
     #[napi]
-    pub async fn delete(&self, path: String) -> Result<()> {
-        self.async_op.delete(&path).await.map_err(format_napi_error)
+    pub async fn delete(
+        &self,
+        path: String,
+        options: Option<options::DeleteOptions>,
+    ) -> Result<()> {
+        let options = options.map_or_else(DeleteOptions::default, 
DeleteOptions::from);
+        self.async_op
+            .delete_options(&path, options)
+            .await
+            .map_err(format_napi_error)
     }
 
     /// Delete the given path synchronously.
@@ -469,8 +477,11 @@ impl Operator {
     /// op.deleteSync("test");
     /// ```
     #[napi]
-    pub fn delete_sync(&self, path: String) -> Result<()> {
-        self.blocking_op.delete(&path).map_err(format_napi_error)
+    pub fn delete_sync(&self, path: String, options: 
Option<options::DeleteOptions>) -> Result<()> {
+        let options = options.map_or_else(DeleteOptions::default, 
DeleteOptions::from);
+        self.blocking_op
+            .delete_options(&path, options)
+            .map_err(format_napi_error)
     }
 
     /// Remove given paths.
diff --git a/bindings/nodejs/src/options.rs b/bindings/nodejs/src/options.rs
index d1b45fb4d..994f0d74b 100644
--- a/bindings/nodejs/src/options.rs
+++ b/bindings/nodejs/src/options.rs
@@ -391,3 +391,17 @@ impl From<ListOptions> for opendal::options::ListOptions {
         }
     }
 }
+
+#[napi(object)]
+#[derive(Default)]
+pub struct DeleteOptions {
+    pub version: Option<String>,
+}
+
+impl From<DeleteOptions> for opendal::options::DeleteOptions {
+    fn from(value: DeleteOptions) -> Self {
+        Self {
+            version: value.version,
+        }
+    }
+}
diff --git a/bindings/nodejs/tests/suites/asyncDeleteOptions.suite.mjs 
b/bindings/nodejs/tests/suites/asyncDeleteOptions.suite.mjs
new file mode 100644
index 000000000..fe2e21559
--- /dev/null
+++ b/bindings/nodejs/tests/suites/asyncDeleteOptions.suite.mjs
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { randomUUID } from 'node:crypto'
+import { test, describe, expect, assert } from 'vitest'
+
+import { generateBytes } from '../utils.mjs'
+
+/**
+ * @param {import("../../index").Operator} op
+ */
+export function run(op) {
+  const capability = op.capability()
+
+  describe.runIf(capability.write && capability.delete)('async delete 
options', () => {
+    test('test delete file', async () => {
+      const filename = `random_file_${randomUUID()}`
+      const content = generateBytes()
+
+      await op.write(filename, content)
+      await op.delete(filename)
+
+      assert.isFalse(await op.exists(filename))
+    })
+
+    test.runIf(capability.createDir)('test delete empty dir', async () => {
+      const dirname = `random_dir_${randomUUID()}/`
+      await op.createDir(dirname)
+      await op.delete(dirname)
+
+      assert.isFalse(await op.exists(dirname))
+    })
+
+    test.runIf(capability.createDir)('test delete not existing', async () => {
+      const filename = `random_file_${randomUUID()}`
+
+      await op.delete(filename)
+      assert.isFalse(await op.exists(filename))
+    })
+
+    test.runIf(capability.deleteWithVersion)('test delete with version', async 
() => {
+      const filename = `random_file_${randomUUID()}`
+      const content = generateBytes()
+
+      await op.write(filename, content)
+      const meta = await op.stat(filename)
+      const version = meta.version
+
+      await op.delete(filename)
+      assert.isFalse(await op.exists(filename))
+
+      const metadata = await op.stat(filename, { version })
+      expect(metadata.version).toBe(version)
+
+      await op.delete(filename, { version })
+
+      await expect(op.stat(filename, { version 
})).rejects.toThrowError('NotFound')
+    })
+
+    test.runIf(capability.deleteWithVersion)('test delete with not existing 
version', async () => {
+      const filename1 = `random_file_${randomUUID()}`
+      const content1 = generateBytes()
+
+      await op.write(filename1, content1)
+      const meta = await op.stat(filename1)
+      const version = meta.version
+
+      const filename2 = `random_file_${randomUUID()}`
+      const content2 = generateBytes()
+      await op.write(filename2, content2)
+
+      await op.delete(filename2, { version })
+      await op.delete(filename1)
+    })
+  })
+}
diff --git a/bindings/nodejs/tests/suites/index.mjs 
b/bindings/nodejs/tests/suites/index.mjs
index e012a6047..258a749ae 100644
--- a/bindings/nodejs/tests/suites/index.mjs
+++ b/bindings/nodejs/tests/suites/index.mjs
@@ -30,6 +30,8 @@ import { run as AsyncReadOptionsTestRun } from 
'./asyncReadOptions.suite.mjs'
 import { run as SyncReadOptionsTestRun } from './syncReadOptions.suite.mjs'
 import { run as AsyncListOptionsTestRun } from './asyncListOptions.suite.mjs'
 import { run as SyncListOptionsTestRun } from './syncListOptions.suite.mjs'
+import { run as AsyncDeleteOptionsTestRun } from 
'./asyncDeleteOptions.suite.mjs'
+import { run as SyncDeleteOptionsTestRun } from './syncDeleteOptions.suite.mjs'
 
 export function runner(testName, scheme) {
   if (!scheme) {
@@ -65,5 +67,7 @@ export function runner(testName, scheme) {
     SyncReadOptionsTestRun(operator)
     AsyncListOptionsTestRun(operator)
     SyncListOptionsTestRun(operator)
+    AsyncDeleteOptionsTestRun(operator)
+    SyncDeleteOptionsTestRun(operator)
   })
 }
diff --git a/bindings/nodejs/tests/suites/syncDeleteOptions.suite.mjs 
b/bindings/nodejs/tests/suites/syncDeleteOptions.suite.mjs
new file mode 100644
index 000000000..d6a98ed59
--- /dev/null
+++ b/bindings/nodejs/tests/suites/syncDeleteOptions.suite.mjs
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { randomUUID } from 'node:crypto'
+import { test, describe, expect, assert } from 'vitest'
+
+import { generateBytes } from '../utils.mjs'
+
+/**
+ * @param {import("../../index").Operator} op
+ */
+export function run(op) {
+  const capability = op.capability()
+
+  describe.runIf(capability.write && capability.delete)('async delete 
options', () => {
+    test('test delete file', () => {
+      const filename = `random_file_${randomUUID()}`
+      const content = generateBytes()
+
+      op.writeSync(filename, content)
+      op.deleteSync(filename)
+
+      assert.isFalse(op.existsSync(filename))
+    })
+
+    test.runIf(capability.createDir)('test delete empty dir', () => {
+      const dirname = `random_dir_${randomUUID()}/`
+      op.createDirSync(dirname)
+      op.deleteSync(dirname)
+
+      assert.isFalse(op.existsSync(dirname))
+    })
+
+    test.runIf(capability.createDir)('test delete not existing', () => {
+      const filename = `random_file_${randomUUID()}`
+
+      op.deleteSync(filename)
+      assert.isFalse(op.existsSync(filename))
+    })
+
+    test.runIf(capability.deleteWithVersion)('test delete with version', () => 
{
+      const filename = `random_file_${randomUUID()}`
+      const content = generateBytes()
+
+      op.writeSync(filename, content)
+      const meta = op.statSync(filename)
+      const version = meta.version
+
+      op.deleteSync(filename)
+      assert.isFalse(op.existsSync(filename))
+
+      const metadata = op.statSync(filename, { version })
+      expect(metadata.version).toBe(version)
+
+      op.deleteSync(filename, { version })
+
+      expect(() => op.statSync(filename, { version })).toThrowError('NotFound')
+    })
+
+    test.runIf(capability.deleteWithVersion)('test delete with not existing 
version', () => {
+      const filename1 = `random_file_${randomUUID()}`
+      const content1 = generateBytes()
+
+      op.writeSync(filename1, content1)
+      const meta = op.statSync(filename1)
+      const version = meta.version
+
+      const filename2 = `random_file_${randomUUID()}`
+      const content2 = generateBytes()
+      op.writeSync(filename2, content2)
+
+      op.deleteSync(filename2, { version })
+      op.deleteSync(filename1)
+    })
+  })
+}

Reply via email to