briefcase-node 2.4.1

Node.js bindings for Briefcase AI
Documentation
//! Node.js bindings for storage backends

use napi::{Result, bindgen_prelude::*, Task, Env, JsFunction};
use briefcase_core::storage::{SqliteBackend, LakeFSBackend, StorageBackend};
use crate::models::{JsDecisionSnapshot, JsSnapshot, JsSnapshotQuery};

/// Node.js wrapper for SqliteBackend
#[napi]
pub struct JsSqliteBackend {
    inner: SqliteBackend,
}

#[napi]
impl JsSqliteBackend {
    #[napi(constructor)]
    pub fn new(path: Option<String>) -> Result<Self> {
        let backend = if let Some(path) = path {
            SqliteBackend::new(path)
        } else {
            SqliteBackend::in_memory()
        };

        match backend {
            Ok(backend) => Ok(Self { inner: backend }),
            Err(e) => Err(napi::Error::from_reason(format!("Failed to create SQLite backend: {}", e))),
        }
    }

    #[napi(factory)]
    pub fn in_memory() -> Result<Self> {
        match SqliteBackend::in_memory() {
            Ok(backend) => Ok(Self { inner: backend }),
            Err(e) => Err(napi::Error::from_reason(format!("Failed to create in-memory SQLite backend: {}", e))),
        }
    }

    #[napi]
    pub fn save(&self, env: Env, snapshot: &JsSnapshot) -> Result<napi::JsObject> {
        let backend = self.inner.clone();
        let snapshot_inner = snapshot.inner.clone();

        let task = SaveSnapshotTask {
            backend,
            snapshot: snapshot_inner,
        };

        env.spawn(task).map(|async_work| async_work.promise_object())
    }

    #[napi]
    pub fn save_decision(&self, env: Env, decision: &JsDecisionSnapshot) -> Result<napi::JsObject> {
        let backend = self.inner.clone();
        let decision_inner = decision.inner.clone();

        let task = SaveDecisionTask {
            backend,
            decision: decision_inner,
        };

        env.spawn(task).map(|async_work| async_work.promise_object())
    }

    #[napi]
    pub fn load(&self, env: Env, snapshot_id: String) -> Result<napi::JsObject> {
        let backend = self.inner.clone();

        let task = LoadSnapshotTask {
            backend,
            snapshot_id,
        };

        env.spawn(task).map(|async_work| async_work.promise_object())
    }

    #[napi]
    pub fn load_decision(&self, env: Env, decision_id: String) -> Result<napi::JsObject> {
        let backend = self.inner.clone();

        let task = LoadDecisionTask {
            backend,
            decision_id,
        };

        env.spawn(task).map(|async_work| async_work.promise_object())
    }

    #[napi]
    pub fn query(&self, env: Env, query: &JsSnapshotQuery) -> Result<napi::JsObject> {
        let backend = self.inner.clone();
        let query_inner = query.inner.clone();

        let task = QuerySnapshotsTask {
            backend,
            query: query_inner,
        };

        env.spawn(task).map(|async_work| async_work.promise_object())
    }

    #[napi]
    pub fn delete(&self, env: Env, snapshot_id: String) -> Result<napi::JsObject> {
        let backend = self.inner.clone();

        let task = DeleteSnapshotTask {
            backend,
            snapshot_id,
        };

        env.spawn(task).map(|async_work| async_work.promise_object())
    }

    #[napi]
    pub fn health_check(&self, env: Env) -> Result<napi::JsObject> {
        let backend = self.inner.clone();

        let task = HealthCheckTask {
            backend,
        };

        env.spawn(task).map(|async_work| async_work.promise_object())
    }
}

/// Node.js wrapper for LakeFSBackend
#[napi]
pub struct JsLakeFSBackend {
    inner: LakeFSBackend,
}

#[napi]
impl JsLakeFSBackend {
    #[napi(constructor)]
    pub fn new(endpoint: String, repository: String, branch: String, access_key: String, secret_key: String) -> Result<Self> {
        match LakeFSBackend::new(endpoint, repository, branch, access_key, secret_key) {
            Ok(backend) => Ok(Self { inner: backend }),
            Err(e) => Err(napi::Error::from_reason(format!("Failed to create LakeFS backend: {}", e))),
        }
    }

    #[napi]
    pub fn save(&self, env: Env, snapshot: &JsSnapshot) -> Result<napi::JsObject> {
        let backend = self.inner.clone();
        let snapshot_inner = snapshot.inner.clone();

        let task = SaveSnapshotTask {
            backend,
            snapshot: snapshot_inner,
        };

        env.spawn(task).map(|async_work| async_work.promise_object())
    }

    #[napi]
    pub fn save_decision(&self, env: Env, decision: &JsDecisionSnapshot) -> Result<napi::JsObject> {
        let backend = self.inner.clone();
        let decision_inner = decision.inner.clone();

        let task = SaveDecisionTask {
            backend,
            decision: decision_inner,
        };

        env.spawn(task).map(|async_work| async_work.promise_object())
    }

    #[napi]
    pub fn flush(&self, env: Env) -> Result<napi::JsObject> {
        let backend = self.inner.clone();

        let task = FlushTask {
            backend,
        };

        env.spawn(task).map(|async_work| async_work.promise_object())
    }
}

// Async task implementations

struct SaveSnapshotTask<T: StorageBackend + Send + Sync + 'static> {
    backend: T,
    snapshot: briefcase_core::Snapshot,
}

