diff --git a/lib/types/holder-types.js b/lib/types/holder-types.js --- a/lib/types/holder-types.js +++ b/lib/types/holder-types.js @@ -27,3 +27,9 @@ +blobHash: string, +holder: string, }; + +export type ClientDBHolderItem = { + +hash: string, + +holder: string, + +status: string, +}; diff --git a/native/cpp/CommonCpp/DatabaseManagers/CMakeLists.txt b/native/cpp/CommonCpp/DatabaseManagers/CMakeLists.txt --- a/native/cpp/CommonCpp/DatabaseManagers/CMakeLists.txt +++ b/native/cpp/CommonCpp/DatabaseManagers/CMakeLists.txt @@ -16,6 +16,7 @@ "entities/EntityQueryHelpers.h" "entities/SQLiteDataConverters.h" "entities/Draft.h" + "entities/Holder.h" "entities/Media.h" "entities/Message.h" "entities/Metadata.h" diff --git a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h --- a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h @@ -6,6 +6,7 @@ #include "entities/DMOperation.h" #include "entities/Draft.h" #include "entities/EntryInfo.h" +#include "entities/Holder.h" #include "entities/InboundP2PMessage.h" #include "entities/IntegrityThreadHash.h" #include "entities/KeyserverInfo.h" @@ -208,6 +209,9 @@ virtual int getDatabaseVersion() const = 0; virtual std::optional getSyncedMetadata(const std::string &entryName) const = 0; + virtual void replaceHolder(const Holder &holder) const = 0; + virtual void removeHolders(const std::vector &hashes) const = 0; + virtual std::vector getHolders() const = 0; virtual ~DatabaseQueryExecutor() = default; }; diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h @@ -7,6 +7,7 @@ #include "entities/CommunityInfo.h" #include "entities/DMOperation.h" #include "entities/Draft.h" +#include "entities/Holder.h" #include "entities/IntegrityThreadHash.h" #include "entities/KeyserverInfo.h" #include "entities/LocalMessageInfo.h" @@ -199,6 +200,9 @@ int getDatabaseVersion() const override; std::optional getSyncedMetadata(const std::string &entryName) const override; + void replaceHolder(const Holder &holder) const override; + void removeHolders(const std::vector &hashes) const override; + std::vector getHolders() const override; }; } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp @@ -1743,4 +1743,31 @@ return entry->data; } +void SQLiteQueryExecutor::replaceHolder(const Holder &holder) const { + static std::string query = + "REPLACE INTO holders (hash, holder, status) " + "VALUES (:hash, :holder, :status);"; + replaceEntity(this->getConnection(), query, holder); +} + +void SQLiteQueryExecutor::removeHolders( + const std::vector &hashes) const { + if (!hashes.size()) { + return; + } + + std::stringstream queryStream; + queryStream << "DELETE FROM holders " + "WHERE hash IN " + << getSQLStatementArray(hashes.size()) << ";"; + removeEntitiesByKeys(this->getConnection(), queryStream.str(), hashes); +} + +std::vector SQLiteQueryExecutor::getHolders() const { + static std::string query = + "SELECT hash, holder, status " + "FROM holders;"; + return getAllEntities(this->getConnection(), query); +} + } // namespace comm diff --git a/native/cpp/CommonCpp/DatabaseManagers/entities/Holder.h b/native/cpp/CommonCpp/DatabaseManagers/entities/Holder.h new file mode 100644 --- /dev/null +++ b/native/cpp/CommonCpp/DatabaseManagers/entities/Holder.h @@ -0,0 +1,37 @@ +#pragma once + +#include "SQLiteDataConverters.h" +#include +#include + +namespace comm { + +struct Holder { + std::string hash; + std::string holder; + std::string status; + + static Holder fromSQLResult(sqlite3_stmt *sqlRow, int idx) { + return Holder{ + getStringFromSQLRow(sqlRow, idx), + getStringFromSQLRow(sqlRow, idx + 1), + getStringFromSQLRow(sqlRow, idx + 2)}; + } + + int bindToSQL(sqlite3_stmt *sql, int idx) const { + int err; + + int hash_index = sqlite3_bind_parameter_index(sql, ":hash"); + err = bindStringToSQL(hash, sql, hash_index); + + int holder_index = sqlite3_bind_parameter_index(sql, ":holder"); + err = bindStringToSQL(holder, sql, holder_index); + + int status_index = sqlite3_bind_parameter_index(sql, ":status"); + err = bindStringToSQL(status, sql, status_index); + + return err; + } +}; + +} // namespace comm diff --git a/native/ios/Comm.xcodeproj/project.pbxproj b/native/ios/Comm.xcodeproj/project.pbxproj --- a/native/ios/Comm.xcodeproj/project.pbxproj +++ b/native/ios/Comm.xcodeproj/project.pbxproj @@ -258,6 +258,7 @@ 8E1805542DA954B600B772A4 /* SQLiteSchema.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SQLiteSchema.h; sourceTree = ""; }; 8E1805552DA954B600B772A4 /* SQLiteSchema.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SQLiteSchema.cpp; sourceTree = ""; }; 8E1805582DA95E7C00B772A4 /* SQLiteSchemaMigrations.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SQLiteSchemaMigrations.cpp; sourceTree = ""; }; + 8E189A282E16D1B500E11E87 /* Holder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Holder.h; sourceTree = ""; }; 8E2CC2562B5C999A000C94D6 /* KeyserverStoreOperations.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KeyserverStoreOperations.h; sourceTree = ""; }; 8E2CC2572B5C99B0000C94D6 /* KeyserverStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = KeyserverStore.h; path = PersistentStorageUtilities/DataStores/KeyserverStore.h; sourceTree = ""; }; 8E2CC2582B5C99B0000C94D6 /* KeyserverStore.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = KeyserverStore.cpp; path = PersistentStorageUtilities/DataStores/KeyserverStore.cpp; sourceTree = ""; }; @@ -588,6 +589,7 @@ 71BE84442636A944002849D2 /* entities */ = { isa = PBXGroup; children = ( + 8E189A282E16D1B500E11E87 /* Holder.h */, 0E02676D2D81EAD800788249 /* DMOperation.h */, 816D2D5B2C480E9E001C0B67 /* MessageSearchResult.h */, CB01F0C32B67F3970089E1F9 /* SQLiteStatementWrapper.cpp */, diff --git a/web/cpp/SQLiteQueryExecutorBindings.cpp b/web/cpp/SQLiteQueryExecutorBindings.cpp --- a/web/cpp/SQLiteQueryExecutorBindings.cpp +++ b/web/cpp/SQLiteQueryExecutorBindings.cpp @@ -140,6 +140,11 @@ .field("plaintext", &InboundP2PMessage::plaintext) .field("status", &InboundP2PMessage::status); + value_object("Holder") + .field("hash", &Holder::hash) + .field("holder", &Holder::holder) + .field("status", &Holder::status); + class_("SQLiteQueryExecutor") .constructor() .function("migrate", &SQLiteQueryExecutor::migrate) @@ -344,7 +349,10 @@ .function("searchMessages", &SQLiteQueryExecutor::searchMessages) .function("fetchMessages", &SQLiteQueryExecutor::fetchMessages) .function("getDatabaseVersion", &SQLiteQueryExecutor::getDatabaseVersion) - .function("getSyncedMetadata", &SQLiteQueryExecutor::getSyncedMetadata); + .function("getSyncedMetadata", &SQLiteQueryExecutor::getSyncedMetadata) + .function("replaceHolder", &SQLiteQueryExecutor::replaceHolder) + .function("removeHolders", &SQLiteQueryExecutor::removeHolders) + .function("getHolders", &SQLiteQueryExecutor::getHolders); class_("SQLiteBackup") .class_function( diff --git a/web/shared-worker/_generated/comm_query_executor.wasm b/web/shared-worker/_generated/comm_query_executor.wasm index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@ { + let queryExecutor: ?SQLiteQueryExecutor = null; + let dbModule: ?EmscriptenModule = null; + + beforeAll(async () => { + dbModule = getDatabaseModule(); + }); + + beforeEach(() => { + if (!dbModule) { + throw new Error('Database module is missing'); + } + queryExecutor = new dbModule.SQLiteQueryExecutor(FILE_PATH, false); + if (!queryExecutor) { + throw new Error('SQLiteQueryExecutor is missing'); + } + queryExecutor?.replaceHolder(TEST_HOLDER_1); + queryExecutor?.replaceHolder(TEST_HOLDER_2); + }); + + afterEach(() => { + if (!dbModule || !queryExecutor) { + return; + } + clearSensitiveData(dbModule, FILE_PATH, queryExecutor); + }); + + it('should return all holders', () => { + const holders = queryExecutor?.getHolders() ?? []; + expect(holders?.length).toBe(2); + + expect( + [...holders].sort((a: ClientDBHolderItem, b: ClientDBHolderItem) => + a.hash.localeCompare(b.hash), + ), + ).toEqual( + [TEST_HOLDER_1, TEST_HOLDER_2].sort( + (a: ClientDBHolderItem, b: ClientDBHolderItem) => + a.hash.localeCompare(b.hash), + ), + ); + }); + + it('should remove holders', () => { + queryExecutor?.removeHolders([TEST_HOLDER_1.hash]); + const holders = queryExecutor?.getHolders(); + expect(holders?.length).toBe(1); + + const remainingHashes = holders?.map(h => h.hash); + expect(remainingHashes).toContain(TEST_HOLDER_2.hash); + expect(remainingHashes).not.toContain(TEST_HOLDER_1.hash); + }); + + it('should update holder', () => { + queryExecutor?.replaceHolder({ ...TEST_HOLDER_2, status: 'NOT_REMOVED' }); + const holders = queryExecutor?.getHolders(); + expect(holders?.length).toBe(2); + + const holder = holders?.find(h => h.hash === TEST_HOLDER_2.hash); + expect(holder?.status).toEqual('NOT_REMOVED'); + }); +}); diff --git a/web/shared-worker/types/sqlite-query-executor.js b/web/shared-worker/types/sqlite-query-executor.js --- a/web/shared-worker/types/sqlite-query-executor.js +++ b/web/shared-worker/types/sqlite-query-executor.js @@ -11,6 +11,7 @@ import type { ClientDBThreadActivityEntry } from 'lib/ops/thread-activity-store-ops.js'; import type { ClientDBUserInfo } from 'lib/ops/user-store-ops.js'; import type { ClientDBDraftInfo } from 'lib/types/draft-types.js'; +import type { ClientDBHolderItem } from 'lib/types/holder-types.js'; import type { ClientDBLocalMessageInfo } from 'lib/types/message-types.js'; import type { OutboundP2PMessage, @@ -224,6 +225,10 @@ getDatabaseVersion(): number; getSyncedMetadata(entryName: string): ?string; + replaceHolder(holder: ClientDBHolderItem): void; + removeHolders(hashes: $ReadOnlyArray): void; + getHolders(): $ReadOnlyArray; + // method is provided to manually signal that a C++ object // is no longer needed and can be deleted delete(): void;