#include "database.js.h" #include "log.h" #include "mem.h" #include "ssb.h" #include "task.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; } static JSValue _database_get(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) { JSValue entry = JS_UNDEFINED; database_t* database = JS_GetOpaque(this_val, _database_class_id); if (database) { tf_ssb_t* ssb = tf_task_get_ssb(database->task); 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) { size_t length; const char* keyString = JS_ToCStringLen(context, &length, argv[0]); if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, keyString, length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_ROW) { entry = JS_NewStringLen(context, (const char*)sqlite3_column_text(statement, 0), sqlite3_column_bytes(statement, 0)); } JS_FreeCString(context, keyString); sqlite3_finalize(statement); } tf_ssb_release_db_reader(ssb, db); } return entry; } JSValue _database_set(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, "INSERT OR REPLACE INTO properties (id, key, value) VALUES ($1, $2, $3)", -1, &statement, NULL) == SQLITE_OK) { size_t keyLength; const char* keyString = JS_ToCStringLen(context, &keyLength, argv[0]); size_t valueLength; const char* valueString = JS_ToCStringLen(context, &valueLength, argv[1]); if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, keyString, keyLength, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 3, valueString, valueLength, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_OK) { } JS_FreeCString(context, keyString); JS_FreeCString(context, valueString); sqlite3_finalize(statement); } tf_ssb_release_db_writer(ssb, db); } return JS_UNDEFINED; } static JSValue _database_exchange(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) { JSValue exchanged = 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_writer(ssb); if (JS_IsNull(argv[1]) || JS_IsUndefined(argv[1])) { if (sqlite3_prepare(db, "INSERT INTO properties (id, key, value) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", -1, &statement, NULL) == SQLITE_OK) { size_t key_length; size_t set_length; const char* key = JS_ToCStringLen(context, &key_length, argv[0]); const char* set = JS_ToCStringLen(context, &set_length, argv[2]); if (sqlite3_bind_text(statement, 1, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, key, key_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 3, set, set_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE) { exchanged = sqlite3_changes(db) != 0 ? JS_TRUE : JS_FALSE; } JS_FreeCString(context, key); JS_FreeCString(context, set); 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) { size_t key_length; size_t expected_length; size_t set_length; const char* key = JS_ToCStringLen(context, &key_length, argv[0]); const char* expected = JS_ToCStringLen(context, &expected_length, argv[1]); const char* set = JS_ToCStringLen(context, &set_length, argv[2]); if (sqlite3_bind_text(statement, 1, set, set_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 2, database->id, -1, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 3, key, key_length, NULL) == SQLITE_OK && sqlite3_bind_text(statement, 4, expected, expected_length, NULL) == SQLITE_OK && sqlite3_step(statement) == SQLITE_DONE) { exchanged = sqlite3_changes(db) != 0 ? JS_TRUE : JS_FALSE; } JS_FreeCString(context, key); JS_FreeCString(context, expected); JS_FreeCString(context, set); sqlite3_finalize(statement); } tf_ssb_release_db_writer(ssb, db); } return exchanged; } 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; } JSValue _database_get_all(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv) { JSValue array = 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 = $1", -1, &statement, NULL) == SQLITE_OK) { if (sqlite3_bind_text(statement, 1, database->id, -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))); } } sqlite3_finalize(statement); } tf_ssb_release_db_reader(ssb, db); } return array; } 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; }