#[napi]
impl<T: StorageBackend + Send + Sync + 'static> Task for SaveSnapshotTask<T> {
    type Output = String;
    type JsValue = String;

    fn compute(&mut self) -> Result<Self::Output> {
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| napi::Error::from_reason(e.to_string()))?;

        rt.block_on(async {
            self.backend.save(&self.snapshot)
                .await
                .map_err(|e| napi::Error::from_reason(e.to_string()))
        })
    }

    fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
        Ok(output)
    }
}

struct SaveDecisionTask<T: StorageBackend + Send + Sync + 'static> {
    backend: T,
    decision: briefcase_core::DecisionSnapshot,
}

#[napi]
impl<T: StorageBackend + Send + Sync + 'static> Task for SaveDecisionTask<T> {
    type Output = String;
    type JsValue = String;

    fn compute(&mut self) -> Result<Self::Output> {
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| napi::Error::from_reason(e.to_string()))?;

        rt.block_on(async {
            self.backend.save_decision(&self.decision)
                .await
                .map_err(|e| napi::Error::from_reason(e.to_string()))
        })
    }

    fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
        Ok(output)
    }
}

struct LoadSnapshotTask<T: StorageBackend + Send + Sync + 'static> {
    backend: T,
    snapshot_id: String,
}

#[napi]
impl<T: StorageBackend + Send + Sync + 'static> Task for LoadSnapshotTask<T> {
    type Output = briefcase_core::Snapshot;
    type JsValue = JsSnapshot;

    fn compute(&mut self) -> Result<Self::Output> {
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| napi::Error::from_reason(e.to_string()))?;

        rt.block_on(async {
            self.backend.load(&self.snapshot_id)
                .await
                .map_err(|e| napi::Error::from_reason(e.to_string()))
        })
    }

    fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
        Ok(JsSnapshot { inner: output })
    }
}

struct LoadDecisionTask<T: StorageBackend + Send + Sync + 'static> {
    backend: T,
    decision_id: String,
}

#[napi]
impl<T: StorageBackend + Send + Sync + 'static> Task for LoadDecisionTask<T> {
    type Output = briefcase_core::DecisionSnapshot;
    type JsValue = JsDecisionSnapshot;

    fn compute(&mut self) -> Result<Self::Output> {
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| napi::Error::from_reason(e.to_string()))?;

        rt.block_on(async {
            self.backend.load_decision(&self.decision_id)
                .await
                .map_err(|e| napi::Error::from_reason(e.to_string()))
        })
    }

    fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
        Ok(JsDecisionSnapshot { inner: output })
    }
}

struct QuerySnapshotsTask<T: StorageBackend + Send + Sync + 'static> {
    backend: T,
    query: briefcase_core::storage::SnapshotQuery,
}

#[napi]
impl<T: StorageBackend + Send + Sync + 'static> Task for QuerySnapshotsTask<T> {
    type Output = Vec<briefcase_core::Snapshot>;
    type JsValue = Vec<JsSnapshot>;

    fn compute(&mut self) -> Result<Self::Output> {
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| napi::Error::from_reason(e.to_string()))?;

        rt.block_on(async {
            self.backend.query(self.query.clone())
                .await
                .map_err(|e| napi::Error::from_reason(e.to_string()))
        })
    }

    fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
        Ok(output.into_iter().map(|snapshot| JsSnapshot { inner: snapshot }).collect())
    }
}

struct DeleteSnapshotTask<T: StorageBackend + Send + Sync + 'static> {
    backend: T,
    snapshot_id: String,
}

#[napi]
impl<T: StorageBackend + Send + Sync + 'static> Task for DeleteSnapshotTask<T> {
    type Output = bool;
    type JsValue = bool;

    fn compute(&mut self) -> Result<Self::Output> {
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| napi::Error::from_reason(e.to_string()))?;

        rt.block_on(async {
            self.backend.delete(&self.snapshot_id)
                .await
                .map_err(|e| napi::Error::from_reason(e.to_string()))
        })
    }

    fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
        Ok(output)
    }
}

struct HealthCheckTask<T: StorageBackend + Send + Sync + 'static> {
    backend: T,
}

#[napi]
impl<T: StorageBackend + Send + Sync + 'static> Task for HealthCheckTask<T> {
    type Output = bool;
    type JsValue = bool;

    fn compute(&mut self) -> Result<Self::Output> {
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| napi::Error::from_reason(e.to_string()))?;

        rt.block_on(async {
            self.backend.health_check()
                .await
                .map_err(|e| napi::Error::from_reason(e.to_string()))
        })
    }

    fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
        Ok(output)
    }
}

struct FlushTask<T: StorageBackend + Send + Sync + 'static> {
    backend: T,
}

#[napi]
impl<T: StorageBackend + Send + Sync + 'static> Task for FlushTask<T> {
    type Output = briefcase_core::storage::FlushResult;
    type JsValue = serde_json::Value;

    fn compute(&mut self) -> Result<Self::Output> {
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| napi::Error::from_reason(e.to_string()))?;

        rt.block_on(async {
            self.backend.flush()
                .await
                .map_err(|e| napi::Error::from_reason(e.to_string()))
        })
    }

    fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
        Ok(serde_json::json!({
            "snapshots_written": output.snapshots_written,
            "bytes_written": output.bytes_written,
            "checkpoint_id": output.checkpoint_id
        }))
    }
}