diff --git a/.buildkite/swift-test.sh b/.buildkite/swift-test.sh index 2155606e7..1b12f4af1 100755 --- a/.buildkite/swift-test.sh +++ b/.buildkite/swift-test.sh @@ -18,7 +18,7 @@ function build_for_real_device() { echo "--- :swift: Building for $platform device" export NSUnbufferedIO=YES xcodebuild -destination "generic/platform=$platform" \ - -scheme WordPressAPI \ + -scheme WordPressAPI-Package \ -derivedDataPath DerivedData \ -skipPackagePluginValidation \ build | xcbeautify diff --git a/.gitignore b/.gitignore index 5a213c67c..313f21887 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ /docs /docs.tar.gz +# Test DB Files +kotlin.db +swift.db + # Ignore Gradle project-specific cache directory .gradle @@ -33,7 +37,6 @@ fastlane/report.xml libwordPressFFI.xcframework* /swift-docs.tar.gz - # Auto-generated Swift Files native/swift/Sources/wordpress-api-wrapper/*.swift diff --git a/Cargo.lock b/Cargo.lock index b12a2c701..998e588b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,6 +875,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -1004,6 +1016,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1332,6 +1350,18 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] [[package]] name = "heck" @@ -2044,6 +2074,17 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -3096,6 +3137,20 @@ dependencies = [ "syn", ] +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.8.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -5000,6 +5055,7 @@ dependencies = [ "roxmltree", "rstest", "rstest_reuse", + "rusqlite", "rustls", "scraper", "serde", diff --git a/Makefile b/Makefile index 729deb4fa..bc49d24cd 100644 --- a/Makefile +++ b/Makefile @@ -102,7 +102,7 @@ _build-apple-%-tvos _build-apple-%-tvos-sim _build-apple-%-watchos _build-apple- # Build the library for a specific target _build-apple-%: - cargo $(CARGO_OPTS) $(cargo_config_library) build --target $* --package wp_api --profile $(CARGO_PROFILE) + cargo $(CARGO_OPTS) $(cargo_config_library) build --target $* --package wp_api --profile $(CARGO_PROFILE) --no-default-features ./scripts/swift-bindings.sh target/$*/$(CARGO_PROFILE_DIRNAME)/libwp_api.a # Build the library for one single platform, including real device and simulator. diff --git a/Package.resolved b/Package.resolved index 9bd433af1..e0098449e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c800e108ef4eae436d8444968bc4e6f1d076a0d019136719caee3dcce13fe09a", + "originHash" : "6791ea51c1f6a770231d8e16e3b9310037ce94ea7a924534fab9039ed08061cd", "pins" : [ { "identity" : "collectionconcurrencykit", diff --git a/Package.swift b/Package.swift index 9f3f548c9..8fe6d4bb1 100644 --- a/Package.swift +++ b/Package.swift @@ -26,6 +26,10 @@ var package = Package( .library( name: "WordPressAPI", targets: ["WordPressAPI"] + ), + .library( + name: "WordPressApiCache", + targets: ["WordPressApiCache"] ) ], dependencies: [ @@ -56,6 +60,13 @@ var package = Package( .swiftLanguageMode(.v5) ] ), + .target( + name: "WordPressApiCache", + dependencies: [ + .target(name: "WordPressAPIInternal") + ], + path: "native/swift/Sources/wordpress-api-cache" + ), libwordpressFFI, .testTarget( name: "WordPressAPITests", @@ -68,6 +79,14 @@ var package = Package( swiftSettings: [ .define("PROGRESS_REPORTING_ENABLED", .when(platforms: [.iOS, .macOS, .tvOS, .watchOS])) ] + ), + .testTarget( + name: "WordPressApiCacheTests", + dependencies: [ + .target(name: "WordPressApiCache"), + .target(name: "WordPressAPIInternal") + ], + path: "native/swift/Tests/wordpress-api-cache" ) ].addingIntegrationTests() ) diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/WordPressApiCacheTest.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/WordPressApiCacheTest.kt new file mode 100644 index 000000000..d70f56ee5 --- /dev/null +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/WordPressApiCacheTest.kt @@ -0,0 +1,32 @@ +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode +import rs.wordpress.cache.kotlin.WordPressApiCache +import rs.wordpress.cache.kotlin.WordPressApiCacheDelegate +import kotlin.test.assertEquals + +@Execution(ExecutionMode.CONCURRENT) +class WordPressApiCacheTest { + + @Test + fun testThatMigrationsWork() = runTest { + assertEquals(2, WordPressApiCache().performMigrations()) + } + + @Test + fun testBackgroundUpdateNotificationsWork() = runTest { + var updateCount = 0 + val delegate = WordPressApiCacheDelegate( + callback = { updateHook -> + updateCount += 1 + } + ) + + val cache = WordPressApiCache(delegate = delegate) + cache.startListeningForUpdates() + + val migrationCount = cache.performMigrations() + assertEquals(updateCount, migrationCount) + } +} diff --git a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/cache/kotlin/WordPressApiCache.kt b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/cache/kotlin/WordPressApiCache.kt new file mode 100644 index 000000000..e8117e37a --- /dev/null +++ b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/cache/kotlin/WordPressApiCache.kt @@ -0,0 +1,54 @@ +package rs.wordpress.cache.kotlin + +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.withContext +import uniffi.wp_api.DatabaseDelegate +import uniffi.wp_api.UpdateHook +import uniffi.wp_api.WpApiCache +import java.nio.file.Path +import java.util.concurrent.Executors + +class WordPressApiCacheLoggingDelegate: DatabaseDelegate { + override fun didUpdate(updateHook: UpdateHook) { + println("Received update: $updateHook") + } +} +class WordPressApiCacheDelegate( + private val callback: (updateHook: UpdateHook) -> Unit +) : DatabaseDelegate { + + override fun didUpdate(updateHook: UpdateHook) { + callback(updateHook) + } +} + +class WordPressApiCache { + private val cache: WpApiCache + private val internalDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private val delegate: DatabaseDelegate? + + // Creates a new in-memory cache + constructor(delegate: WordPressApiCacheDelegate? = null) : this(":memory:", delegate) + + // Creates a new cache at the specified file system URL + constructor(path: Path, delegate: WordPressApiCacheDelegate? = null) : this(path.toString(), delegate) + + // Creates a new cache at the specified path + constructor(string: String, delegate: WordPressApiCacheDelegate? = null) { + this.cache = WpApiCache(string) + this.delegate = delegate + } + + suspend fun performMigrations(): Int = withContext(internalDispatcher) { + cache.performMigrations().toInt() + } + fun startListeningForUpdates() { + if (this.delegate != null) { + this.cache.startListeningForUpdates(this.delegate) + } + } + + fun stopListeningForUpdates() { + this.cache.stopListeningForUpdates() + } +} diff --git a/native/swift/Sources/wordpress-api-cache/WordPressApiCache.swift b/native/swift/Sources/wordpress-api-cache/WordPressApiCache.swift new file mode 100644 index 000000000..8c29e73eb --- /dev/null +++ b/native/swift/Sources/wordpress-api-cache/WordPressApiCache.swift @@ -0,0 +1,57 @@ +import Foundation +import WordPressAPIInternal + +public actor WordPressApiCache { + + private let cache: WpApiCache + private let delegate: any DatabaseDelegate + + public struct Notifications { + public static let cacheDidUpdate = Notification.Name("WordPressApiCache.cacheDidUpdate") + + public static func name(for table: String) -> Notification.Name { + Notification.Name(rawValue: "WordPressApiCache.cacheDidUpdate.\(table)") + } + } + + final public class ApiCacheDelegate: DatabaseDelegate { + public init() {} + + public func didUpdate(updateHook: WordPressAPIInternal.UpdateHook) { + let name = Notifications.name(for: updateHook.tableName) + NotificationCenter.default.post(name: name, object: updateHook) + } + } + + /// Creates a new in-memory cache + public init(delegate: DatabaseDelegate = ApiCacheDelegate()) throws { + try self.init(path: ":memory:", delegate: delegate) + } + + /// Creates a new cache at the specified file system URL + public init(url: URL, delegate: DatabaseDelegate = ApiCacheDelegate()) throws { + try self.init(path: url.absoluteString, delegate: delegate) + } + + /// Creates a new cache at the specified path + public init(path: String, delegate: DatabaseDelegate = ApiCacheDelegate()) throws { + self.cache = try WpApiCache(path: path) + self.delegate = delegate + } + + public func performMigrations() async throws -> Int { + return Int(try self.cache.performMigrations()) + } + + public func startListeningForUpdates() { + self.cache.startListeningForUpdates(delegate: self.delegate) + } + + public func stopListeningForUpdates() { + self.cache.stopListeningForUpdates() + } + + deinit { + self.cache.stopListeningForUpdates() + } +} diff --git a/native/swift/Tests/wordpress-api-cache/WordPressApiCacheTests.swift b/native/swift/Tests/wordpress-api-cache/WordPressApiCacheTests.swift new file mode 100644 index 000000000..00d4eb891 --- /dev/null +++ b/native/swift/Tests/wordpress-api-cache/WordPressApiCacheTests.swift @@ -0,0 +1,41 @@ +import Foundation +import Testing +import WordPressApiCache + +actor Test { + + private var cache: WordPressApiCache! + private var changeCount = 0 + + init() throws { + self.cache = try WordPressApiCache() + } + + @Test func testMigrationsWork() async throws { + let migrationsPerformed = try await self.cache.performMigrations() + #expect(migrationsPerformed == 2) + } + + @Test func testBackgroundUpdateNotificationsWork() async throws { + let name = WordPressApiCache.Notifications.name(for: "_migrations") + + let handle = Task { + for await _ in NotificationCenter.default.notifications(named: name) { + self.incrementChangeCount() + } + } + + await self.cache.startListeningForUpdates() + let migrationCount = try await self.cache.performMigrations() + + // Wait for NotificationCenter to finish delivery + try await Task.sleep(nanoseconds: 10 * NSEC_PER_MSEC) + + #expect(migrationCount == self.changeCount) + handle.cancel() + } + + func incrementChangeCount() { + self.changeCount += 1 + } +} diff --git a/scripts/swift-bindings.sh b/scripts/swift-bindings.sh index 79bfd9442..27574aac9 100755 --- a/scripts/swift-bindings.sh +++ b/scripts/swift-bindings.sh @@ -32,6 +32,11 @@ extension $error_type: LocalizedError { } EOF done + + # Use sed to replace `import SQLite3` with the wrapped version + sed -i.bak 's/^import SQLite3$/#if canImport(SQLite3)\ +import SQLite3\ +#endif/' $swift_binding } for swift_binding in "$output_dir"/*.swift; do diff --git a/scripts/xcodebuild-test.sh b/scripts/xcodebuild-test.sh index 1afeb8d5f..b48af587f 100755 --- a/scripts/xcodebuild-test.sh +++ b/scripts/xcodebuild-test.sh @@ -11,7 +11,7 @@ device_id=$(xcrun simctl list --json devices available | jq -re ".devices.\"com. export NSUnbufferedIO=YES xcodebuild \ - -scheme WordPressAPI \ + -scheme WordPressAPI-Package \ -derivedDataPath DerivedData \ -destination "id=${device_id}" \ -skipPackagePluginValidation \ diff --git a/wp_api/Cargo.toml b/wp_api/Cargo.toml index 8b67c2536..86779c086 100644 --- a/wp_api/Cargo.toml +++ b/wp_api/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [features] +default = ["rusqlite/bundled"] integration-tests = [] reqwest-request-executor = ["dep:reqwest", "dep:tokio", "dep:hyper-util", "dep:rustls", "dep:hickory-resolver", "dep:hyper", "dep:h2"] @@ -47,6 +48,7 @@ wp_localization = { path = "../wp_localization" } wp_localization_macro = { path = "../wp_localization_macro" } wp_serde_helper = { path = "../wp_serde_helper" } x509-cert = { workspace = true } +rusqlite = { version = "0.37.0", features = ["hooks"] } [dev-dependencies] rstest = { workspace = true } diff --git a/wp_api/src/cache/migrations/0001-create-posts-table.sql b/wp_api/src/cache/migrations/0001-create-posts-table.sql new file mode 100644 index 000000000..07a74c91c --- /dev/null +++ b/wp_api/src/cache/migrations/0001-create-posts-table.sql @@ -0,0 +1,22 @@ +CREATE TABLE `posts` ( + `rowid` INTEGER PRIMARY KEY AUTOINCREMENT, + `post_id` INTEGER NOT NULL, + `context` TEXT COLLATE NOCASE NOT NULL, + `post_author` INTEGER NOT NULL, + `post_date` TEXT COLLATE NOCASE NOT NULL, + `post_content` TEXT COLLATE NOCASE NOT NULL, + `post_title` TEXT COLLATE NOCASE NOT NULL, + `post_excerpt` TEXT COLLATE NOCASE NOT NULL, + `post_status` TEXT COLLATE NOCASE NOT NULL, + `comment_status` TEXT COLLATE NOCASE NOT NULL, + `ping_status` TEXT COLLATE NOCASE NOT NULL, + `post_password` TEXT COLLATE NOCASE DEFAULT NULL, + `post_modified` TEXT COLLATE NOCASE NOT NULL, + `post_parent` INTEGER, + `guid` TEXT COLLATE NOCASE NOT NULL, + `menu_order` INTEGER NOT NULL DEFAULT '0', + `post_type` TEXT COLLATE NOCASE NOT NULL, + `comment_count` INTEGER NOT NULL +) STRICT; + +CREATE UNIQUE INDEX idx_posts_have_unique_post_id_and_context ON posts(post_id, context); diff --git a/wp_api/src/cache/migrations/0002-create-users-table.sql b/wp_api/src/cache/migrations/0002-create-users-table.sql new file mode 100644 index 000000000..b5b68af95 --- /dev/null +++ b/wp_api/src/cache/migrations/0002-create-users-table.sql @@ -0,0 +1,14 @@ +CREATE TABLE `users` ( + `rowid` INTEGER PRIMARY KEY AUTOINCREMENT, + `user_id` INTEGER NOT NULL, + `context` TEXT COLLATE NOCASE NOT NULL, + `user_login` TEXT COLLATE NOCASE NOT NULL, + `user_nicename` TEXT COLLATE NOCASE NOT NULL, + `user_email` TEXT COLLATE NOCASE NOT NULL, + `user_url` TEXT COLLATE NOCASE NOT NULL, + `user_registered` TEXT COLLATE NOCASE NOT NULL, + `user_status` INTEGER NOT NULL, + `display_name` TEXT COLLATE NOCASE NOT NULL +) STRICT; + +CREATE UNIQUE INDEX idx_users_have_unique_user_id_and_context ON users(user_id, context); diff --git a/wp_api/src/cache/mod.rs b/wp_api/src/cache/mod.rs new file mode 100644 index 000000000..dd46670a4 --- /dev/null +++ b/wp_api/src/cache/mod.rs @@ -0,0 +1 @@ +pub mod wp_api_cache; diff --git a/wp_api/src/cache/wp_api_cache.rs b/wp_api/src/cache/wp_api_cache.rs new file mode 100644 index 000000000..b4b2a74ae --- /dev/null +++ b/wp_api/src/cache/wp_api_cache.rs @@ -0,0 +1,195 @@ +use rusqlite::hooks::Action; +use rusqlite::{Connection, Result as SqliteResult, params}; +use std::sync::{Arc, Mutex}; + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, uniffi::Error)] +pub enum SqliteDbError { + SqliteError(String), +} + +impl std::fmt::Display for SqliteDbError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SqliteDbError::SqliteError(message) => write!(f, "SqliteDbError: message={}", message), + } + } +} + +impl From for SqliteDbError { + fn from(err: rusqlite::Error) -> Self { + SqliteDbError::SqliteError(err.to_string()) + } +} + +#[derive(uniffi::Object)] +pub struct WpApiCache { + inner: DBManager +} + +#[uniffi::export] +impl WpApiCache { + #[uniffi::constructor] + pub fn new(path: Option) -> Result { + Ok(Self { + inner: DBManager::new(&path)? + }) + } + + pub fn perform_migrations(&self) -> Result { + let connection: &Connection = &self.inner.connection.lock().unwrap(); + Ok(MigrationManager::new(connection)?.perform_migrations()?) + } + + pub fn flush(&self) -> Result<(), SqliteDbError> { + let connection: &Connection = &self.inner.connection.lock().unwrap(); + connection.execute("commit", ())?; + Ok(()) + } + + pub fn start_listening_for_updates(&self, delegate: Arc) { + let connection: &Connection = &self.inner.connection.lock().unwrap(); + connection.update_hook(Some( + move |action: Action, db_name: &str, table_name: &str, row_id: i64| { + let hook_data = UpdateHook { + action: action.into(), + db_name: db_name.to_string(), + table_name: table_name.to_string(), + row_id, + }; + + delegate.did_update(hook_data); + }, + )); + } + + pub fn stop_listening_for_updates(&self) { + let connection: &Connection = &self.inner.connection.lock().unwrap(); + connection.update_hook(None::); + } +} + +static MIGRATION_QUERIES: [&str; 2] = [ + include_str!("migrations/0001-create-posts-table.sql"), + include_str!("migrations/0002-create-users-table.sql"), +]; + +pub struct MigrationManager<'a> { + connection: &'a Connection, +} + +impl<'a> MigrationManager<'a> { + pub fn new(connection: &'a Connection) -> Result { + Ok(Self { connection }) + } + + pub fn has_migrations_table(&self) -> SqliteResult { + let mut statement: rusqlite::Statement<'_> = self.connection.prepare( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='_migrations'", + )?; + + let result = statement.query_row([], |row| row.get::<_, i32>(0))?; + + Ok(result > 0) + } + + pub fn perform_migrations(&mut self) -> SqliteResult { + if !self.has_migrations_table()? { + self.create_migrations_table()?; + } + + let next_migration_id = self.get_next_migration_index()?; + for (index, migration) in MIGRATION_QUERIES[next_migration_id..].iter().enumerate() { + for query in migration + .split(";") + .filter(|query| !query.trim().is_empty()) + { + self.connection.execute(query, ())?; + } + + self.insert_migration((index + 1) as u64)?; + } + + Ok(MIGRATION_QUERIES[next_migration_id..].len() as u64) + } + + pub fn create_migrations_table(&self) -> SqliteResult<()> { + self.connection.execute( + "CREATE TABLE _migrations (migration_id INTEGER PRIMARY KEY)", + (), + )?; + Ok(()) + } + + pub fn insert_migration(&mut self, migration_id: u64) -> SqliteResult<()> { + let mut insert_migration_query = self + .connection + .prepare("INSERT INTO _migrations (migration_id) VALUES (?)")?; + insert_migration_query.execute(params![migration_id])?; + Ok(()) + } + + /// Returns the index of the next migration to run in the `MIGRATION_QUERIES` array. + /// Note that this is *not* the same as the migration ID. + pub fn get_next_migration_index(&self) -> SqliteResult { + let mut statement = self + .connection + .prepare("SELECT MAX(migration_id) FROM _migrations")?; + let result = statement.query_row([], |row| row.get::<_, Option>(0))?; + Ok(result.unwrap_or(0)) + } +} + +#[uniffi::export(with_foreign)] +pub trait DatabaseDelegate: Send + Sync { + fn did_update(&self, update_hook: UpdateHook); +} + +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)] +pub struct User { + id: i64, + name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)] +pub struct UpdateHook { + action: HookAction, + db_name: String, + table_name: String, + row_id: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)] +pub enum HookAction { + Insert, + Update, + Delete, +} + +impl From for HookAction { + fn from(action: Action) -> Self { + match action { + Action::SQLITE_INSERT => HookAction::Insert, + Action::SQLITE_UPDATE => HookAction::Update, + Action::SQLITE_DELETE => HookAction::Delete, + _ => panic!("Invalid action: {:?}", action), + } + } +} + +struct DBManager { + connection: Mutex, +} + +impl DBManager { + pub fn new(path: &Option) -> Result { + let connection: Connection; + + if let Some(path) = path.clone() { + connection = Connection::open(path)?; + } else { + connection = Connection::open_in_memory()?; + } + + Ok(Self { connection: Mutex::new(connection) }) + } +} diff --git a/wp_api/src/lib.rs b/wp_api/src/lib.rs index 490a2af15..6ade5c71d 100644 --- a/wp_api/src/lib.rs +++ b/wp_api/src/lib.rs @@ -6,6 +6,7 @@ use users::*; use wp_localization::{MessageBundle, WpMessages, WpSupportsLocalization}; use wp_localization_macro::WpDeriveLocalizable; +pub mod cache; pub mod jetpack; pub mod wp_com; diff --git a/wp_api/uniffi.toml b/wp_api/uniffi.toml index 214bc4f86..71ca20d4a 100644 --- a/wp_api/uniffi.toml +++ b/wp_api/uniffi.toml @@ -12,7 +12,7 @@ from_custom = "{}.toInstant().epochSecond" [bindings.swift.custom_types.WpGmtDateTime] type_name = "Date" -imports = ["Foundation"] +imports = ["SQLite3"] into_custom = "Date(timeIntervalSince1970: TimeInterval({}))" from_custom = "Int64({}.timeIntervalSince1970)"