|
|
@@ -0,0 +1,2048 @@
|
|
|
+#include "script_engine.hpp"
|
|
|
+#include "common/uuid.hpp"
|
|
|
+#include "logging/logger.hpp"
|
|
|
+#include "runner/imap/imap_client.hpp"
|
|
|
+#include <quickjs.h>
|
|
|
+#include <chrono>
|
|
|
+#include <thread>
|
|
|
+#include <curl/curl.h>
|
|
|
+#include <sstream>
|
|
|
+#include <iomanip>
|
|
|
+#include <algorithm>
|
|
|
+#include <regex>
|
|
|
+#include <openssl/sha.h>
|
|
|
+#include <openssl/evp.h>
|
|
|
+#include <openssl/buffer.h>
|
|
|
+
|
|
|
+namespace smartbotic::runner::engine {
|
|
|
+
|
|
|
+using namespace common;
|
|
|
+
|
|
|
+// Base64 encoding helper
|
|
|
+static std::string base64Encode(const std::string& input) {
|
|
|
+ BIO* bio = BIO_new(BIO_s_mem());
|
|
|
+ BIO* b64 = BIO_new(BIO_f_base64());
|
|
|
+ BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
|
|
|
+ bio = BIO_push(b64, bio);
|
|
|
+
|
|
|
+ BIO_write(bio, input.data(), static_cast<int>(input.size()));
|
|
|
+ BIO_flush(bio);
|
|
|
+
|
|
|
+ BUF_MEM* bufferPtr;
|
|
|
+ BIO_get_mem_ptr(bio, &bufferPtr);
|
|
|
+
|
|
|
+ std::string result(bufferPtr->data, bufferPtr->length);
|
|
|
+ BIO_free_all(bio);
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+// Base64 decoding helper
|
|
|
+static std::string base64Decode(const std::string& input) {
|
|
|
+ BIO* bio = BIO_new_mem_buf(input.data(), static_cast<int>(input.size()));
|
|
|
+ BIO* b64 = BIO_new(BIO_f_base64());
|
|
|
+ BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
|
|
|
+ bio = BIO_push(b64, bio);
|
|
|
+
|
|
|
+ std::string result(input.size(), '\0');
|
|
|
+ int decoded_length = BIO_read(bio, result.data(), static_cast<int>(input.size()));
|
|
|
+
|
|
|
+ BIO_free_all(bio);
|
|
|
+
|
|
|
+ if (decoded_length > 0) {
|
|
|
+ result.resize(decoded_length);
|
|
|
+ } else {
|
|
|
+ result.clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+// SHA256 hash helper - returns hex string
|
|
|
+static std::string sha256Hash(const std::string& input) {
|
|
|
+ unsigned char hash[SHA256_DIGEST_LENGTH];
|
|
|
+ EVP_MD_CTX* ctx = EVP_MD_CTX_new();
|
|
|
+
|
|
|
+ EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr);
|
|
|
+ EVP_DigestUpdate(ctx, input.data(), input.size());
|
|
|
+ EVP_DigestFinal_ex(ctx, hash, nullptr);
|
|
|
+ EVP_MD_CTX_free(ctx);
|
|
|
+
|
|
|
+ // Convert to hex string
|
|
|
+ std::ostringstream ss;
|
|
|
+ for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
|
|
|
+ ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(hash[i]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return ss.str();
|
|
|
+}
|
|
|
+
|
|
|
+// Helper structure for curl response
|
|
|
+struct CurlResponse {
|
|
|
+ std::string body;
|
|
|
+ std::string headers;
|
|
|
+ long status_code = 0;
|
|
|
+};
|
|
|
+
|
|
|
+// Curl write callback for response body
|
|
|
+static size_t curlWriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata) {
|
|
|
+ auto* response = static_cast<std::string*>(userdata);
|
|
|
+ size_t total = size * nmemb;
|
|
|
+ response->append(ptr, total);
|
|
|
+ return total;
|
|
|
+}
|
|
|
+
|
|
|
+// Curl header callback
|
|
|
+static size_t curlHeaderCallback(char* buffer, size_t size, size_t nitems, void* userdata) {
|
|
|
+ auto* headers = static_cast<std::string*>(userdata);
|
|
|
+ size_t total = size * nitems;
|
|
|
+ headers->append(buffer, total);
|
|
|
+ return total;
|
|
|
+}
|
|
|
+
|
|
|
+// Parse raw headers into JSON object
|
|
|
+static nlohmann::json parseHeaders(const std::string& raw_headers) {
|
|
|
+ nlohmann::json headers = nlohmann::json::object();
|
|
|
+ std::istringstream stream(raw_headers);
|
|
|
+ std::string line;
|
|
|
+
|
|
|
+ while (std::getline(stream, line)) {
|
|
|
+ // Remove \r if present
|
|
|
+ if (!line.empty() && line.back() == '\r') {
|
|
|
+ line.pop_back();
|
|
|
+ }
|
|
|
+ // Skip empty lines and status line
|
|
|
+ if (line.empty() || line.find("HTTP/") == 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // Find colon separator
|
|
|
+ size_t colon = line.find(':');
|
|
|
+ if (colon != std::string::npos) {
|
|
|
+ std::string key = line.substr(0, colon);
|
|
|
+ std::string value = line.substr(colon + 1);
|
|
|
+ // Trim leading whitespace from value
|
|
|
+ size_t start = value.find_first_not_of(" \t");
|
|
|
+ if (start != std::string::npos) {
|
|
|
+ value = value.substr(start);
|
|
|
+ }
|
|
|
+ // Convert header name to lowercase for consistency
|
|
|
+ std::transform(key.begin(), key.end(), key.begin(), ::tolower);
|
|
|
+ headers[key] = value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return headers;
|
|
|
+}
|
|
|
+
|
|
|
+// Perform HTTP request using curl
|
|
|
+static CurlResponse performHttpRequest(
|
|
|
+ const std::string& method,
|
|
|
+ const std::string& url,
|
|
|
+ const nlohmann::json& headers,
|
|
|
+ const std::string& body,
|
|
|
+ long timeout_ms,
|
|
|
+ bool follow_redirects
|
|
|
+) {
|
|
|
+ CurlResponse response;
|
|
|
+
|
|
|
+ CURL* curl = curl_easy_init();
|
|
|
+ if (!curl) {
|
|
|
+ throw std::runtime_error("Failed to initialize curl");
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string response_body;
|
|
|
+ std::string response_headers;
|
|
|
+
|
|
|
+ // Set URL
|
|
|
+ curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
|
|
+
|
|
|
+ // Set method
|
|
|
+ if (method == "POST") {
|
|
|
+ curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
|
|
+ } else if (method == "PUT") {
|
|
|
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
|
|
|
+ } else if (method == "PATCH") {
|
|
|
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
|
|
|
+ } else if (method == "DELETE") {
|
|
|
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
|
|
|
+ } else if (method == "HEAD") {
|
|
|
+ curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
|
|
|
+ } else if (method == "OPTIONS") {
|
|
|
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "OPTIONS");
|
|
|
+ }
|
|
|
+ // GET is default
|
|
|
+
|
|
|
+ // Set headers
|
|
|
+ struct curl_slist* header_list = nullptr;
|
|
|
+ if (headers.is_object()) {
|
|
|
+ for (auto& [key, value] : headers.items()) {
|
|
|
+ if (value.is_string()) {
|
|
|
+ std::string header = key + ": " + value.get<std::string>();
|
|
|
+ header_list = curl_slist_append(header_list, header.c_str());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (header_list) {
|
|
|
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set body
|
|
|
+ if (!body.empty() && (method == "POST" || method == "PUT" || method == "PATCH")) {
|
|
|
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
|
|
|
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set timeout
|
|
|
+ if (timeout_ms > 0) {
|
|
|
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set redirect following
|
|
|
+ if (follow_redirects) {
|
|
|
+ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
|
+ curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 10L);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set callbacks
|
|
|
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback);
|
|
|
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_body);
|
|
|
+ curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, curlHeaderCallback);
|
|
|
+ curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_headers);
|
|
|
+
|
|
|
+ // Perform request
|
|
|
+ CURLcode res = curl_easy_perform(curl);
|
|
|
+
|
|
|
+ if (res != CURLE_OK) {
|
|
|
+ std::string error_msg = curl_easy_strerror(res);
|
|
|
+ if (header_list) curl_slist_free_all(header_list);
|
|
|
+ curl_easy_cleanup(curl);
|
|
|
+ throw std::runtime_error("HTTP request failed: " + error_msg);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get status code
|
|
|
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response.status_code);
|
|
|
+
|
|
|
+ response.body = std::move(response_body);
|
|
|
+ response.headers = std::move(response_headers);
|
|
|
+
|
|
|
+ if (header_list) curl_slist_free_all(header_list);
|
|
|
+ curl_easy_cleanup(curl);
|
|
|
+
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+// JavaScript HTTP request function
|
|
|
+static JSValue js_http_request(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
+ if (argc < 1 || !JS_IsObject(argv[0])) {
|
|
|
+ return JS_ThrowTypeError(ctx, "http.request requires an options object");
|
|
|
+ }
|
|
|
+
|
|
|
+ JSValue options = argv[0];
|
|
|
+
|
|
|
+ // Get method (default GET)
|
|
|
+ std::string method = "GET";
|
|
|
+ JSValue method_val = JS_GetPropertyStr(ctx, options, "method");
|
|
|
+ if (!JS_IsUndefined(method_val)) {
|
|
|
+ const char* method_str = JS_ToCString(ctx, method_val);
|
|
|
+ if (method_str) {
|
|
|
+ method = method_str;
|
|
|
+ JS_FreeCString(ctx, method_str);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, method_val);
|
|
|
+
|
|
|
+ // Get URL (required)
|
|
|
+ std::string url;
|
|
|
+ JSValue url_val = JS_GetPropertyStr(ctx, options, "url");
|
|
|
+ if (JS_IsUndefined(url_val)) {
|
|
|
+ JS_FreeValue(ctx, url_val);
|
|
|
+ return JS_ThrowTypeError(ctx, "http.request requires a url");
|
|
|
+ }
|
|
|
+ const char* url_str = JS_ToCString(ctx, url_val);
|
|
|
+ if (url_str) {
|
|
|
+ url = url_str;
|
|
|
+ JS_FreeCString(ctx, url_str);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, url_val);
|
|
|
+
|
|
|
+ // Get headers (optional)
|
|
|
+ nlohmann::json headers = nlohmann::json::object();
|
|
|
+ JSValue headers_val = JS_GetPropertyStr(ctx, options, "headers");
|
|
|
+ if (JS_IsObject(headers_val)) {
|
|
|
+ JSValue json_str = JS_JSONStringify(ctx, headers_val, JS_UNDEFINED, JS_UNDEFINED);
|
|
|
+ const char* str = JS_ToCString(ctx, json_str);
|
|
|
+ if (str) {
|
|
|
+ try {
|
|
|
+ headers = nlohmann::json::parse(str);
|
|
|
+ } catch (...) {}
|
|
|
+ JS_FreeCString(ctx, str);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, json_str);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, headers_val);
|
|
|
+
|
|
|
+ // Get body (optional)
|
|
|
+ std::string body;
|
|
|
+ JSValue body_val = JS_GetPropertyStr(ctx, options, "body");
|
|
|
+ if (!JS_IsUndefined(body_val) && !JS_IsNull(body_val)) {
|
|
|
+ if (JS_IsString(body_val)) {
|
|
|
+ const char* body_str = JS_ToCString(ctx, body_val);
|
|
|
+ if (body_str) {
|
|
|
+ body = body_str;
|
|
|
+ JS_FreeCString(ctx, body_str);
|
|
|
+ }
|
|
|
+ } else if (JS_IsObject(body_val)) {
|
|
|
+ // Stringify object body
|
|
|
+ JSValue json_str = JS_JSONStringify(ctx, body_val, JS_UNDEFINED, JS_UNDEFINED);
|
|
|
+ const char* str = JS_ToCString(ctx, json_str);
|
|
|
+ if (str) {
|
|
|
+ body = str;
|
|
|
+ JS_FreeCString(ctx, str);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, json_str);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, body_val);
|
|
|
+
|
|
|
+ // Get timeout (default 30000ms)
|
|
|
+ long timeout_ms = 30000;
|
|
|
+ JSValue timeout_val = JS_GetPropertyStr(ctx, options, "timeout");
|
|
|
+ if (JS_IsNumber(timeout_val)) {
|
|
|
+ double t;
|
|
|
+ JS_ToFloat64(ctx, &t, timeout_val);
|
|
|
+ timeout_ms = static_cast<long>(t);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, timeout_val);
|
|
|
+
|
|
|
+ // Get followRedirects (default true)
|
|
|
+ bool follow_redirects = true;
|
|
|
+ JSValue follow_val = JS_GetPropertyStr(ctx, options, "followRedirects");
|
|
|
+ if (JS_IsBool(follow_val)) {
|
|
|
+ follow_redirects = JS_ToBool(ctx, follow_val);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, follow_val);
|
|
|
+
|
|
|
+ // Perform the request
|
|
|
+ try {
|
|
|
+ CurlResponse response = performHttpRequest(method, url, headers, body, timeout_ms, follow_redirects);
|
|
|
+
|
|
|
+ // Create response object
|
|
|
+ JSValue result = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, result, "status", JS_NewInt32(ctx, static_cast<int32_t>(response.status_code)));
|
|
|
+
|
|
|
+ // Parse response headers
|
|
|
+ nlohmann::json resp_headers = parseHeaders(response.headers);
|
|
|
+ std::string headers_json = resp_headers.dump();
|
|
|
+ JSValue headers_obj = JS_ParseJSON(ctx, headers_json.c_str(), headers_json.size(), "<headers>");
|
|
|
+ JS_SetPropertyStr(ctx, result, "headers", headers_obj);
|
|
|
+
|
|
|
+ // Try to parse body as JSON, fall back to string
|
|
|
+ std::string content_type;
|
|
|
+ if (resp_headers.contains("content-type")) {
|
|
|
+ content_type = resp_headers["content-type"].get<std::string>();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (content_type.find("application/json") != std::string::npos && !response.body.empty()) {
|
|
|
+ JSValue data = JS_ParseJSON(ctx, response.body.c_str(), response.body.size(), "<body>");
|
|
|
+ if (JS_IsException(data)) {
|
|
|
+ // JSON parse failed, use as string
|
|
|
+ JS_FreeValue(ctx, JS_GetException(ctx));
|
|
|
+ JS_SetPropertyStr(ctx, result, "data", JS_NewString(ctx, response.body.c_str()));
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, result, "data", data);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, result, "data", JS_NewString(ctx, response.body.c_str()));
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ return JS_ThrowInternalError(ctx, "%s", e.what());
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Interrupt handler returns int (0 to continue, non-zero to stop)
|
|
|
+static int interruptHandler(JSRuntime* rt, void* opaque) {
|
|
|
+ auto* engine = static_cast<ScriptEngine*>(opaque);
|
|
|
+ return engine->isCancelled() ? 1 : 0;
|
|
|
+}
|
|
|
+
|
|
|
+ScriptEngine::ScriptEngine(const ScriptEngineConfig& config)
|
|
|
+ : config_(config) {
|
|
|
+ initRuntime();
|
|
|
+}
|
|
|
+
|
|
|
+ScriptEngine::~ScriptEngine() {
|
|
|
+ destroyRuntime();
|
|
|
+}
|
|
|
+
|
|
|
+void ScriptEngine::initRuntime() {
|
|
|
+ runtime_ = JS_NewRuntime();
|
|
|
+ if (!runtime_) {
|
|
|
+ throw std::runtime_error("Failed to create QuickJS runtime");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set memory limit
|
|
|
+ JS_SetMemoryLimit(runtime_, static_cast<size_t>(config_.max_memory_mb) * 1024 * 1024);
|
|
|
+
|
|
|
+ // Set stack size
|
|
|
+ JS_SetMaxStackSize(runtime_, static_cast<size_t>(config_.stack_size_kb) * 1024);
|
|
|
+
|
|
|
+ // Set interrupt handler for cancellation support
|
|
|
+ JS_SetInterruptHandler(runtime_, interruptHandler, this);
|
|
|
+
|
|
|
+ // Create context
|
|
|
+ context_ = JS_NewContext(runtime_);
|
|
|
+ if (!context_) {
|
|
|
+ JS_FreeRuntime(runtime_);
|
|
|
+ runtime_ = nullptr;
|
|
|
+ throw std::runtime_error("Failed to create QuickJS context");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set up global objects and APIs
|
|
|
+ setupBuiltinAPIs();
|
|
|
+}
|
|
|
+
|
|
|
+void ScriptEngine::destroyRuntime() {
|
|
|
+ if (context_) {
|
|
|
+ JS_FreeContext(context_);
|
|
|
+ context_ = nullptr;
|
|
|
+ }
|
|
|
+ if (runtime_) {
|
|
|
+ JS_FreeRuntime(runtime_);
|
|
|
+ runtime_ = nullptr;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void ScriptEngine::setupBuiltinAPIs() {
|
|
|
+ if (!context_) return;
|
|
|
+
|
|
|
+ JSContext* ctx = context_;
|
|
|
+ JSValue global = JS_GetGlobalObject(ctx);
|
|
|
+
|
|
|
+ // Create smartbotic namespace object
|
|
|
+ JSValue smartbotic = JS_NewObject(ctx);
|
|
|
+
|
|
|
+ // smartbotic.log
|
|
|
+ JSValue log = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, log, "info", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc > 0) {
|
|
|
+ const char* str = JS_ToCString(ctx, argv[0]);
|
|
|
+ if (str) {
|
|
|
+ LOG_INFO("[JS] {}", str);
|
|
|
+ JS_FreeCString(ctx, str);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return JS_UNDEFINED;
|
|
|
+ }, "info", 1));
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, log, "warn", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc > 0) {
|
|
|
+ const char* str = JS_ToCString(ctx, argv[0]);
|
|
|
+ if (str) {
|
|
|
+ LOG_WARN("[JS] {}", str);
|
|
|
+ JS_FreeCString(ctx, str);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return JS_UNDEFINED;
|
|
|
+ }, "warn", 1));
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, log, "error", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc > 0) {
|
|
|
+ const char* str = JS_ToCString(ctx, argv[0]);
|
|
|
+ if (str) {
|
|
|
+ LOG_ERROR("[JS] {}", str);
|
|
|
+ JS_FreeCString(ctx, str);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return JS_UNDEFINED;
|
|
|
+ }, "error", 1));
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, smartbotic, "log", log);
|
|
|
+
|
|
|
+ // smartbotic.utils
|
|
|
+ JSValue utils = JS_NewObject(ctx);
|
|
|
+
|
|
|
+ // utils.uuid() - Generate a UUID
|
|
|
+ JS_SetPropertyStr(ctx, utils, "uuid", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ std::string uuid = common::UUID::generate();
|
|
|
+ return JS_NewString(ctx, uuid.c_str());
|
|
|
+ }, "uuid", 0));
|
|
|
+
|
|
|
+ // utils.sleep(ms) - Pause execution for specified milliseconds
|
|
|
+ JS_SetPropertyStr(ctx, utils, "sleep", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_ThrowTypeError(ctx, "utils.sleep requires a number of milliseconds");
|
|
|
+ }
|
|
|
+
|
|
|
+ int64_t ms = 0;
|
|
|
+ if (JS_ToInt64(ctx, &ms, argv[0]) != 0) {
|
|
|
+ return JS_ThrowTypeError(ctx, "utils.sleep requires a valid number");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ms < 0) {
|
|
|
+ ms = 0;
|
|
|
+ }
|
|
|
+ if (ms > 300000) { // Cap at 5 minutes to prevent abuse
|
|
|
+ ms = 300000;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
|
|
+ return JS_UNDEFINED;
|
|
|
+ }, "sleep", 1));
|
|
|
+
|
|
|
+ // utils.interpolate(template, data) - Replace {{key}} placeholders with values from data
|
|
|
+ JS_SetPropertyStr(ctx, utils, "interpolate", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 2) {
|
|
|
+ return JS_ThrowTypeError(ctx, "utils.interpolate requires a template string and data object");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get template string
|
|
|
+ const char* tpl_cstr = JS_ToCString(ctx, argv[0]);
|
|
|
+ if (!tpl_cstr) {
|
|
|
+ return JS_ThrowTypeError(ctx, "First argument must be a string");
|
|
|
+ }
|
|
|
+ std::string tpl(tpl_cstr);
|
|
|
+ JS_FreeCString(ctx, tpl_cstr);
|
|
|
+
|
|
|
+ // Get data object
|
|
|
+ if (!JS_IsObject(argv[1])) {
|
|
|
+ return JS_ThrowTypeError(ctx, "Second argument must be an object");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Parse data object to JSON for easier access
|
|
|
+ JSValue json_str = JS_JSONStringify(ctx, argv[1], JS_UNDEFINED, JS_UNDEFINED);
|
|
|
+ const char* data_cstr = JS_ToCString(ctx, json_str);
|
|
|
+ nlohmann::json data;
|
|
|
+ if (data_cstr) {
|
|
|
+ try {
|
|
|
+ data = nlohmann::json::parse(data_cstr);
|
|
|
+ } catch (...) {
|
|
|
+ data = nlohmann::json::object();
|
|
|
+ }
|
|
|
+ JS_FreeCString(ctx, data_cstr);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, json_str);
|
|
|
+
|
|
|
+ // Replace {{key}} patterns with values from data
|
|
|
+ // Also supports {{key.nested}} for nested access
|
|
|
+ std::regex pattern(R"(\{\{([^}]+)\}\})");
|
|
|
+ std::string result;
|
|
|
+ std::smatch match;
|
|
|
+ std::string temp = tpl;
|
|
|
+
|
|
|
+ while (std::regex_search(temp, match, pattern)) {
|
|
|
+ // Add text before the match
|
|
|
+ result += match.prefix().str();
|
|
|
+
|
|
|
+ // Get the key (without {{ and }})
|
|
|
+ std::string key = match[1].str();
|
|
|
+
|
|
|
+ // Trim whitespace from key
|
|
|
+ size_t start = key.find_first_not_of(" \t");
|
|
|
+ size_t end = key.find_last_not_of(" \t");
|
|
|
+ if (start != std::string::npos) {
|
|
|
+ key = key.substr(start, end - start + 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle nested keys (e.g., "user.name")
|
|
|
+ nlohmann::json* current = &data;
|
|
|
+ std::istringstream key_stream(key);
|
|
|
+ std::string part;
|
|
|
+ bool found = true;
|
|
|
+
|
|
|
+ while (std::getline(key_stream, part, '.')) {
|
|
|
+ if (current->is_object() && current->contains(part)) {
|
|
|
+ current = &(*current)[part];
|
|
|
+ } else if (current->is_array()) {
|
|
|
+ try {
|
|
|
+ size_t idx = std::stoul(part);
|
|
|
+ if (idx < current->size()) {
|
|
|
+ current = &(*current)[idx];
|
|
|
+ } else {
|
|
|
+ found = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } catch (...) {
|
|
|
+ found = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ found = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (found && current) {
|
|
|
+ if (current->is_string()) {
|
|
|
+ result += current->get<std::string>();
|
|
|
+ } else if (current->is_number()) {
|
|
|
+ if (current->is_number_integer()) {
|
|
|
+ result += std::to_string(current->get<int64_t>());
|
|
|
+ } else {
|
|
|
+ result += std::to_string(current->get<double>());
|
|
|
+ }
|
|
|
+ } else if (current->is_boolean()) {
|
|
|
+ result += current->get<bool>() ? "true" : "false";
|
|
|
+ } else if (current->is_null()) {
|
|
|
+ result += "null";
|
|
|
+ } else {
|
|
|
+ // For objects/arrays, use JSON representation
|
|
|
+ result += current->dump();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Keep original placeholder if key not found
|
|
|
+ result += match[0].str();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Continue with the rest of the string
|
|
|
+ temp = match.suffix().str();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add remaining text after last match
|
|
|
+ result += temp;
|
|
|
+
|
|
|
+ return JS_NewString(ctx, result.c_str());
|
|
|
+ }, "interpolate", 2));
|
|
|
+
|
|
|
+ // utils.deepClone(obj) - Deep clone an object
|
|
|
+ JS_SetPropertyStr(ctx, utils, "deepClone", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_UNDEFINED;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Use JSON stringify/parse for deep clone
|
|
|
+ JSValue json_str = JS_JSONStringify(ctx, argv[0], JS_UNDEFINED, JS_UNDEFINED);
|
|
|
+ if (JS_IsException(json_str)) {
|
|
|
+ return JS_ThrowTypeError(ctx, "Cannot deep clone: object is not JSON serializable");
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* str = JS_ToCString(ctx, json_str);
|
|
|
+ if (!str) {
|
|
|
+ JS_FreeValue(ctx, json_str);
|
|
|
+ return JS_ThrowTypeError(ctx, "Failed to stringify object");
|
|
|
+ }
|
|
|
+
|
|
|
+ JSValue result = JS_ParseJSON(ctx, str, strlen(str), "<clone>");
|
|
|
+ JS_FreeCString(ctx, str);
|
|
|
+ JS_FreeValue(ctx, json_str);
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }, "deepClone", 1));
|
|
|
+
|
|
|
+ // utils.isEmpty(value) - Check if value is empty (null, undefined, empty string, empty array, empty object)
|
|
|
+ JS_SetPropertyStr(ctx, utils, "isEmpty", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_TRUE;
|
|
|
+ }
|
|
|
+
|
|
|
+ JSValue val = argv[0];
|
|
|
+
|
|
|
+ // Check for null/undefined
|
|
|
+ if (JS_IsNull(val) || JS_IsUndefined(val)) {
|
|
|
+ return JS_TRUE;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check for empty string
|
|
|
+ if (JS_IsString(val)) {
|
|
|
+ const char* str = JS_ToCString(ctx, val);
|
|
|
+ bool empty = !str || strlen(str) == 0;
|
|
|
+ JS_FreeCString(ctx, str);
|
|
|
+ return empty ? JS_TRUE : JS_FALSE;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check for empty array
|
|
|
+ if (JS_IsArray(ctx, val)) {
|
|
|
+ JSValue length = JS_GetPropertyStr(ctx, val, "length");
|
|
|
+ int64_t len = 0;
|
|
|
+ JS_ToInt64(ctx, &len, length);
|
|
|
+ JS_FreeValue(ctx, length);
|
|
|
+ return len == 0 ? JS_TRUE : JS_FALSE;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check for empty object
|
|
|
+ if (JS_IsObject(val)) {
|
|
|
+ JSPropertyEnum* props;
|
|
|
+ uint32_t count;
|
|
|
+ if (JS_GetOwnPropertyNames(ctx, &props, &count, val, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) {
|
|
|
+ for (uint32_t i = 0; i < count; i++) {
|
|
|
+ JS_FreeAtom(ctx, props[i].atom);
|
|
|
+ }
|
|
|
+ js_free(ctx, props);
|
|
|
+ return count == 0 ? JS_TRUE : JS_FALSE;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return JS_FALSE;
|
|
|
+ }, "isEmpty", 1));
|
|
|
+
|
|
|
+ // utils.pick(obj, keys) - Pick specific keys from an object
|
|
|
+ JS_SetPropertyStr(ctx, utils, "pick", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 2 || !JS_IsObject(argv[0]) || !JS_IsArray(ctx, argv[1])) {
|
|
|
+ return JS_ThrowTypeError(ctx, "utils.pick requires an object and an array of keys");
|
|
|
+ }
|
|
|
+
|
|
|
+ JSValue result = JS_NewObject(ctx);
|
|
|
+ JSValue keys = argv[1];
|
|
|
+ JSValue length = JS_GetPropertyStr(ctx, keys, "length");
|
|
|
+ int64_t len = 0;
|
|
|
+ JS_ToInt64(ctx, &len, length);
|
|
|
+ JS_FreeValue(ctx, length);
|
|
|
+
|
|
|
+ for (int64_t i = 0; i < len; i++) {
|
|
|
+ JSValue key_val = JS_GetPropertyUint32(ctx, keys, i);
|
|
|
+ const char* key = JS_ToCString(ctx, key_val);
|
|
|
+ if (key) {
|
|
|
+ JSValue prop = JS_GetPropertyStr(ctx, argv[0], key);
|
|
|
+ if (!JS_IsUndefined(prop)) {
|
|
|
+ JS_SetPropertyStr(ctx, result, key, prop);
|
|
|
+ } else {
|
|
|
+ JS_FreeValue(ctx, prop);
|
|
|
+ }
|
|
|
+ JS_FreeCString(ctx, key);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, key_val);
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }, "pick", 2));
|
|
|
+
|
|
|
+ // utils.omit(obj, keys) - Omit specific keys from an object
|
|
|
+ JS_SetPropertyStr(ctx, utils, "omit", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 2 || !JS_IsObject(argv[0]) || !JS_IsArray(ctx, argv[1])) {
|
|
|
+ return JS_ThrowTypeError(ctx, "utils.omit requires an object and an array of keys");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get keys to omit
|
|
|
+ JSValue omit_keys = argv[1];
|
|
|
+ JSValue omit_length = JS_GetPropertyStr(ctx, omit_keys, "length");
|
|
|
+ int64_t omit_len = 0;
|
|
|
+ JS_ToInt64(ctx, &omit_len, omit_length);
|
|
|
+ JS_FreeValue(ctx, omit_length);
|
|
|
+
|
|
|
+ std::vector<std::string> keys_to_omit;
|
|
|
+ for (int64_t i = 0; i < omit_len; i++) {
|
|
|
+ JSValue key_val = JS_GetPropertyUint32(ctx, omit_keys, i);
|
|
|
+ const char* key = JS_ToCString(ctx, key_val);
|
|
|
+ if (key) {
|
|
|
+ keys_to_omit.push_back(key);
|
|
|
+ JS_FreeCString(ctx, key);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, key_val);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Clone object without omitted keys
|
|
|
+ JSValue result = JS_NewObject(ctx);
|
|
|
+ JSPropertyEnum* props;
|
|
|
+ uint32_t count;
|
|
|
+ if (JS_GetOwnPropertyNames(ctx, &props, &count, argv[0], JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) {
|
|
|
+ for (uint32_t i = 0; i < count; i++) {
|
|
|
+ const char* prop_name = JS_AtomToCString(ctx, props[i].atom);
|
|
|
+ if (prop_name) {
|
|
|
+ bool should_omit = std::find(keys_to_omit.begin(), keys_to_omit.end(), prop_name) != keys_to_omit.end();
|
|
|
+ if (!should_omit) {
|
|
|
+ JSValue prop = JS_GetProperty(ctx, argv[0], props[i].atom);
|
|
|
+ JS_SetPropertyStr(ctx, result, prop_name, prop);
|
|
|
+ }
|
|
|
+ JS_FreeCString(ctx, prop_name);
|
|
|
+ }
|
|
|
+ JS_FreeAtom(ctx, props[i].atom);
|
|
|
+ }
|
|
|
+ js_free(ctx, props);
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }, "omit", 2));
|
|
|
+
|
|
|
+ // utils.base64Encode(data) - Encode string to Base64
|
|
|
+ JS_SetPropertyStr(ctx, utils, "base64Encode", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_ThrowTypeError(ctx, "utils.base64Encode requires a string argument");
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* input_str = JS_ToCString(ctx, argv[0]);
|
|
|
+ if (!input_str) {
|
|
|
+ return JS_ThrowTypeError(ctx, "Argument must be a string");
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string input(input_str);
|
|
|
+ JS_FreeCString(ctx, input_str);
|
|
|
+
|
|
|
+ std::string encoded = base64Encode(input);
|
|
|
+ return JS_NewString(ctx, encoded.c_str());
|
|
|
+ }, "base64Encode", 1));
|
|
|
+
|
|
|
+ // utils.base64Decode(base64) - Decode Base64 to string
|
|
|
+ JS_SetPropertyStr(ctx, utils, "base64Decode", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_ThrowTypeError(ctx, "utils.base64Decode requires a string argument");
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* input_str = JS_ToCString(ctx, argv[0]);
|
|
|
+ if (!input_str) {
|
|
|
+ return JS_ThrowTypeError(ctx, "Argument must be a string");
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string input(input_str);
|
|
|
+ JS_FreeCString(ctx, input_str);
|
|
|
+
|
|
|
+ std::string decoded = base64Decode(input);
|
|
|
+ return JS_NewString(ctx, decoded.c_str());
|
|
|
+ }, "base64Decode", 1));
|
|
|
+
|
|
|
+ // utils.sha256(data) - Calculate SHA256 hash (hex string)
|
|
|
+ JS_SetPropertyStr(ctx, utils, "sha256", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_ThrowTypeError(ctx, "utils.sha256 requires a string argument");
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* input_str = JS_ToCString(ctx, argv[0]);
|
|
|
+ if (!input_str) {
|
|
|
+ return JS_ThrowTypeError(ctx, "Argument must be a string");
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string input(input_str);
|
|
|
+ JS_FreeCString(ctx, input_str);
|
|
|
+
|
|
|
+ std::string hash = sha256Hash(input);
|
|
|
+ return JS_NewString(ctx, hash.c_str());
|
|
|
+ }, "sha256", 1));
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, smartbotic, "utils", utils);
|
|
|
+
|
|
|
+ // smartbotic.http
|
|
|
+ JSValue http = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, http, "request", JS_NewCFunction(ctx, js_http_request, "request", 1));
|
|
|
+
|
|
|
+ // Convenience methods
|
|
|
+ JS_SetPropertyStr(ctx, http, "get", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_ThrowTypeError(ctx, "http.get requires a URL");
|
|
|
+ }
|
|
|
+ JSValue options = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, options, "method", JS_NewString(ctx, "GET"));
|
|
|
+ if (JS_IsString(argv[0])) {
|
|
|
+ JS_SetPropertyStr(ctx, options, "url", JS_DupValue(ctx, argv[0]));
|
|
|
+ } else if (JS_IsObject(argv[0])) {
|
|
|
+ // Copy properties from options object
|
|
|
+ JSValue url = JS_GetPropertyStr(ctx, argv[0], "url");
|
|
|
+ JS_SetPropertyStr(ctx, options, "url", url);
|
|
|
+ JSValue headers = JS_GetPropertyStr(ctx, argv[0], "headers");
|
|
|
+ if (!JS_IsUndefined(headers)) {
|
|
|
+ JS_SetPropertyStr(ctx, options, "headers", headers);
|
|
|
+ } else {
|
|
|
+ JS_FreeValue(ctx, headers);
|
|
|
+ }
|
|
|
+ JSValue timeout = JS_GetPropertyStr(ctx, argv[0], "timeout");
|
|
|
+ if (!JS_IsUndefined(timeout)) {
|
|
|
+ JS_SetPropertyStr(ctx, options, "timeout", timeout);
|
|
|
+ } else {
|
|
|
+ JS_FreeValue(ctx, timeout);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ JSValue args[1] = { options };
|
|
|
+ JSValue result = js_http_request(ctx, JS_UNDEFINED, 1, args);
|
|
|
+ JS_FreeValue(ctx, options);
|
|
|
+ return result;
|
|
|
+ }, "get", 1));
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, http, "post", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_ThrowTypeError(ctx, "http.post requires a URL or options object");
|
|
|
+ }
|
|
|
+ JSValue options = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, options, "method", JS_NewString(ctx, "POST"));
|
|
|
+ if (JS_IsString(argv[0])) {
|
|
|
+ JS_SetPropertyStr(ctx, options, "url", JS_DupValue(ctx, argv[0]));
|
|
|
+ if (argc > 1) {
|
|
|
+ JS_SetPropertyStr(ctx, options, "body", JS_DupValue(ctx, argv[1]));
|
|
|
+ }
|
|
|
+ } else if (JS_IsObject(argv[0])) {
|
|
|
+ JSValue url = JS_GetPropertyStr(ctx, argv[0], "url");
|
|
|
+ JS_SetPropertyStr(ctx, options, "url", url);
|
|
|
+ JSValue headers = JS_GetPropertyStr(ctx, argv[0], "headers");
|
|
|
+ if (!JS_IsUndefined(headers)) {
|
|
|
+ JS_SetPropertyStr(ctx, options, "headers", headers);
|
|
|
+ } else {
|
|
|
+ JS_FreeValue(ctx, headers);
|
|
|
+ }
|
|
|
+ JSValue body = JS_GetPropertyStr(ctx, argv[0], "body");
|
|
|
+ if (!JS_IsUndefined(body)) {
|
|
|
+ JS_SetPropertyStr(ctx, options, "body", body);
|
|
|
+ } else {
|
|
|
+ JS_FreeValue(ctx, body);
|
|
|
+ }
|
|
|
+ JSValue timeout = JS_GetPropertyStr(ctx, argv[0], "timeout");
|
|
|
+ if (!JS_IsUndefined(timeout)) {
|
|
|
+ JS_SetPropertyStr(ctx, options, "timeout", timeout);
|
|
|
+ } else {
|
|
|
+ JS_FreeValue(ctx, timeout);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ JSValue args[1] = { options };
|
|
|
+ JSValue result = js_http_request(ctx, JS_UNDEFINED, 1, args);
|
|
|
+ JS_FreeValue(ctx, options);
|
|
|
+ return result;
|
|
|
+ }, "post", 2));
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, smartbotic, "http", http);
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, global, "smartbotic", smartbotic);
|
|
|
+
|
|
|
+ // Console for compatibility
|
|
|
+ JS_SetPropertyStr(ctx, global, "console", JS_DupValue(ctx, log));
|
|
|
+
|
|
|
+ JS_FreeValue(ctx, global);
|
|
|
+}
|
|
|
+
|
|
|
+// Helper to get ScriptContext from QuickJS context opaque data
|
|
|
+static const ScriptContext* getScriptContext(JSContext* ctx) {
|
|
|
+ return static_cast<const ScriptContext*>(JS_GetContextOpaque(ctx));
|
|
|
+}
|
|
|
+
|
|
|
+// Helper to create error result in JS
|
|
|
+static JSValue createJsError(JSContext* ctx, const std::string& message) {
|
|
|
+ return JS_ThrowInternalError(ctx, "%s", message.c_str());
|
|
|
+}
|
|
|
+
|
|
|
+// Helper to create success result object
|
|
|
+static JSValue createJsResult(JSContext* ctx, bool success, const nlohmann::json& data = nlohmann::json{}) {
|
|
|
+ JSValue result = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, result, "success", success ? JS_TRUE : JS_FALSE);
|
|
|
+ if (!data.is_null()) {
|
|
|
+ std::string data_str = data.dump();
|
|
|
+ JSValue data_val = JS_ParseJSON(ctx, data_str.c_str(), data_str.size(), "<data>");
|
|
|
+ JS_SetPropertyStr(ctx, result, "data", data_val);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+// storage.get(collection, id) - Get document by ID
|
|
|
+static JSValue js_storage_get(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
+ const ScriptContext* script_ctx = getScriptContext(ctx);
|
|
|
+ if (!script_ctx || !script_ctx->storage_get_doc) {
|
|
|
+ return createJsError(ctx, "Storage API not available");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (argc < 2) {
|
|
|
+ return JS_ThrowTypeError(ctx, "storage.get requires collection and id arguments");
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* collection = JS_ToCString(ctx, argv[0]);
|
|
|
+ const char* id = JS_ToCString(ctx, argv[1]);
|
|
|
+
|
|
|
+ if (!collection || !id) {
|
|
|
+ if (collection) JS_FreeCString(ctx, collection);
|
|
|
+ if (id) JS_FreeCString(ctx, id);
|
|
|
+ return JS_ThrowTypeError(ctx, "collection and id must be strings");
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string coll_str(collection);
|
|
|
+ std::string id_str(id);
|
|
|
+ JS_FreeCString(ctx, collection);
|
|
|
+ JS_FreeCString(ctx, id);
|
|
|
+
|
|
|
+ auto result = script_ctx->storage_get_doc(coll_str, id_str);
|
|
|
+
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ if (result.ok()) {
|
|
|
+ JS_SetPropertyStr(ctx, response, "found", JS_TRUE);
|
|
|
+ std::string doc_str = result.value().dump();
|
|
|
+ JSValue doc_val = JS_ParseJSON(ctx, doc_str.c_str(), doc_str.size(), "<document>");
|
|
|
+ JS_SetPropertyStr(ctx, response, "document", doc_val);
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, response, "found", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "document", JS_NULL);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
|
|
|
+ }
|
|
|
+ JS_SetPropertyStr(ctx, response, "collection", JS_NewString(ctx, coll_str.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, response, "id", JS_NewString(ctx, id_str.c_str()));
|
|
|
+
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+// storage.insert(collection, data, id?, ttlMs?) - Insert new document
|
|
|
+static JSValue js_storage_insert(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
+ const ScriptContext* script_ctx = getScriptContext(ctx);
|
|
|
+ if (!script_ctx || !script_ctx->storage_insert) {
|
|
|
+ return createJsError(ctx, "Storage API not available");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (argc < 2) {
|
|
|
+ return JS_ThrowTypeError(ctx, "storage.insert requires collection and data arguments");
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* collection = JS_ToCString(ctx, argv[0]);
|
|
|
+ if (!collection) {
|
|
|
+ return JS_ThrowTypeError(ctx, "collection must be a string");
|
|
|
+ }
|
|
|
+ std::string coll_str(collection);
|
|
|
+ JS_FreeCString(ctx, collection);
|
|
|
+
|
|
|
+ // Parse data
|
|
|
+ JSValue json_str = JS_JSONStringify(ctx, argv[1], JS_UNDEFINED, JS_UNDEFINED);
|
|
|
+ const char* data_cstr = JS_ToCString(ctx, json_str);
|
|
|
+ nlohmann::json data;
|
|
|
+ if (data_cstr) {
|
|
|
+ try {
|
|
|
+ data = nlohmann::json::parse(data_cstr);
|
|
|
+ } catch (...) {
|
|
|
+ JS_FreeCString(ctx, data_cstr);
|
|
|
+ JS_FreeValue(ctx, json_str);
|
|
|
+ return JS_ThrowTypeError(ctx, "data must be a valid JSON object");
|
|
|
+ }
|
|
|
+ JS_FreeCString(ctx, data_cstr);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, json_str);
|
|
|
+
|
|
|
+ // Optional id
|
|
|
+ std::string id_str;
|
|
|
+ if (argc > 2 && !JS_IsUndefined(argv[2]) && !JS_IsNull(argv[2])) {
|
|
|
+ const char* id = JS_ToCString(ctx, argv[2]);
|
|
|
+ if (id) {
|
|
|
+ id_str = id;
|
|
|
+ JS_FreeCString(ctx, id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Optional TTL
|
|
|
+ int64_t ttl_ms = 0;
|
|
|
+ if (argc > 3 && JS_IsNumber(argv[3])) {
|
|
|
+ double ttl;
|
|
|
+ JS_ToFloat64(ctx, &ttl, argv[3]);
|
|
|
+ ttl_ms = static_cast<int64_t>(ttl);
|
|
|
+ }
|
|
|
+
|
|
|
+ auto result = script_ctx->storage_insert(coll_str, data, id_str, ttl_ms);
|
|
|
+
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ if (result.ok()) {
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "id", JS_NewString(ctx, result.value().c_str()));
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
|
|
|
+ }
|
|
|
+ JS_SetPropertyStr(ctx, response, "collection", JS_NewString(ctx, coll_str.c_str()));
|
|
|
+
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+// storage.update(collection, id, data, expectedVersion?, partial?) - Update document
|
|
|
+static JSValue js_storage_update(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
+ const ScriptContext* script_ctx = getScriptContext(ctx);
|
|
|
+ if (!script_ctx || !script_ctx->storage_update) {
|
|
|
+ return createJsError(ctx, "Storage API not available");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (argc < 3) {
|
|
|
+ return JS_ThrowTypeError(ctx, "storage.update requires collection, id, and data arguments");
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* collection = JS_ToCString(ctx, argv[0]);
|
|
|
+ const char* id = JS_ToCString(ctx, argv[1]);
|
|
|
+
|
|
|
+ if (!collection || !id) {
|
|
|
+ if (collection) JS_FreeCString(ctx, collection);
|
|
|
+ if (id) JS_FreeCString(ctx, id);
|
|
|
+ return JS_ThrowTypeError(ctx, "collection and id must be strings");
|
|
|
+ }
|
|
|
+ std::string coll_str(collection);
|
|
|
+ std::string id_str(id);
|
|
|
+ JS_FreeCString(ctx, collection);
|
|
|
+ JS_FreeCString(ctx, id);
|
|
|
+
|
|
|
+ // Parse data
|
|
|
+ JSValue json_str = JS_JSONStringify(ctx, argv[2], JS_UNDEFINED, JS_UNDEFINED);
|
|
|
+ const char* data_cstr = JS_ToCString(ctx, json_str);
|
|
|
+ nlohmann::json data;
|
|
|
+ if (data_cstr) {
|
|
|
+ try {
|
|
|
+ data = nlohmann::json::parse(data_cstr);
|
|
|
+ } catch (...) {
|
|
|
+ JS_FreeCString(ctx, data_cstr);
|
|
|
+ JS_FreeValue(ctx, json_str);
|
|
|
+ return JS_ThrowTypeError(ctx, "data must be a valid JSON object");
|
|
|
+ }
|
|
|
+ JS_FreeCString(ctx, data_cstr);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, json_str);
|
|
|
+
|
|
|
+ // Optional expectedVersion
|
|
|
+ int64_t expected_version = 0;
|
|
|
+ if (argc > 3 && JS_IsNumber(argv[3])) {
|
|
|
+ double ver;
|
|
|
+ JS_ToFloat64(ctx, &ver, argv[3]);
|
|
|
+ expected_version = static_cast<int64_t>(ver);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Optional partial
|
|
|
+ bool partial = false;
|
|
|
+ if (argc > 4 && JS_IsBool(argv[4])) {
|
|
|
+ partial = JS_ToBool(ctx, argv[4]);
|
|
|
+ }
|
|
|
+
|
|
|
+ auto result = script_ctx->storage_update(coll_str, id_str, data, expected_version, partial);
|
|
|
+
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ if (result.ok()) {
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "version", JS_NewInt64(ctx, result.value()));
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
|
|
|
+ }
|
|
|
+ JS_SetPropertyStr(ctx, response, "collection", JS_NewString(ctx, coll_str.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, response, "id", JS_NewString(ctx, id_str.c_str()));
|
|
|
+
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+// storage.delete(collection, id, expectedVersion?) - Delete document
|
|
|
+static JSValue js_storage_delete(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
+ const ScriptContext* script_ctx = getScriptContext(ctx);
|
|
|
+ if (!script_ctx || !script_ctx->storage_delete) {
|
|
|
+ return createJsError(ctx, "Storage API not available");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (argc < 2) {
|
|
|
+ return JS_ThrowTypeError(ctx, "storage.delete requires collection and id arguments");
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* collection = JS_ToCString(ctx, argv[0]);
|
|
|
+ const char* id = JS_ToCString(ctx, argv[1]);
|
|
|
+
|
|
|
+ if (!collection || !id) {
|
|
|
+ if (collection) JS_FreeCString(ctx, collection);
|
|
|
+ if (id) JS_FreeCString(ctx, id);
|
|
|
+ return JS_ThrowTypeError(ctx, "collection and id must be strings");
|
|
|
+ }
|
|
|
+ std::string coll_str(collection);
|
|
|
+ std::string id_str(id);
|
|
|
+ JS_FreeCString(ctx, collection);
|
|
|
+ JS_FreeCString(ctx, id);
|
|
|
+
|
|
|
+ // Optional expectedVersion
|
|
|
+ int64_t expected_version = 0;
|
|
|
+ if (argc > 2 && JS_IsNumber(argv[2])) {
|
|
|
+ double ver;
|
|
|
+ JS_ToFloat64(ctx, &ver, argv[2]);
|
|
|
+ expected_version = static_cast<int64_t>(ver);
|
|
|
+ }
|
|
|
+
|
|
|
+ auto result = script_ctx->storage_delete(coll_str, id_str, expected_version);
|
|
|
+
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ if (result.ok()) {
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
|
|
|
+ }
|
|
|
+ JS_SetPropertyStr(ctx, response, "collection", JS_NewString(ctx, coll_str.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, response, "id", JS_NewString(ctx, id_str.c_str()));
|
|
|
+
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+// storage.query(collection, options?) - Query documents
|
|
|
+static JSValue js_storage_query(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
+ const ScriptContext* script_ctx = getScriptContext(ctx);
|
|
|
+ if (!script_ctx || !script_ctx->storage_query) {
|
|
|
+ return createJsError(ctx, "Storage API not available");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (argc < 1) {
|
|
|
+ return JS_ThrowTypeError(ctx, "storage.query requires collection argument");
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* collection = JS_ToCString(ctx, argv[0]);
|
|
|
+ if (!collection) {
|
|
|
+ return JS_ThrowTypeError(ctx, "collection must be a string");
|
|
|
+ }
|
|
|
+ std::string coll_str(collection);
|
|
|
+ JS_FreeCString(ctx, collection);
|
|
|
+
|
|
|
+ // Parse options
|
|
|
+ StorageQueryOptions options;
|
|
|
+ if (argc > 1 && JS_IsObject(argv[1])) {
|
|
|
+ JSValue opts = argv[1];
|
|
|
+
|
|
|
+ // Parse filters
|
|
|
+ JSValue filters_val = JS_GetPropertyStr(ctx, opts, "filters");
|
|
|
+ if (JS_IsArray(ctx, filters_val)) {
|
|
|
+ JSValue length = JS_GetPropertyStr(ctx, filters_val, "length");
|
|
|
+ int64_t len = 0;
|
|
|
+ JS_ToInt64(ctx, &len, length);
|
|
|
+ JS_FreeValue(ctx, length);
|
|
|
+
|
|
|
+ for (int64_t i = 0; i < len; i++) {
|
|
|
+ JSValue filter = JS_GetPropertyUint32(ctx, filters_val, i);
|
|
|
+ if (JS_IsObject(filter)) {
|
|
|
+ JSValue field_val = JS_GetPropertyStr(ctx, filter, "field");
|
|
|
+ JSValue value_val = JS_GetPropertyStr(ctx, filter, "value");
|
|
|
+
|
|
|
+ const char* field = JS_ToCString(ctx, field_val);
|
|
|
+ if (field) {
|
|
|
+ // Get value as JSON
|
|
|
+ JSValue val_str = JS_JSONStringify(ctx, value_val, JS_UNDEFINED, JS_UNDEFINED);
|
|
|
+ const char* val_cstr = JS_ToCString(ctx, val_str);
|
|
|
+ if (val_cstr) {
|
|
|
+ try {
|
|
|
+ nlohmann::json val = nlohmann::json::parse(val_cstr);
|
|
|
+ options.filters.push_back({field, val});
|
|
|
+ } catch (...) {}
|
|
|
+ JS_FreeCString(ctx, val_cstr);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, val_str);
|
|
|
+ JS_FreeCString(ctx, field);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, field_val);
|
|
|
+ JS_FreeValue(ctx, value_val);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, filter);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, filters_val);
|
|
|
+
|
|
|
+ // Parse sorts
|
|
|
+ JSValue sorts_val = JS_GetPropertyStr(ctx, opts, "sorts");
|
|
|
+ if (JS_IsArray(ctx, sorts_val)) {
|
|
|
+ JSValue length = JS_GetPropertyStr(ctx, sorts_val, "length");
|
|
|
+ int64_t len = 0;
|
|
|
+ JS_ToInt64(ctx, &len, length);
|
|
|
+ JS_FreeValue(ctx, length);
|
|
|
+
|
|
|
+ for (int64_t i = 0; i < len; i++) {
|
|
|
+ JSValue sort = JS_GetPropertyUint32(ctx, sorts_val, i);
|
|
|
+ if (JS_IsObject(sort)) {
|
|
|
+ JSValue field_val = JS_GetPropertyStr(ctx, sort, "field");
|
|
|
+ JSValue asc_val = JS_GetPropertyStr(ctx, sort, "ascending");
|
|
|
+
|
|
|
+ const char* field = JS_ToCString(ctx, field_val);
|
|
|
+ if (field) {
|
|
|
+ bool asc = JS_IsUndefined(asc_val) ? true : JS_ToBool(ctx, asc_val);
|
|
|
+ options.sorts.push_back({field, asc});
|
|
|
+ JS_FreeCString(ctx, field);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, field_val);
|
|
|
+ JS_FreeValue(ctx, asc_val);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, sort);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, sorts_val);
|
|
|
+
|
|
|
+ // Parse fields (projection)
|
|
|
+ JSValue fields_val = JS_GetPropertyStr(ctx, opts, "fields");
|
|
|
+ if (JS_IsArray(ctx, fields_val)) {
|
|
|
+ JSValue length = JS_GetPropertyStr(ctx, fields_val, "length");
|
|
|
+ int64_t len = 0;
|
|
|
+ JS_ToInt64(ctx, &len, length);
|
|
|
+ JS_FreeValue(ctx, length);
|
|
|
+
|
|
|
+ for (int64_t i = 0; i < len; i++) {
|
|
|
+ JSValue field = JS_GetPropertyUint32(ctx, fields_val, i);
|
|
|
+ const char* field_str = JS_ToCString(ctx, field);
|
|
|
+ if (field_str) {
|
|
|
+ options.fields.push_back(field_str);
|
|
|
+ JS_FreeCString(ctx, field_str);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, field);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, fields_val);
|
|
|
+
|
|
|
+ // Parse page
|
|
|
+ JSValue page_val = JS_GetPropertyStr(ctx, opts, "page");
|
|
|
+ if (JS_IsNumber(page_val)) {
|
|
|
+ double page;
|
|
|
+ JS_ToFloat64(ctx, &page, page_val);
|
|
|
+ options.page = static_cast<int32_t>(page);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, page_val);
|
|
|
+
|
|
|
+ // Parse pageSize
|
|
|
+ JSValue page_size_val = JS_GetPropertyStr(ctx, opts, "pageSize");
|
|
|
+ if (JS_IsNumber(page_size_val)) {
|
|
|
+ double page_size;
|
|
|
+ JS_ToFloat64(ctx, &page_size, page_size_val);
|
|
|
+ options.page_size = static_cast<int32_t>(page_size);
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, page_size_val);
|
|
|
+ }
|
|
|
+
|
|
|
+ auto result = script_ctx->storage_query(coll_str, options);
|
|
|
+
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ if (result.ok()) {
|
|
|
+ const auto& query_result = result.value();
|
|
|
+
|
|
|
+ // Convert documents to JS array
|
|
|
+ JSValue docs_array = JS_NewArray(ctx);
|
|
|
+ for (size_t i = 0; i < query_result.documents.size(); i++) {
|
|
|
+ std::string doc_str = query_result.documents[i].dump();
|
|
|
+ JSValue doc_val = JS_ParseJSON(ctx, doc_str.c_str(), doc_str.size(), "<document>");
|
|
|
+ JS_SetPropertyUint32(ctx, docs_array, i, doc_val);
|
|
|
+ }
|
|
|
+ JS_SetPropertyStr(ctx, response, "documents", docs_array);
|
|
|
+ JS_SetPropertyStr(ctx, response, "totalCount", JS_NewInt64(ctx, query_result.total_count));
|
|
|
+ JS_SetPropertyStr(ctx, response, "hasMore", query_result.has_more ? JS_TRUE : JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "page", JS_NewInt32(ctx, options.page));
|
|
|
+ JS_SetPropertyStr(ctx, response, "pageSize", JS_NewInt32(ctx, options.page_size));
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, response, "documents", JS_NewArray(ctx));
|
|
|
+ JS_SetPropertyStr(ctx, response, "totalCount", JS_NewInt32(ctx, 0));
|
|
|
+ JS_SetPropertyStr(ctx, response, "hasMore", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
|
|
|
+ }
|
|
|
+ JS_SetPropertyStr(ctx, response, "collection", JS_NewString(ctx, coll_str.c_str()));
|
|
|
+
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+// storage.listCollections(includeSystem?) - List accessible collections
|
|
|
+static JSValue js_storage_list_collections(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
+ const ScriptContext* script_ctx = getScriptContext(ctx);
|
|
|
+ if (!script_ctx || !script_ctx->storage_list_collections) {
|
|
|
+ return createJsError(ctx, "Storage API not available");
|
|
|
+ }
|
|
|
+
|
|
|
+ bool include_system = false;
|
|
|
+ if (argc > 0 && JS_IsBool(argv[0])) {
|
|
|
+ include_system = JS_ToBool(ctx, argv[0]);
|
|
|
+ }
|
|
|
+
|
|
|
+ auto result = script_ctx->storage_list_collections(include_system);
|
|
|
+
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ if (result.ok()) {
|
|
|
+ const auto& collections = result.value();
|
|
|
+
|
|
|
+ JSValue coll_array = JS_NewArray(ctx);
|
|
|
+ for (size_t i = 0; i < collections.size(); i++) {
|
|
|
+ JS_SetPropertyUint32(ctx, coll_array, i, JS_NewString(ctx, collections[i].c_str()));
|
|
|
+ }
|
|
|
+ JS_SetPropertyStr(ctx, response, "collections", coll_array);
|
|
|
+ JS_SetPropertyStr(ctx, response, "count", JS_NewInt64(ctx, collections.size()));
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, response, "collections", JS_NewArray(ctx));
|
|
|
+ JS_SetPropertyStr(ctx, response, "count", JS_NewInt32(ctx, 0));
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
|
|
|
+ }
|
|
|
+
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+// Setup storage API on smartbotic namespace
|
|
|
+static void setupStorageAPI(JSContext* ctx, JSValue smartbotic) {
|
|
|
+ JSValue storage = JS_NewObject(ctx);
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, storage, "get", JS_NewCFunction(ctx, js_storage_get, "get", 2));
|
|
|
+ JS_SetPropertyStr(ctx, storage, "insert", JS_NewCFunction(ctx, js_storage_insert, "insert", 4));
|
|
|
+ JS_SetPropertyStr(ctx, storage, "update", JS_NewCFunction(ctx, js_storage_update, "update", 5));
|
|
|
+ JS_SetPropertyStr(ctx, storage, "delete", JS_NewCFunction(ctx, js_storage_delete, "delete", 3));
|
|
|
+ JS_SetPropertyStr(ctx, storage, "query", JS_NewCFunction(ctx, js_storage_query, "query", 2));
|
|
|
+ JS_SetPropertyStr(ctx, storage, "listCollections", JS_NewCFunction(ctx, js_storage_list_collections, "listCollections", 1));
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, smartbotic, "storage", storage);
|
|
|
+}
|
|
|
+
|
|
|
+// credentials.get(id) - Get credential authentication header
|
|
|
+static JSValue js_credentials_get(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
+ const ScriptContext* script_ctx = getScriptContext(ctx);
|
|
|
+ if (!script_ctx || !script_ctx->credentials_get) {
|
|
|
+ // Return error object if credentials API not available
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, "Credentials API not available"));
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (argc < 1) {
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, "credentials.get requires credential ID argument"));
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* id = JS_ToCString(ctx, argv[0]);
|
|
|
+ if (!id) {
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, "Credential ID must be a string"));
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+ std::string id_str(id);
|
|
|
+ JS_FreeCString(ctx, id);
|
|
|
+
|
|
|
+ auto result = script_ctx->credentials_get(id_str);
|
|
|
+
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ if (result.ok()) {
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "headerName", JS_NewString(ctx, result.value().header_name.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, response, "headerValue", JS_NewString(ctx, result.value().header_value.c_str()));
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
|
|
|
+ }
|
|
|
+
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+// credentials.getImap(id) - Get IMAP credentials
|
|
|
+static JSValue js_credentials_get_imap(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
+ const ScriptContext* script_ctx = getScriptContext(ctx);
|
|
|
+ if (!script_ctx || !script_ctx->credentials_get_imap) {
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, "IMAP Credentials API not available"));
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (argc < 1) {
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, "credentials.getImap requires credential ID argument"));
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ const char* id = JS_ToCString(ctx, argv[0]);
|
|
|
+ if (!id) {
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, "Credential ID must be a string"));
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+ std::string id_str(id);
|
|
|
+ JS_FreeCString(ctx, id);
|
|
|
+
|
|
|
+ auto result = script_ctx->credentials_get_imap(id_str);
|
|
|
+
|
|
|
+ JSValue response = JS_NewObject(ctx);
|
|
|
+ if (result.ok()) {
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_TRUE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "host", JS_NewString(ctx, result.value().host.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, response, "port", JS_NewInt32(ctx, result.value().port));
|
|
|
+ JS_SetPropertyStr(ctx, response, "username", JS_NewString(ctx, result.value().username.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, response, "password", JS_NewString(ctx, result.value().password.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, response, "useSsl", result.value().use_ssl ? JS_TRUE : JS_FALSE);
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, response, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, response, "error", JS_NewString(ctx, result.error().message().c_str()));
|
|
|
+ }
|
|
|
+
|
|
|
+ return response;
|
|
|
+}
|
|
|
+
|
|
|
+// IMAP operations now use the imap::ImapClient class
|
|
|
+
|
|
|
+// imap.search(options) - Search emails
|
|
|
+static JSValue js_imap_search(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
+ const ScriptContext* script_ctx = getScriptContext(ctx);
|
|
|
+ if (!script_ctx || !script_ctx->credentials_get_imap) {
|
|
|
+ return JS_ThrowInternalError(ctx, "IMAP credentials API not available");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (argc < 1 || !JS_IsObject(argv[0])) {
|
|
|
+ return JS_ThrowTypeError(ctx, "imap.search requires an options object");
|
|
|
+ }
|
|
|
+
|
|
|
+ JSValue options = argv[0];
|
|
|
+
|
|
|
+ // Get credential ID
|
|
|
+ JSValue cred_val = JS_GetPropertyStr(ctx, options, "credentialId");
|
|
|
+ if (JS_IsUndefined(cred_val)) {
|
|
|
+ JS_FreeValue(ctx, cred_val);
|
|
|
+ return JS_ThrowTypeError(ctx, "imap.search requires credentialId");
|
|
|
+ }
|
|
|
+ const char* cred_id = JS_ToCString(ctx, cred_val);
|
|
|
+ std::string credential_id(cred_id ? cred_id : "");
|
|
|
+ JS_FreeCString(ctx, cred_id);
|
|
|
+ JS_FreeValue(ctx, cred_val);
|
|
|
+
|
|
|
+ // Get mailbox (default INBOX)
|
|
|
+ std::string mailbox = "INBOX";
|
|
|
+ JSValue mailbox_val = JS_GetPropertyStr(ctx, options, "mailbox");
|
|
|
+ if (!JS_IsUndefined(mailbox_val)) {
|
|
|
+ const char* mb = JS_ToCString(ctx, mailbox_val);
|
|
|
+ if (mb) { mailbox = mb; JS_FreeCString(ctx, mb); }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, mailbox_val);
|
|
|
+
|
|
|
+ // Get search criteria (default ALL)
|
|
|
+ std::string criteria = "ALL";
|
|
|
+ JSValue criteria_val = JS_GetPropertyStr(ctx, options, "criteria");
|
|
|
+ if (!JS_IsUndefined(criteria_val)) {
|
|
|
+ const char* cr = JS_ToCString(ctx, criteria_val);
|
|
|
+ if (cr) { criteria = cr; JS_FreeCString(ctx, cr); }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, criteria_val);
|
|
|
+
|
|
|
+ // Get limit
|
|
|
+ int limit = 50;
|
|
|
+ JSValue limit_val = JS_GetPropertyStr(ctx, options, "limit");
|
|
|
+ if (JS_IsNumber(limit_val)) {
|
|
|
+ int32_t l;
|
|
|
+ JS_ToInt32(ctx, &l, limit_val);
|
|
|
+ limit = l;
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, limit_val);
|
|
|
+
|
|
|
+ // Get credentials
|
|
|
+ auto cred_result = script_ctx->credentials_get_imap(credential_id);
|
|
|
+ if (cred_result.failed()) {
|
|
|
+ return JS_ThrowInternalError(ctx, "Failed to get IMAP credentials: %s",
|
|
|
+ cred_result.error().message().c_str());
|
|
|
+ }
|
|
|
+
|
|
|
+ const auto& cred = cred_result.value();
|
|
|
+
|
|
|
+ // Create ImapClient and perform search
|
|
|
+ imap::ImapCredentials imap_creds{cred.host, cred.port, cred.username, cred.password, cred.use_ssl};
|
|
|
+ imap::ImapClient client(imap_creds);
|
|
|
+
|
|
|
+ auto search_result = client.search(mailbox, criteria, limit);
|
|
|
+ if (search_result.failed()) {
|
|
|
+ return JS_ThrowInternalError(ctx, "IMAP search failed: %s",
|
|
|
+ search_result.error().message().c_str());
|
|
|
+ }
|
|
|
+
|
|
|
+ const auto& search_data = search_result.value();
|
|
|
+
|
|
|
+ // Build JS result
|
|
|
+ JSValue result = JS_NewObject(ctx);
|
|
|
+ JSValue uids = JS_NewArray(ctx);
|
|
|
+
|
|
|
+ for (int i = 0; i < search_data.count; i++) {
|
|
|
+ JS_SetPropertyUint32(ctx, uids, i, JS_NewString(ctx, search_data.uids[i].c_str()));
|
|
|
+ }
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, result, "success", JS_TRUE);
|
|
|
+ JS_SetPropertyStr(ctx, result, "uids", uids);
|
|
|
+ JS_SetPropertyStr(ctx, result, "count", JS_NewInt32(ctx, search_data.count));
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+// imap.fetch(options) - Fetch email content
|
|
|
+static JSValue js_imap_fetch(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
+ const ScriptContext* script_ctx = getScriptContext(ctx);
|
|
|
+ if (!script_ctx || !script_ctx->credentials_get_imap) {
|
|
|
+ return JS_ThrowInternalError(ctx, "IMAP credentials API not available");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (argc < 1 || !JS_IsObject(argv[0])) {
|
|
|
+ return JS_ThrowTypeError(ctx, "imap.fetch requires an options object");
|
|
|
+ }
|
|
|
+
|
|
|
+ JSValue options = argv[0];
|
|
|
+
|
|
|
+ // Get credential ID
|
|
|
+ JSValue cred_val = JS_GetPropertyStr(ctx, options, "credentialId");
|
|
|
+ if (JS_IsUndefined(cred_val)) {
|
|
|
+ JS_FreeValue(ctx, cred_val);
|
|
|
+ return JS_ThrowTypeError(ctx, "imap.fetch requires credentialId");
|
|
|
+ }
|
|
|
+ const char* cred_id = JS_ToCString(ctx, cred_val);
|
|
|
+ std::string credential_id(cred_id ? cred_id : "");
|
|
|
+ JS_FreeCString(ctx, cred_id);
|
|
|
+ JS_FreeValue(ctx, cred_val);
|
|
|
+
|
|
|
+ // Get UID (required)
|
|
|
+ JSValue uid_val = JS_GetPropertyStr(ctx, options, "uid");
|
|
|
+ if (JS_IsUndefined(uid_val)) {
|
|
|
+ JS_FreeValue(ctx, uid_val);
|
|
|
+ return JS_ThrowTypeError(ctx, "imap.fetch requires uid");
|
|
|
+ }
|
|
|
+ const char* uid_str = JS_ToCString(ctx, uid_val);
|
|
|
+ std::string uid(uid_str ? uid_str : "");
|
|
|
+ JS_FreeCString(ctx, uid_str);
|
|
|
+ JS_FreeValue(ctx, uid_val);
|
|
|
+
|
|
|
+ // Get mailbox (default INBOX)
|
|
|
+ std::string mailbox = "INBOX";
|
|
|
+ JSValue mailbox_val = JS_GetPropertyStr(ctx, options, "mailbox");
|
|
|
+ if (!JS_IsUndefined(mailbox_val)) {
|
|
|
+ const char* mb = JS_ToCString(ctx, mailbox_val);
|
|
|
+ if (mb) { mailbox = mb; JS_FreeCString(ctx, mb); }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, mailbox_val);
|
|
|
+
|
|
|
+ // Get credentials
|
|
|
+ auto cred_result = script_ctx->credentials_get_imap(credential_id);
|
|
|
+ if (cred_result.failed()) {
|
|
|
+ return JS_ThrowInternalError(ctx, "Failed to get IMAP credentials: %s",
|
|
|
+ cred_result.error().message().c_str());
|
|
|
+ }
|
|
|
+
|
|
|
+ const auto& cred = cred_result.value();
|
|
|
+
|
|
|
+ // Create ImapClient and perform fetch
|
|
|
+ imap::ImapCredentials imap_creds{cred.host, cred.port, cred.username, cred.password, cred.use_ssl};
|
|
|
+ imap::ImapClient client(imap_creds);
|
|
|
+
|
|
|
+ auto fetch_result = client.fetch(mailbox, uid);
|
|
|
+ if (fetch_result.failed()) {
|
|
|
+ return JS_ThrowInternalError(ctx, "IMAP fetch failed: %s",
|
|
|
+ fetch_result.error().message().c_str());
|
|
|
+ }
|
|
|
+
|
|
|
+ const auto& email = fetch_result.value();
|
|
|
+
|
|
|
+ // Build JS result
|
|
|
+ JSValue result = JS_NewObject(ctx);
|
|
|
+ JS_SetPropertyStr(ctx, result, "success", JS_TRUE);
|
|
|
+ JS_SetPropertyStr(ctx, result, "uid", JS_NewString(ctx, email.uid.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, result, "subject", JS_NewString(ctx, email.subject.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, result, "from", JS_NewString(ctx, email.from.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, result, "to", JS_NewString(ctx, email.to.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, result, "date", JS_NewString(ctx, email.date.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, result, "body", JS_NewString(ctx, email.body.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, result, "raw", JS_NewString(ctx, email.raw.c_str()));
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+// imap.modify(options) - Modify email flags
|
|
|
+static JSValue js_imap_modify(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
|
+ const ScriptContext* script_ctx = getScriptContext(ctx);
|
|
|
+ if (!script_ctx || !script_ctx->credentials_get_imap) {
|
|
|
+ return JS_ThrowInternalError(ctx, "IMAP credentials API not available");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (argc < 1 || !JS_IsObject(argv[0])) {
|
|
|
+ return JS_ThrowTypeError(ctx, "imap.modify requires an options object");
|
|
|
+ }
|
|
|
+
|
|
|
+ JSValue options = argv[0];
|
|
|
+
|
|
|
+ // Get credential ID
|
|
|
+ JSValue cred_val = JS_GetPropertyStr(ctx, options, "credentialId");
|
|
|
+ if (JS_IsUndefined(cred_val)) {
|
|
|
+ JS_FreeValue(ctx, cred_val);
|
|
|
+ return JS_ThrowTypeError(ctx, "imap.modify requires credentialId");
|
|
|
+ }
|
|
|
+ const char* cred_id = JS_ToCString(ctx, cred_val);
|
|
|
+ std::string credential_id(cred_id ? cred_id : "");
|
|
|
+ JS_FreeCString(ctx, cred_id);
|
|
|
+ JS_FreeValue(ctx, cred_val);
|
|
|
+
|
|
|
+ // Get UID (required)
|
|
|
+ JSValue uid_val = JS_GetPropertyStr(ctx, options, "uid");
|
|
|
+ if (JS_IsUndefined(uid_val)) {
|
|
|
+ JS_FreeValue(ctx, uid_val);
|
|
|
+ return JS_ThrowTypeError(ctx, "imap.modify requires uid");
|
|
|
+ }
|
|
|
+ const char* uid_str = JS_ToCString(ctx, uid_val);
|
|
|
+ std::string uid(uid_str ? uid_str : "");
|
|
|
+ JS_FreeCString(ctx, uid_str);
|
|
|
+ JS_FreeValue(ctx, uid_val);
|
|
|
+
|
|
|
+ // Get mailbox (default INBOX)
|
|
|
+ std::string mailbox = "INBOX";
|
|
|
+ JSValue mailbox_val = JS_GetPropertyStr(ctx, options, "mailbox");
|
|
|
+ if (!JS_IsUndefined(mailbox_val)) {
|
|
|
+ const char* mb = JS_ToCString(ctx, mailbox_val);
|
|
|
+ if (mb) { mailbox = mb; JS_FreeCString(ctx, mb); }
|
|
|
+ }
|
|
|
+ JS_FreeValue(ctx, mailbox_val);
|
|
|
+
|
|
|
+ // Get action (required)
|
|
|
+ JSValue action_val = JS_GetPropertyStr(ctx, options, "action");
|
|
|
+ if (JS_IsUndefined(action_val)) {
|
|
|
+ JS_FreeValue(ctx, action_val);
|
|
|
+ return JS_ThrowTypeError(ctx, "imap.modify requires action");
|
|
|
+ }
|
|
|
+ const char* action_str = JS_ToCString(ctx, action_val);
|
|
|
+ std::string action(action_str ? action_str : "");
|
|
|
+ JS_FreeCString(ctx, action_str);
|
|
|
+ JS_FreeValue(ctx, action_val);
|
|
|
+
|
|
|
+ // Get credentials
|
|
|
+ auto cred_result = script_ctx->credentials_get_imap(credential_id);
|
|
|
+ if (cred_result.failed()) {
|
|
|
+ return JS_ThrowInternalError(ctx, "Failed to get IMAP credentials: %s",
|
|
|
+ cred_result.error().message().c_str());
|
|
|
+ }
|
|
|
+
|
|
|
+ const auto& cred = cred_result.value();
|
|
|
+
|
|
|
+ // Create ImapClient
|
|
|
+ imap::ImapCredentials imap_creds{cred.host, cred.port, cred.username, cred.password, cred.use_ssl};
|
|
|
+ imap::ImapClient client(imap_creds);
|
|
|
+
|
|
|
+ // Handle move action separately
|
|
|
+ if (action == "move") {
|
|
|
+ JSValue target_val = JS_GetPropertyStr(ctx, options, "targetMailbox");
|
|
|
+ if (JS_IsUndefined(target_val)) {
|
|
|
+ JS_FreeValue(ctx, target_val);
|
|
|
+ return JS_ThrowTypeError(ctx, "imap.modify with 'move' action requires targetMailbox");
|
|
|
+ }
|
|
|
+ const char* target_str = JS_ToCString(ctx, target_val);
|
|
|
+ std::string target_mailbox(target_str ? target_str : "");
|
|
|
+ JS_FreeCString(ctx, target_str);
|
|
|
+ JS_FreeValue(ctx, target_val);
|
|
|
+
|
|
|
+ auto move_result = client.move(mailbox, uid, target_mailbox);
|
|
|
+ JSValue result = JS_NewObject(ctx);
|
|
|
+ if (move_result.failed()) {
|
|
|
+ JS_SetPropertyStr(ctx, result, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, move_result.error().message().c_str()));
|
|
|
+ } else {
|
|
|
+ const auto& modify_data = move_result.value();
|
|
|
+ JS_SetPropertyStr(ctx, result, "success", modify_data.success ? JS_TRUE : JS_FALSE);
|
|
|
+ if (!modify_data.success) {
|
|
|
+ JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, modify_data.error.c_str()));
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, result, "action", JS_NewString(ctx, action.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, result, "uid", JS_NewString(ctx, uid.c_str()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle other modify actions
|
|
|
+ auto modify_result = client.modify(mailbox, uid, action);
|
|
|
+
|
|
|
+ JSValue result = JS_NewObject(ctx);
|
|
|
+ if (modify_result.failed()) {
|
|
|
+ JS_SetPropertyStr(ctx, result, "success", JS_FALSE);
|
|
|
+ JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, modify_result.error().message().c_str()));
|
|
|
+ } else {
|
|
|
+ const auto& modify_data = modify_result.value();
|
|
|
+ JS_SetPropertyStr(ctx, result, "success", modify_data.success ? JS_TRUE : JS_FALSE);
|
|
|
+ if (!modify_data.success) {
|
|
|
+ JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, modify_data.error.c_str()));
|
|
|
+ } else {
|
|
|
+ JS_SetPropertyStr(ctx, result, "action", JS_NewString(ctx, action.c_str()));
|
|
|
+ JS_SetPropertyStr(ctx, result, "uid", JS_NewString(ctx, uid.c_str()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+// Setup IMAP API on smartbotic namespace
|
|
|
+static void setupImapAPI(JSContext* ctx, JSValue smartbotic) {
|
|
|
+ JSValue imap = JS_NewObject(ctx);
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, imap, "search", JS_NewCFunction(ctx, js_imap_search, "search", 1));
|
|
|
+ JS_SetPropertyStr(ctx, imap, "fetch", JS_NewCFunction(ctx, js_imap_fetch, "fetch", 1));
|
|
|
+ JS_SetPropertyStr(ctx, imap, "modify", JS_NewCFunction(ctx, js_imap_modify, "modify", 1));
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, smartbotic, "imap", imap);
|
|
|
+}
|
|
|
+
|
|
|
+// Setup credentials API on smartbotic namespace
|
|
|
+static void setupCredentialsAPI(JSContext* ctx, JSValue smartbotic) {
|
|
|
+ JSValue credentials = JS_NewObject(ctx);
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, credentials, "get", JS_NewCFunction(ctx, js_credentials_get, "get", 1));
|
|
|
+ JS_SetPropertyStr(ctx, credentials, "getImap", JS_NewCFunction(ctx, js_credentials_get_imap, "getImap", 1));
|
|
|
+
|
|
|
+ JS_SetPropertyStr(ctx, smartbotic, "credentials", credentials);
|
|
|
+}
|
|
|
+
|
|
|
+ScriptResult ScriptEngine::execute(const std::string& script, const ScriptContext& context) {
|
|
|
+ ScriptResult result;
|
|
|
+ result.success = true;
|
|
|
+
|
|
|
+ // Reset context to ensure it's created on this thread
|
|
|
+ // QuickJS contexts should be used only on the thread that created them
|
|
|
+ destroyRuntime();
|
|
|
+ initRuntime();
|
|
|
+
|
|
|
+ if (!context_ || !runtime_) {
|
|
|
+ result.error = "Runtime not initialized";
|
|
|
+ result.success = false;
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ cancelled_.store(false);
|
|
|
+
|
|
|
+ // Store ScriptContext pointer for storage API callbacks
|
|
|
+ JS_SetContextOpaque(context_, const_cast<ScriptContext*>(&context));
|
|
|
+
|
|
|
+ JSValue global = JS_GetGlobalObject(context_);
|
|
|
+
|
|
|
+ // Set up storage, credentials, and IMAP APIs on smartbotic namespace
|
|
|
+ JSValue smartbotic = JS_GetPropertyStr(context_, global, "smartbotic");
|
|
|
+ if (JS_IsObject(smartbotic)) {
|
|
|
+ setupStorageAPI(context_, smartbotic);
|
|
|
+ setupCredentialsAPI(context_, smartbotic);
|
|
|
+ setupImapAPI(context_, smartbotic);
|
|
|
+ }
|
|
|
+ JS_FreeValue(context_, smartbotic);
|
|
|
+
|
|
|
+ // Set up module.exports for CommonJS compatibility
|
|
|
+ JSValue module_obj = JS_NewObject(context_);
|
|
|
+ JSValue exports_obj = JS_NewObject(context_);
|
|
|
+ JS_SetPropertyStr(context_, module_obj, "exports", exports_obj); // module owns exports
|
|
|
+ JS_SetPropertyStr(context_, global, "module", module_obj);
|
|
|
+ // Also set exports directly for shorthand access
|
|
|
+ JSValue exports_ref = JS_GetPropertyStr(context_, module_obj, "exports");
|
|
|
+ JS_SetPropertyStr(context_, global, "exports", exports_ref);
|
|
|
+
|
|
|
+ // Set input data (default to empty object if null)
|
|
|
+ std::string input_json = context.input.is_null() ? "{}" : context.input.dump();
|
|
|
+ JSValue input_val = JS_ParseJSON(context_, input_json.c_str(), input_json.size(), "<input>");
|
|
|
+ if (JS_IsException(input_val)) {
|
|
|
+ LOG_ERROR("Failed to parse input JSON: {}", input_json);
|
|
|
+ input_val = JS_NewObject(context_);
|
|
|
+ }
|
|
|
+ JS_SetPropertyStr(context_, global, "input", input_val);
|
|
|
+
|
|
|
+ // Set config data (default to empty object if null)
|
|
|
+ std::string config_json = context.config.is_null() ? "{}" : context.config.dump();
|
|
|
+ JSValue config_val = JS_ParseJSON(context_, config_json.c_str(), config_json.size(), "<config>");
|
|
|
+ if (JS_IsException(config_val)) {
|
|
|
+ LOG_ERROR("Failed to parse config JSON: {}", config_json);
|
|
|
+ config_val = JS_NewObject(context_);
|
|
|
+ }
|
|
|
+ JS_SetPropertyStr(context_, global, "config", config_val);
|
|
|
+
|
|
|
+ // Create context object for the execute function
|
|
|
+ nlohmann::json ctx_json = {
|
|
|
+ {"executionId", context.execution_id},
|
|
|
+ {"nodeId", context.node_id}
|
|
|
+ };
|
|
|
+ std::string ctx_str = ctx_json.dump();
|
|
|
+ JSValue ctx_val = JS_ParseJSON(context_, ctx_str.c_str(), ctx_str.size(), "<context>");
|
|
|
+ if (JS_IsException(ctx_val)) {
|
|
|
+ LOG_ERROR("Failed to parse context JSON");
|
|
|
+ ctx_val = JS_NewObject(context_);
|
|
|
+ }
|
|
|
+ JS_SetPropertyStr(context_, global, "context", ctx_val);
|
|
|
+
|
|
|
+ // Execute the script (loads module.exports with execute function)
|
|
|
+ LOG_DEBUG("Executing script: {} bytes", script.size());
|
|
|
+
|
|
|
+ JSValue val = JS_Eval(context_, script.c_str(), script.size(),
|
|
|
+ "<script>", JS_EVAL_TYPE_GLOBAL);
|
|
|
+
|
|
|
+ if (JS_IsException(val)) {
|
|
|
+ JSValue exception = JS_GetException(context_);
|
|
|
+
|
|
|
+ // Try to get error message
|
|
|
+ const char* msg = JS_ToCString(context_, exception);
|
|
|
+ std::string error_msg = msg ? msg : "Unknown error";
|
|
|
+ JS_FreeCString(context_, msg);
|
|
|
+
|
|
|
+ // Try to get stack trace
|
|
|
+ if (JS_IsObject(exception)) {
|
|
|
+ JSValue stack = JS_GetPropertyStr(context_, exception, "stack");
|
|
|
+ if (!JS_IsUndefined(stack)) {
|
|
|
+ const char* stack_str = JS_ToCString(context_, stack);
|
|
|
+ if (stack_str) {
|
|
|
+ error_msg += "\nStack: " + std::string(stack_str);
|
|
|
+ JS_FreeCString(context_, stack_str);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ JS_FreeValue(context_, stack);
|
|
|
+ }
|
|
|
+
|
|
|
+ result.error = "Script error: " + error_msg;
|
|
|
+ LOG_ERROR("Script exception: {}", result.error);
|
|
|
+ JS_FreeValue(context_, exception);
|
|
|
+ result.success = false;
|
|
|
+ JS_FreeValue(context_, val);
|
|
|
+ JS_FreeValue(context_, global);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ LOG_DEBUG("Script loaded successfully");
|
|
|
+
|
|
|
+ JS_FreeValue(context_, val);
|
|
|
+
|
|
|
+ // Get the execute function from module.exports
|
|
|
+ JSValue module_val = JS_GetPropertyStr(context_, global, "module");
|
|
|
+ JSValue exports_val = JS_GetPropertyStr(context_, module_val, "exports");
|
|
|
+ JSValue execute_fn = JS_GetPropertyStr(context_, exports_val, "execute");
|
|
|
+
|
|
|
+ if (!JS_IsFunction(context_, execute_fn)) {
|
|
|
+ result.error = "Node does not export an execute function";
|
|
|
+ LOG_ERROR("execute is not a function. JS_VALUE_GET_TAG = {}", JS_VALUE_GET_TAG(execute_fn));
|
|
|
+ result.success = false;
|
|
|
+ JS_FreeValue(context_, execute_fn);
|
|
|
+ JS_FreeValue(context_, exports_val);
|
|
|
+ JS_FreeValue(context_, module_val);
|
|
|
+ JS_FreeValue(context_, global);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ LOG_DEBUG("Found execute function");
|
|
|
+
|
|
|
+ // Get config and input values that were set earlier
|
|
|
+ JSValue config_arg = JS_GetPropertyStr(context_, global, "config");
|
|
|
+ JSValue input_arg = JS_GetPropertyStr(context_, global, "input");
|
|
|
+ JSValue ctx_arg = JS_GetPropertyStr(context_, global, "context");
|
|
|
+
|
|
|
+ // Call execute(config, input, context)
|
|
|
+ JSValue args[3] = { config_arg, input_arg, ctx_arg };
|
|
|
+ JSValue exec_result = JS_Call(context_, execute_fn, JS_UNDEFINED, 3, args);
|
|
|
+
|
|
|
+ if (JS_IsException(exec_result)) {
|
|
|
+ JSValue exception = JS_GetException(context_);
|
|
|
+ JSValue exception_str = JS_ToString(context_, exception);
|
|
|
+ const char* str = JS_ToCString(context_, exception_str);
|
|
|
+ result.error = str ? std::string("Execute error: ") + str : "Execute function threw an exception";
|
|
|
+ LOG_ERROR("Execute exception: {}", result.error);
|
|
|
+ JS_FreeCString(context_, str);
|
|
|
+ JS_FreeValue(context_, exception_str);
|
|
|
+ JS_FreeValue(context_, exception);
|
|
|
+ result.success = false;
|
|
|
+ } else {
|
|
|
+ LOG_DEBUG("Execute call returned successfully");
|
|
|
+ // Process any pending async jobs (for async functions)
|
|
|
+ JSContext* ctx2;
|
|
|
+ int err;
|
|
|
+ while ((err = JS_ExecutePendingJob(JS_GetRuntime(context_), &ctx2)) > 0) {
|
|
|
+ if (cancelled_.load()) {
|
|
|
+ result.error = "Execution cancelled";
|
|
|
+ result.success = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (err < 0) {
|
|
|
+ JSValue exception = JS_GetException(context_);
|
|
|
+ const char* str = JS_ToCString(context_, exception);
|
|
|
+ result.error = str ? str : "Async execution error";
|
|
|
+ JS_FreeCString(context_, str);
|
|
|
+ JS_FreeValue(context_, exception);
|
|
|
+ result.success = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if result is a promise and get its value
|
|
|
+ if (result.success && JS_IsObject(exec_result)) {
|
|
|
+ JSPromiseStateEnum state = JS_PromiseState(context_, exec_result);
|
|
|
+ if (state == JS_PROMISE_REJECTED) {
|
|
|
+ JSValue promise_result = JS_PromiseResult(context_, exec_result);
|
|
|
+ const char* str = JS_ToCString(context_, promise_result);
|
|
|
+ result.error = str ? str : "Promise rejected";
|
|
|
+ JS_FreeCString(context_, str);
|
|
|
+ JS_FreeValue(context_, promise_result);
|
|
|
+ result.success = false;
|
|
|
+ } else if (state == JS_PROMISE_FULFILLED) {
|
|
|
+ JSValue promise_result = JS_PromiseResult(context_, exec_result);
|
|
|
+ JSValue json_str = JS_JSONStringify(context_, promise_result, JS_UNDEFINED, JS_UNDEFINED);
|
|
|
+ const char* str = JS_ToCString(context_, json_str);
|
|
|
+ if (str) {
|
|
|
+ try {
|
|
|
+ result.output = nlohmann::json::parse(str);
|
|
|
+ } catch (...) {
|
|
|
+ result.output = nullptr;
|
|
|
+ }
|
|
|
+ JS_FreeCString(context_, str);
|
|
|
+ }
|
|
|
+ JS_FreeValue(context_, json_str);
|
|
|
+ JS_FreeValue(context_, promise_result);
|
|
|
+ } else if (state == JS_PROMISE_PENDING) {
|
|
|
+ // Not a promise, or promise still pending - try to stringify directly
|
|
|
+ JSValue json_str = JS_JSONStringify(context_, exec_result, JS_UNDEFINED, JS_UNDEFINED);
|
|
|
+ const char* str = JS_ToCString(context_, json_str);
|
|
|
+ if (str) {
|
|
|
+ try {
|
|
|
+ result.output = nlohmann::json::parse(str);
|
|
|
+ } catch (...) {
|
|
|
+ result.output = nullptr;
|
|
|
+ }
|
|
|
+ JS_FreeCString(context_, str);
|
|
|
+ }
|
|
|
+ JS_FreeValue(context_, json_str);
|
|
|
+ }
|
|
|
+ } else if (result.success) {
|
|
|
+ // Non-object result (number, string, etc.)
|
|
|
+ JSValue json_str = JS_JSONStringify(context_, exec_result, JS_UNDEFINED, JS_UNDEFINED);
|
|
|
+ const char* str = JS_ToCString(context_, json_str);
|
|
|
+ if (str) {
|
|
|
+ try {
|
|
|
+ result.output = nlohmann::json::parse(str);
|
|
|
+ } catch (...) {
|
|
|
+ result.output = nullptr;
|
|
|
+ }
|
|
|
+ JS_FreeCString(context_, str);
|
|
|
+ }
|
|
|
+ JS_FreeValue(context_, json_str);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ JS_FreeValue(context_, exec_result);
|
|
|
+ JS_FreeValue(context_, config_arg);
|
|
|
+ JS_FreeValue(context_, input_arg);
|
|
|
+ JS_FreeValue(context_, ctx_arg);
|
|
|
+ JS_FreeValue(context_, execute_fn);
|
|
|
+ JS_FreeValue(context_, exports_val);
|
|
|
+ JS_FreeValue(context_, module_val);
|
|
|
+ JS_FreeValue(context_, global);
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+void ScriptEngine::cancel() {
|
|
|
+ cancelled_.store(true);
|
|
|
+}
|
|
|
+
|
|
|
+bool ScriptEngine::isCancelled() const {
|
|
|
+ return cancelled_.load();
|
|
|
+}
|
|
|
+
|
|
|
+void ScriptEngine::reset() {
|
|
|
+ destroyRuntime();
|
|
|
+ initRuntime();
|
|
|
+}
|
|
|
+
|
|
|
+// ScriptEnginePool implementation
|
|
|
+ScriptEnginePool::ScriptEnginePool(size_t pool_size, const ScriptEngineConfig& config)
|
|
|
+ : config_(config), stop_(false) {
|
|
|
+ for (size_t i = 0; i < pool_size; ++i) {
|
|
|
+ engines_.push_back(std::make_unique<ScriptEngine>(config));
|
|
|
+ available_.push_back(engines_.back().get());
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+ScriptEnginePool::~ScriptEnginePool() {
|
|
|
+ {
|
|
|
+ std::lock_guard<std::mutex> lock(mutex_);
|
|
|
+ stop_ = true;
|
|
|
+ }
|
|
|
+ cv_.notify_all();
|
|
|
+}
|
|
|
+
|
|
|
+ScriptEngine* ScriptEnginePool::acquire() {
|
|
|
+ std::unique_lock<std::mutex> lock(mutex_);
|
|
|
+ cv_.wait(lock, [this] { return !available_.empty() || stop_; });
|
|
|
+
|
|
|
+ if (stop_ || available_.empty()) {
|
|
|
+ return nullptr;
|
|
|
+ }
|
|
|
+
|
|
|
+ ScriptEngine* engine = available_.back();
|
|
|
+ available_.pop_back();
|
|
|
+ return engine;
|
|
|
+}
|
|
|
+
|
|
|
+void ScriptEnginePool::release(ScriptEngine* engine) {
|
|
|
+ if (!engine) return;
|
|
|
+
|
|
|
+ // Note: We don't reset here because execute() resets at the start
|
|
|
+ // to ensure the context is created on the executing thread
|
|
|
+ {
|
|
|
+ std::lock_guard<std::mutex> lock(mutex_);
|
|
|
+ available_.push_back(engine);
|
|
|
+ }
|
|
|
+ cv_.notify_one();
|
|
|
+}
|
|
|
+
|
|
|
+size_t ScriptEnginePool::getAvailableCount() const {
|
|
|
+ std::lock_guard<std::mutex> lock(mutex_);
|
|
|
+ return available_.size();
|
|
|
+}
|
|
|
+
|
|
|
+size_t ScriptEngine::getMemoryUsage() const {
|
|
|
+ // QuickJS memory info would be retrieved here
|
|
|
+ // For now return 0 as placeholder
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace smartbotic::runner::engine
|