#include "database.js.h" #include "log.h" #include "mem.h" #include "ssb.h" #include "task.h" #include "util.js.h" #include "sqlite3.h" #include #include static JSClassID _database_class_id; static int _database_count; typedef struct _database_t { JSContext* context; JSValue object; void* task; const char* id; } database_t; static JSValue _database_create(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data); static void _database_finalizer(JSRuntime* runtime, JSValue value); static JSValue _database_get(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv); static JSValue _database_set(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv); static JSValue _database_exchange(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv); static JSValue _database_remove(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv); static JSValue _database_get_all(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv); static JSValue _database_get_like(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv); static JSValue _databases_list(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data); void tf_database_register(JSContext* context) { JS_NewClassID(&_database_class_id); JSClassDef def = { .class_name = "Database", .finalizer = &_database_finalizer, }; if (JS_NewClass(JS_GetRuntime(context), _database_class_id, &def) != 0) { tf_printf("Failed to register database.\n"); } JSValue global = JS_GetGlobalObject(context); JSValue constructor = JS_NewCFunctionData(context, _database_create, 0, 0, 0, NULL); JS_SetConstructorBit(context, constructor, true); JS_SetPropertyStr(context, global, "Database", constructor); JSValue databases = JS_NewObject(context); JS_SetPropertyStr(context, global, "databases", databases); JS_SetPropertyStr(context, databases, "list", JS_NewCFunctionData(context, _databases_list, 0, 0, 0, NULL)); JS_FreeValue(context, global); } static JSValue _database_create(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data) { ++_database_count; JSValue object = JS_NewObjectClass(context, _database_class_id); database_t* database = tf_malloc(sizeof(database_t)); *database = (database_t) { .task = JS_GetContextOpaque(context), .context = context, .object = object, }; const char* id = JS_ToCString(context, argv[0]); database->id = tf_strdup(id); JS_FreeCString(context, id); JS_SetOpaque(object, database); JS_SetPropertyStr(context, object, "get", JS_NewCFunction(context, _database_get, "get", 1)); JS_SetPropertyStr(context, object, "set", JS_NewCFunction(context, _database_set, "set", 2)); JS_SetPropertyStr(context, object, "exchange", JS_NewCFunction(context, _database_exchange, "exchange", 2)); JS_SetPropertyStr(context, object, "remove", JS_NewCFunction(context, _database_remove, "remove", 1)); JS_SetPropertyStr(context, object, "getAll", JS_NewCFunction(context, _database_get_all, "getAll", 0)); JS_SetPropertyStr(context, object, "getLike", JS_NewCFunction(context, _database_get_like, "getLike", 1)); return object; } static void _database_finalizer(JSRuntime* runtime, JSValue value) { database_t* database = JS_GetOpaque(value, _database_class_id); if (database) { tf_free((void*)database->id); tf_free(database); } --_database_count; } typedef struct _database_get_t { const char* id; const char* key; size_t key_length; char* out_value; size_t out_length; JSValue promise[2]; } database_get_t; static void _database_get_work(tf_ssb_t* ssb, void* user_data) { database_get_t* work = user_data; sqlite3_stmt* statement; sqlite3* db = tf_ssb_acquire_db_reader(ssb); if (sqlite3_prepare(db, "SELECT value FROM properties WHERE id = ? AND key = ?", -1, &statement, NULL) == SQLITE_OK) { if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW) { size_t length = sqlite3_column_bytes(statement, 0); char* data = tf_malloc(length + 1); memcpy(data, sqlite3_column_text(statement, 0), length); data[length] = '\0'; work->out_value = data; work->out_length = length; } sqlite3_finalize(statement); } tf_ssb_release_db_reader(ssb, db); } static void _database_get_after_work(tf_ssb_t* ssb, int status, void* user_data) { database_get_t* work = user_data; JSContext* context = tf_ssb_get_context(ssb); JSValue result = JS_UNDEFINED; if (work->out_value) { result = JS_NewStringLen(context, work->out_value, work->out_length); } JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result); tf_util_report_error(context, error); JS_FreeValue(context, error); JS_FreeValue(context, result); JS_FreeValue(context, work->promise[0]); JS_FreeValue(context, work->promise[1]); tf_free(work->out_value); tf_free(work); } static JSValue _database_get(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) { JSValue result = JS_UNDEFINED; database_t* database = JS_GetOpaque(this_val, _database_class_id); if (database) { tf_ssb_t* ssb = tf_task_get_ssb(database->task); size_t length; const char* key = JS_ToCStringLen(context, &length, argv[0]); database_get_t* work = tf_malloc(sizeof(database_get_t) + strlen(database->id) + 1 + length + 1); *work = (database_get_t) { .id = (const char*)(work + 1), .key = (const char*)(work + 1) + strlen(database->id) + 1, .key_length = length, }; memcpy((char*)work->id, database->id, strlen(database->id) + 1); memcpy((char*)work->key, key, length + 1); JS_FreeCString(context, key); tf_ssb_run_work(ssb, _database_get_work, _database_get_after_work, work); result = JS_NewPromiseCapability(context, work->promise); } return result; } typedef struct _database_set_t { const char* id; const char* key; size_t key_length; const char* value; size_t value_length; bool result; JSValue promise[2]; } database_set_t; static void _database_set_work(tf_ssb_t* ssb, void* user_data) { database_set_t* work = user_data; sqlite3* db = tf_ssb_acquire_db_writer(ssb); sqlite3_stmt* statement; if (sqlite3_prepare(db, "INSERT OR REPLACE INTO properties (id, key, value) VALUES (?1, ?2, ?3)", -1, &statement, NULL) == SQLITE_OK) { if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 3, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_OK) { work->result = true; } sqlite3_finalize(statement); } tf_ssb_release_db_writer(ssb, db); } static void _database_set_after_work(tf_ssb_t* ssb, int status, void* user_data) { database_set_t* work = user_data; JSContext* context = tf_ssb_get_context(ssb); JSValue result = work->result ? JS_TRUE : JS_UNDEFINED; JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result); tf_util_report_error(context, error); JS_FreeValue(context, error); JS_FreeValue(context, result); JS_FreeValue(context, work->promise[0]); JS_FreeValue(context, work->promise[1]); tf_free(work); } static JSValue _database_set(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) { JSValue result = JS_UNDEFINED; database_t* database = JS_GetOpaque(this_val, _database_class_id); if (database) { tf_ssb_t* ssb = tf_task_get_ssb(database->task); size_t key_length = 0; const char* key = JS_ToCStringLen(context, &key_length, argv[0]); size_t value_length = 0; const char* value = JS_ToCStringLen(context, &value_length, argv[1]); database_set_t* work = tf_malloc(sizeof(database_set_t) + strlen(database->id) + 1 + key_length + 1 + value_length + 1); *work = (database_set_t) { .id = (const char*)(work + 1), .key = (const char*)(work + 1) + strlen(database->id) + 1, .value = (const char*)(work + 1) + strlen(database->id) + 1 + key_length + 1, .key_length = key_length, .value_length = value_length, }; memcpy((char*)work->id, database->id, strlen(database->id) + 1); memcpy((char*)work->key, key, key_length + 1); memcpy((char*)work->value, value, value_length + 1); result = JS_NewPromiseCapability(context, work->promise); tf_ssb_run_work(ssb, _database_set_work, _database_set_after_work, work); JS_FreeCString(context, key); JS_FreeCString(context, value); } return result; } typedef struct _database_exchange_t { const char* id; const char* key; size_t key_length; const char* expected; size_t expected_length; const char* value; size_t value_length; bool result; JSValue promise[2]; } database_exchange_t; static void _database_exchange_work(tf_ssb_t* ssb, void* user_data) { database_exchange_t* work = user_data; sqlite3* db = tf_ssb_acquire_db_writer(ssb); sqlite3_stmt* statement; if (!work->expected) { if (sqlite3_prepare(db, "INSERT INTO properties (id, key, value) VALUES (?1, ?2, ?3) ON CONFLICT DO NOTHING", -1, &statement, NULL) == SQLITE_OK) { if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->key, work->key_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 3, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE) { work->result = sqlite3_changes(db) != 0; } sqlite3_finalize(statement); } } else if (sqlite3_prepare(db, "UPDATE properties SET value = ?1 WHERE id = ?2 AND key = ?3 AND value = ?4", -1, &statement, NULL) == SQLITE_OK) { if (sqlite3_bind_text(statement, 1, work->value, work->value_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, work->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 3, work->key, work->key_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 4, work->expected, work->expected_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE) { work->result = sqlite3_changes(db) != 0; } sqlite3_finalize(statement); } tf_ssb_release_db_writer(ssb, db); } static void _database_exchange_after_work(tf_ssb_t* ssb, int status, void* user_data) { database_exchange_t* work = user_data; JSContext* context = tf_ssb_get_context(ssb); JSValue result = work->result ? JS_TRUE : JS_UNDEFINED; JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result); tf_util_report_error(context, error); JS_FreeValue(context, error); JS_FreeValue(context, result); JS_FreeValue(context, work->promise[0]); JS_FreeValue(context, work->promise[1]); JS_FreeCString(context, work->key); JS_FreeCString(context, work->expected); JS_FreeCString(context, work->value); tf_free((char*)work->id); tf_free(work); } static JSValue _database_exchange(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) { JSValue result = JS_UNDEFINED; database_t* database = JS_GetOpaque(this_val, _database_class_id); if (database) { tf_ssb_t* ssb = tf_task_get_ssb(database->task); database_exchange_t* work = tf_malloc(sizeof(database_exchange_t)); *work = (database_exchange_t) { .id = tf_strdup(database->id), }; work->key = JS_ToCStringLen(context, &work->key_length, argv[0]); work->expected = (JS_IsNull(argv[1]) || JS_IsUndefined(argv[1])) ? NULL : JS_ToCStringLen(context, &work->expected_length, argv[1]); work->value = JS_ToCStringLen(context, &work->value_length, argv[2]); result = JS_NewPromiseCapability(context, work->promise); tf_ssb_run_work(ssb, _database_exchange_work, _database_exchange_after_work, work); } return result; } static JSValue _database_remove(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) { database_t* database = JS_GetOpaque(this_val, _database_class_id); if (database) { sqlite3_stmt* statement; tf_ssb_t* ssb = tf_task_get_ssb(database->task); sqlite3* db = tf_ssb_acquire_db_writer(ssb); if (sqlite3_prepare(db, "DELETE FROM properties WHERE id = ?1 AND key = ?2", -1, &statement, NULL) == SQLITE_OK) { size_t keyLength; const char* keyString = JS_ToCStringLen(context, &keyLength, argv[0]); if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, keyString, keyLength, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_OK) { } JS_FreeCString(context, keyString); sqlite3_finalize(statement); } tf_ssb_release_db_writer(ssb, db); } return JS_UNDEFINED; } typedef struct _database_get_all_t { const char* id; const char* key; size_t key_length; char** out_values; size_t* out_lengths; int out_values_length; JSValue promise[2]; } database_get_all_t; static void _database_get_all_work(tf_ssb_t* ssb, void* user_data) { database_get_all_t* work = user_data; sqlite3_stmt* statement; sqlite3* db = tf_ssb_acquire_db_reader(ssb); if (sqlite3_prepare(db, "SELECT key FROM properties WHERE id = ?", -1, &statement, NULL) == SQLITE_OK) { if (sqlite3_bind_text(statement, 1, work->id, -1, NULL) == SQLITE_OK) { while (sqlite3_step(statement) == SQLITE_ROW) { work->out_values = tf_resize_vec(work->out_values, sizeof(char*) * (work->out_values_length + 1)); work->out_lengths = tf_resize_vec(work->out_lengths, sizeof(size_t) * (work->out_values_length + 1)); size_t length = sqlite3_column_bytes(statement, 0); char* data = tf_malloc(length + 1); memcpy(data, sqlite3_column_text(statement, 0), length); data[length] = '\0'; work->out_values[work->out_values_length] = data; work->out_lengths[work->out_values_length] = length; work->out_values_length++; } } sqlite3_finalize(statement); } tf_ssb_release_db_reader(ssb, db); } static void _database_get_all_after_work(tf_ssb_t* ssb, int status, void* user_data) { database_get_all_t* work = user_data; JSContext* context = tf_ssb_get_context(ssb); JSValue result = JS_NewArray(context); ; for (int i = 0; i < work->out_values_length; i++) { JS_SetPropertyUint32(context, result, i, JS_NewStringLen(context, work->out_values[i], work->out_lengths[i])); tf_free((void*)work->out_values[i]); } JSValue error = JS_Call(context, work->promise[0], JS_UNDEFINED, 1, &result); tf_util_report_error(context, error); JS_FreeValue(context, error); JS_FreeValue(context, result); JS_FreeValue(context, work->promise[0]); JS_FreeValue(context, work->promise[1]); tf_free(work->out_values); tf_free(work->out_lengths); tf_free(work); } static JSValue _database_get_all(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) { JSValue result = JS_UNDEFINED; database_t* database = JS_GetOpaque(this_val, _database_class_id); if (database) { tf_ssb_t* ssb = tf_task_get_ssb(database->task); size_t length; const char* key = JS_ToCStringLen(context, &length, argv[0]); database_get_all_t* work = tf_malloc(sizeof(database_get_all_t) + strlen(database->id) + 1 + length + 1); *work = (database_get_all_t) { .id = (const char*)(work + 1), .key = (const char*)(work + 1) + strlen(database->id) + 1, .key_length = length, }; memcpy((char*)work->id, database->id, strlen(database->id) + 1); memcpy((char*)work->key, key, length + 1); JS_FreeCString(context, key); tf_ssb_run_work(ssb, _database_get_all_work, _database_get_all_after_work, work); result = JS_NewPromiseCapability(context, work->promise); } return result; } static JSValue _database_get_like(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) { JSValue result = JS_UNDEFINED; database_t* database = JS_GetOpaque(this_val, _database_class_id); if (database) { sqlite3_stmt* statement; tf_ssb_t* ssb = tf_task_get_ssb(database->task); sqlite3* db = tf_ssb_acquire_db_reader(ssb); if (sqlite3_prepare(db, "SELECT key, value FROM properties WHERE id = ? AND KEY LIKE ?", -1, &statement, NULL) == SQLITE_OK) { const char* pattern = JS_ToCString(context, argv[0]); if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, pattern, -1, NULL) == SQLITE_OK) { result = JS_NewObject(context); while (sqlite3_step(statement) == SQLITE_ROW) { JS_SetPropertyStr(context, result, (const char*)sqlite3_column_text(statement, 0), JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 1), sqlite3_column_bytes(statement, 1))); } } JS_FreeCString(context, pattern); sqlite3_finalize(statement); } tf_ssb_release_db_reader(ssb, db); } return result; } static JSValue _databases_list(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* data) { tf_task_t* task = tf_task_get(context); tf_ssb_t* ssb = tf_task_get_ssb(task); sqlite3* db = tf_ssb_acquire_db_reader(ssb); JSValue array = JS_UNDEFINED; sqlite3_stmt* statement; if (sqlite3_prepare(db, "SELECT DISTINCT id FROM properties WHERE id LIKE ?", -1, &statement, NULL) == SQLITE_OK) { const char* pattern = JS_ToCString(context, argv[0]); if (sqlite3_bind_text(statement, 1, pattern, -1, NULL) == SQLITE_OK) { array = JS_NewArray(context); uint32_t index = 0; while (sqlite3_step(statement) == SQLITE_ROW) { JS_SetPropertyUint32(context, array, index++, JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0))); } } JS_FreeCString(context, pattern); sqlite3_finalize(statement); } tf_ssb_release_db_reader(ssb, db); return array; }