From a5b8953daf2b78cb2ad802dac309ec46baf0bb04 Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 22 Oct 2024 14:57:04 +0200 Subject: [PATCH 01/11] chore: initial upload --- PowerSyncSwift/.gitignore | 8 + PowerSyncSwift/Package.resolved | 23 ++ PowerSyncSwift/Package.swift | 36 +++ .../PowerSyncSwift/PowerSyncSwift.swift | 284 ++++++++++++++++++ .../PowerSyncSwiftTests.swift | 6 + 5 files changed, 357 insertions(+) create mode 100644 PowerSyncSwift/.gitignore create mode 100644 PowerSyncSwift/Package.resolved create mode 100644 PowerSyncSwift/Package.swift create mode 100644 PowerSyncSwift/Sources/PowerSyncSwift/PowerSyncSwift.swift create mode 100644 PowerSyncSwift/Tests/PowerSyncSwiftTests/PowerSyncSwiftTests.swift diff --git a/PowerSyncSwift/.gitignore b/PowerSyncSwift/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/PowerSyncSwift/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/PowerSyncSwift/Package.resolved b/PowerSyncSwift/Package.resolved new file mode 100644 index 0000000..b070bf8 --- /dev/null +++ b/PowerSyncSwift/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "powersync-kotlin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/powersync-ja/powersync-kotlin.git", + "state" : { + "revision" : "4186fa9a2004a4bc85a22c3f37bce4f3ebd4ff81", + "version" : "1.0.0-BETA5.0" + } + }, + { + "identity" : "powersync-sqlite-core-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", + "state" : { + "revision" : "6aaa0606d8053fe2e2f57015a8a275c0440ee643", + "version" : "0.3.4" + } + } + ], + "version" : 2 +} diff --git a/PowerSyncSwift/Package.swift b/PowerSyncSwift/Package.swift new file mode 100644 index 0000000..ffb714b --- /dev/null +++ b/PowerSyncSwift/Package.swift @@ -0,0 +1,36 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "PowerSyncSwift", + platforms: [ + .iOS(.v13), + .macOS(.v10_13) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "PowerSyncSwift", + targets: ["PowerSyncSwift"]), + ], + dependencies: [ + .package(url: "https://github.com/powersync-ja/powersync-kotlin.git", exact: "1.0.0-BETA5.0"), + .package(url: "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", "0.3.1"..<"0.4.0"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "PowerSyncSwift", + dependencies: [ + .product(name: "PowerSync", package: "powersync-kotlin"), + .product(name: "PowerSyncSQLiteCore", package: "powersync-sqlite-core-swift") + ]), + .testTarget( + name: "PowerSyncSwiftTests", + dependencies: ["PowerSyncSwift"] + ), + ] +) diff --git a/PowerSyncSwift/Sources/PowerSyncSwift/PowerSyncSwift.swift b/PowerSyncSwift/Sources/PowerSyncSwift/PowerSyncSwift.swift new file mode 100644 index 0000000..ac50bf4 --- /dev/null +++ b/PowerSyncSwift/Sources/PowerSyncSwift/PowerSyncSwift.swift @@ -0,0 +1,284 @@ +import Foundation +import PowerSync + +typealias SuspendHandle = () async throws -> Any? + +class PowerSync { + let factory = DatabaseDriverFactory() + var db: PowerSyncDatabase! + + func openDb() { + db = PowerSyncDatabase(factory: factory, schema: schema, dbFilename: "powersync-swift.sqlite") + } + + func watch (_ sql: String, parameters: [String: Any] = [:], mapper: @escaping (Cursor) throws -> T) async -> AnyPublisher<[T], Never> { + } + + func execute(_ sql: String, parameters: [String: Any] = [:]) async throws { + try await db.execute(sql: sql, parameters: parameters) + } + + func writeTransaction(_ queryHandle: @escaping () async throws -> Any) async throws -> Any? { + try await db.writeTransaction(callback: SuspendTaskWrapper(queryHandle)) + } + + func readTransaction(_ queryHandle: @escaping () async throws -> Any) async throws -> Any? { + try await db.readTransaction(callback: SuspendTaskWrapper(queryHandle)) + } + + + + func insertList(_ list: NewListContent) async throws { + try await self.db.execute( + sql: "INSERT INTO \(LISTS_TABLE) (id, created_at, name, owner_id) VALUES (uuid(), datetime(), ?, ?)", + parameters: [list.name, connector.currentUserID] + ) + } + + func deleteList(id: String) async throws { + try await db.writeTransaction(callback: SuspendTaskWrapper { + try await self.db.execute( + sql: "DELETE FROM \(LISTS_TABLE) WHERE id = ?", + parameters: [id] + ) + try await self.db.execute( + sql: "DELETE FROM \(TODOS_TABLE) WHERE list_id = ?", + parameters: [id] + ) + return + }) + } + + func watchTodos(_ listId: String, _ cb: @escaping (_ todos: [Todo]) -> Void ) async { + for await todos in self.db.watch( + sql: "SELECT * FROM \(TODOS_TABLE) WHERE list_id = ?", + parameters: [listId], + mapper: { cursor in + return Todo( + id: cursor.getString(index: 0)!, + listId: cursor.getString(index: 1)!, + photoId: cursor.getString(index: 2), + description: cursor.getString(index: 3)!, + isComplete: cursor.getBoolean(index: 4)! as! Bool, + createdAt: cursor.getString(index: 5), + completedAt: cursor.getString(index: 6), + createdBy: cursor.getString(index: 7), + completedBy: cursor.getString(index: 8) + ) + } + ) { + cb(todos as! [Todo]) + } + } + + func insertTodo(_ todo: NewTodo, _ listId: String) async throws { + try await self.db.execute( + sql: "INSERT INTO \(TODOS_TABLE) (id, created_at, created_by, description, list_id, completed) VALUES (uuid(), datetime(), ?, ?, ?, ?)", + parameters: [connector.currentUserID, todo.description, listId, todo.isComplete] + ) + } + + func updateTodo(_ todo: Todo) async throws { + // Do this to avoid needing to handle date time from Swift to Kotlin + if(todo.isComplete) { + try await self.db.execute( + sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = datetime(), completed_by = ? WHERE id = ?", + parameters: [todo.description, todo.isComplete, connector.currentUserID, todo.id] + ) + } else { + try await self.db.execute( + sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = NULL, completed_by = NULL WHERE id = ?", + parameters: [todo.description, todo.isComplete, todo.id] + ) + } + } + + func deleteTodo(id: String) async throws { + try await db.writeTransaction(callback: SuspendTaskWrapper { + try await self.db.execute( + sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?", + parameters: [id] + ) + return + }) + } +} + +class SuspendTaskWrapper: KotlinSuspendFunction1 { + let handle: () async throws -> Any + + init(_ handle: @escaping () async throws -> Any) { + self.handle = handle + } + + @MainActor + func invoke(p1: Any?, completionHandler: @escaping (Any?, Error?) -> Void) { + Task { + do { + let result = try await self.handle() + completionHandler(result, nil) + } catch { + completionHandler(nil, error) + } + } + } +} +import Foundation +import PowerSync + +typealias SuspendHandle = () async throws -> Any? + +@Observable +@MainActor +class PowerSync { + let factory = DatabaseDriverFactory() + let connector = SupabaseConnector() + let schema = AppSchema + var db: PowerSyncDatabase! + + func openDb() { + db = PowerSyncDatabase(factory: factory, schema: schema, dbFilename: "powersync-swift.sqlite") + } + + // openDb must be called before connect + func connect() async { + do { + try await db.connect(connector: connector, crudThrottleMs: 1000, retryDelayMs:5000, params: [:]) + } catch { + print("Unexpected error: \(error.localizedDescription)") // Catches any other error + } + } + + func version() async -> String { + do { + return try await db.getPowerSyncVersion() + } catch { + return error.localizedDescription + } + } + + func signOut() async throws -> Void { + try await db.disconnectAndClear(clearLocal: true) + try await connector.client.auth.signOut() + } + + func writeTransaction(_ queryHandle: @escaping () async throws -> Any) async throws -> Any? { + try await db.writeTransaction(callback: SuspendTaskWrapper(queryHandle)) + } + + func readTransaction(_ queryHandle: @escaping () async throws -> Any) async throws -> Any? { + try await db.readTransaction(callback: SuspendTaskWrapper(queryHandle)) + } + + func watchLists(_ cb: @escaping (_ lists: [ListContent]) -> Void ) async { + for await lists in self.db.watch( + sql: "SELECT * FROM \(LISTS_TABLE)", + parameters: [], + mapper: { cursor in + ListContent( + id: cursor.getString(index: 0)!, + name: cursor.getString(index: 1)!, + createdAt: cursor.getString(index: 2)!, + ownerId: cursor.getString(index: 3)! + ) + } + ) { + cb(lists as! [ListContent]) + } + } + + func insertList(_ list: NewListContent) async throws { + try await self.db.execute( + sql: "INSERT INTO \(LISTS_TABLE) (id, created_at, name, owner_id) VALUES (uuid(), datetime(), ?, ?)", + parameters: [list.name, connector.currentUserID] + ) + } + + func deleteList(id: String) async throws { + try await db.writeTransaction(callback: SuspendTaskWrapper { + try await self.db.execute( + sql: "DELETE FROM \(LISTS_TABLE) WHERE id = ?", + parameters: [id] + ) + try await self.db.execute( + sql: "DELETE FROM \(TODOS_TABLE) WHERE list_id = ?", + parameters: [id] + ) + return + }) + } + + func watchTodos(_ listId: String, _ cb: @escaping (_ todos: [Todo]) -> Void ) async { + for await todos in self.db.watch( + sql: "SELECT * FROM \(TODOS_TABLE) WHERE list_id = ?", + parameters: [listId], + mapper: { cursor in + return Todo( + id: cursor.getString(index: 0)!, + listId: cursor.getString(index: 1)!, + photoId: cursor.getString(index: 2), + description: cursor.getString(index: 3)!, + isComplete: cursor.getBoolean(index: 4)! as! Bool, + createdAt: cursor.getString(index: 5), + completedAt: cursor.getString(index: 6), + createdBy: cursor.getString(index: 7), + completedBy: cursor.getString(index: 8) + ) + } + ) { + cb(todos as! [Todo]) + } + } + + func insertTodo(_ todo: NewTodo, _ listId: String) async throws { + try await self.db.execute( + sql: "INSERT INTO \(TODOS_TABLE) (id, created_at, created_by, description, list_id, completed) VALUES (uuid(), datetime(), ?, ?, ?, ?)", + parameters: [connector.currentUserID, todo.description, listId, todo.isComplete] + ) + } + + func updateTodo(_ todo: Todo) async throws { + // Do this to avoid needing to handle date time from Swift to Kotlin + if(todo.isComplete) { + try await self.db.execute( + sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = datetime(), completed_by = ? WHERE id = ?", + parameters: [todo.description, todo.isComplete, connector.currentUserID, todo.id] + ) + } else { + try await self.db.execute( + sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = NULL, completed_by = NULL WHERE id = ?", + parameters: [todo.description, todo.isComplete, todo.id] + ) + } + } + + func deleteTodo(id: String) async throws { + try await db.writeTransaction(callback: SuspendTaskWrapper { + try await self.db.execute( + sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?", + parameters: [id] + ) + return + }) + } +} + +class SuspendTaskWrapper: KotlinSuspendFunction1 { + let handle: () async throws -> Any + + init(_ handle: @escaping () async throws -> Any) { + self.handle = handle + } + + @MainActor + func invoke(p1: Any?, completionHandler: @escaping (Any?, Error?) -> Void) { + Task { + do { + let result = try await self.handle() + completionHandler(result, nil) + } catch { + completionHandler(nil, error) + } + } + } +} diff --git a/PowerSyncSwift/Tests/PowerSyncSwiftTests/PowerSyncSwiftTests.swift b/PowerSyncSwift/Tests/PowerSyncSwiftTests/PowerSyncSwiftTests.swift new file mode 100644 index 0000000..05accdb --- /dev/null +++ b/PowerSyncSwift/Tests/PowerSyncSwiftTests/PowerSyncSwiftTests.swift @@ -0,0 +1,6 @@ +import Testing +@testable import PowerSyncSwift + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +} From f0d2f8312e20ccb5bd193af4cb6f486ea6ea3c0b Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 24 Oct 2024 14:29:14 +0200 Subject: [PATCH 02/11] feat: add demo and connect --- .gitignore | 9 + .../project.pbxproj | 878 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 5 + .../xcshareddata/swiftpm/Package.resolved | 141 +++ .../xcschemes/PowerSyncExample.xcscheme | 90 ++ Demo/PowerSyncExample/.ci/pre_build.sh | 3 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../Assets.xcassets/Contents.json | 6 + .../Components/AddListView.swift | 38 + .../Components/AddTodoListView.swift | 41 + .../PowerSyncExample/Components/ListRow.swift | 26 + .../Components/ListView.swift | 94 ++ .../Components/TodoListRow.swift | 36 + .../Components/TodoListView.swift | 107 +++ .../Components/WifiIcon.swift | 22 + Demo/PowerSyncExample/Constants.swift | 5 + Demo/PowerSyncExample/Debug.swift | 19 + Demo/PowerSyncExample/ErrorText.swift | 21 + Demo/PowerSyncExample/Navigation.swift | 17 + Demo/PowerSyncExample/PowerSync/Lists.swift | 22 + .../PowerSync/PowerSyncManager.swift | 130 +++ Demo/PowerSyncExample/PowerSync/Schema.swift | 47 + .../PowerSync/SupabaseConnector.swift | 89 ++ Demo/PowerSyncExample/PowerSync/Todos.swift | 38 + .../PowerSyncExampleApp.swift | 12 + .../Preview Assets.xcassets/Contents.json | 6 + Demo/PowerSyncExample/RootView.swift | 44 + .../PowerSyncExample/Screens/HomeScreen.swift | 39 + .../Screens/SignInScreen.swift | 80 ++ .../Screens/SignUpScreen.swift | 80 ++ .../Screens/TodosScreen.swift | 20 + Demo/PowerSyncExample/_Secrets.swift | 8 + Demo/README.md | 47 + .../Package.resolved => Package.resolved | 0 PowerSyncSwift/Package.swift => Package.swift | 7 +- PowerSyncSwift/.gitignore | 8 - .../PowerSyncSwift/PowerSyncSwift.swift | 284 ------ Sources/PowerSyncSwift/KotlinTypes.swift | 13 + .../PowerSyncSwift/PowerSyncDatabase.swift | 21 + .../PowerSyncDatabaseImpl.swift | 173 ++++ .../PowerSyncDatabaseProtocol.swift | 93 ++ Sources/PowerSyncSwift/PowerSyncSwift.swift | 106 +++ .../PowerSyncTransactionProtocol.swift | 29 + Sources/PowerSyncSwift/QueriesProtocol.swift | 44 + .../PowerSyncSwiftTests.swift | 0 48 files changed, 2742 insertions(+), 295 deletions(-) create mode 100644 Demo/PowerSyncExample.xcodeproj/project.pbxproj create mode 100644 Demo/PowerSyncExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Demo/PowerSyncExample.xcodeproj/xcshareddata/xcschemes/PowerSyncExample.xcscheme create mode 100644 Demo/PowerSyncExample/.ci/pre_build.sh create mode 100644 Demo/PowerSyncExample/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Demo/PowerSyncExample/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Demo/PowerSyncExample/Assets.xcassets/Contents.json create mode 100644 Demo/PowerSyncExample/Components/AddListView.swift create mode 100644 Demo/PowerSyncExample/Components/AddTodoListView.swift create mode 100644 Demo/PowerSyncExample/Components/ListRow.swift create mode 100644 Demo/PowerSyncExample/Components/ListView.swift create mode 100644 Demo/PowerSyncExample/Components/TodoListRow.swift create mode 100644 Demo/PowerSyncExample/Components/TodoListView.swift create mode 100644 Demo/PowerSyncExample/Components/WifiIcon.swift create mode 100644 Demo/PowerSyncExample/Constants.swift create mode 100644 Demo/PowerSyncExample/Debug.swift create mode 100644 Demo/PowerSyncExample/ErrorText.swift create mode 100644 Demo/PowerSyncExample/Navigation.swift create mode 100644 Demo/PowerSyncExample/PowerSync/Lists.swift create mode 100644 Demo/PowerSyncExample/PowerSync/PowerSyncManager.swift create mode 100644 Demo/PowerSyncExample/PowerSync/Schema.swift create mode 100644 Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift create mode 100644 Demo/PowerSyncExample/PowerSync/Todos.swift create mode 100644 Demo/PowerSyncExample/PowerSyncExampleApp.swift create mode 100644 Demo/PowerSyncExample/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Demo/PowerSyncExample/RootView.swift create mode 100644 Demo/PowerSyncExample/Screens/HomeScreen.swift create mode 100644 Demo/PowerSyncExample/Screens/SignInScreen.swift create mode 100644 Demo/PowerSyncExample/Screens/SignUpScreen.swift create mode 100644 Demo/PowerSyncExample/Screens/TodosScreen.swift create mode 100644 Demo/PowerSyncExample/_Secrets.swift create mode 100644 Demo/README.md rename PowerSyncSwift/Package.resolved => Package.resolved (100%) rename PowerSyncSwift/Package.swift => Package.swift (91%) delete mode 100644 PowerSyncSwift/.gitignore delete mode 100644 PowerSyncSwift/Sources/PowerSyncSwift/PowerSyncSwift.swift create mode 100644 Sources/PowerSyncSwift/KotlinTypes.swift create mode 100644 Sources/PowerSyncSwift/PowerSyncDatabase.swift create mode 100644 Sources/PowerSyncSwift/PowerSyncDatabaseImpl.swift create mode 100644 Sources/PowerSyncSwift/PowerSyncDatabaseProtocol.swift create mode 100644 Sources/PowerSyncSwift/PowerSyncSwift.swift create mode 100644 Sources/PowerSyncSwift/PowerSyncTransactionProtocol.swift create mode 100644 Sources/PowerSyncSwift/QueriesProtocol.swift rename {PowerSyncSwift/Tests => Tests}/PowerSyncSwiftTests/PowerSyncSwiftTests.swift (100%) diff --git a/.gitignore b/.gitignore index 52fe2f7..1a06cf8 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,12 @@ fastlane/report.xml fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output + +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Demo/PowerSyncExample.xcodeproj/project.pbxproj b/Demo/PowerSyncExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..6ebc829 --- /dev/null +++ b/Demo/PowerSyncExample.xcodeproj/project.pbxproj @@ -0,0 +1,878 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 60; + objects = { + +/* Begin PBXBuildFile section */ + 18F30B2A2CCA4CD900A58917 /* PowerSyncSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 18F30B292CCA4CD900A58917 /* PowerSyncSwift */; }; + 6A4AD3852B9EE763005CBFD4 /* SupabaseConnector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4AD3842B9EE763005CBFD4 /* SupabaseConnector.swift */; }; + 6A4AD3892B9EEB21005CBFD4 /* _Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4AD3882B9EEB21005CBFD4 /* _Secrets.swift */; }; + 6A4AD3902B9EF775005CBFD4 /* ErrorText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4AD38F2B9EF775005CBFD4 /* ErrorText.swift */; }; + 6A7315882B9854220004CB17 /* PowerSyncExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A7315872B9854220004CB17 /* PowerSyncExampleApp.swift */; }; + 6A73158C2B9854240004CB17 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A73158B2B9854240004CB17 /* Assets.xcassets */; }; + 6A73158F2B9854240004CB17 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A73158E2B9854240004CB17 /* Preview Assets.xcassets */; }; + 6A7315BB2B98BDD30004CB17 /* PowerSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A7315BA2B98BDD30004CB17 /* PowerSyncManager.swift */; }; + 6A9668FE2B9EE4FE00B05DCF /* Auth in Frameworks */ = {isa = PBXBuildFile; productRef = 6A9668FD2B9EE4FE00B05DCF /* Auth */; }; + 6A9669002B9EE4FE00B05DCF /* PostgREST in Frameworks */ = {isa = PBXBuildFile; productRef = 6A9668FF2B9EE4FE00B05DCF /* PostgREST */; }; + 6A9669022B9EE69500B05DCF /* Supabase in Frameworks */ = {isa = PBXBuildFile; productRef = 6A9669012B9EE69500B05DCF /* Supabase */; }; + 6A9669042B9EE6FA00B05DCF /* SignInScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A9669032B9EE6FA00B05DCF /* SignInScreen.swift */; }; + 6ABD78672B9F2B4800558A41 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ABD78662B9F2B4800558A41 /* RootView.swift */; }; + 6ABD786B2B9F2C1500558A41 /* TodoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ABD786A2B9F2C1500558A41 /* TodoListView.swift */; }; + 6ABD78782B9F2D2800558A41 /* Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ABD78772B9F2D2800558A41 /* Schema.swift */; }; + 6ABD787A2B9F2D8300558A41 /* TodoListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ABD78792B9F2D8300558A41 /* TodoListRow.swift */; }; + 6ABD787C2B9F2E6700558A41 /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ABD787B2B9F2E6700558A41 /* Debug.swift */; }; + 6ABD78802B9F2F1300558A41 /* AddListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ABD787F2B9F2F1300558A41 /* AddListView.swift */; }; + B65C4D6D2C60D38B00176007 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C4D6C2C60D38B00176007 /* HomeScreen.swift */; }; + B65C4D712C60D7D800176007 /* SignUpScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C4D702C60D7D800176007 /* SignUpScreen.swift */; }; + B65C4D732C60D7EB00176007 /* TodosScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C4D722C60D7EB00176007 /* TodosScreen.swift */; }; + B666585B2C620C3900159A81 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B666585A2C620C3900159A81 /* Constants.swift */; }; + B666585D2C620E9E00159A81 /* WifiIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = B666585C2C620E9E00159A81 /* WifiIcon.swift */; }; + B666585F2C62115300159A81 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B666585E2C62115300159A81 /* ListRow.swift */; }; + B66658612C62179E00159A81 /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B66658602C62179E00159A81 /* ListView.swift */; }; + B66658632C621CA700159A81 /* AddTodoListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B66658622C621CA700159A81 /* AddTodoListView.swift */; }; + B66658652C62314B00159A81 /* Lists.swift in Sources */ = {isa = PBXBuildFile; fileRef = B66658642C62314B00159A81 /* Lists.swift */; }; + B66658672C62315400159A81 /* Todos.swift in Sources */ = {isa = PBXBuildFile; fileRef = B66658662C62315400159A81 /* Todos.swift */; }; + B66658772C63B7BB00159A81 /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = B66658762C63B7BB00159A81 /* IdentifiedCollections */; }; + B666587A2C63B88700159A81 /* SwiftUINavigation in Frameworks */ = {isa = PBXBuildFile; productRef = B66658792C63B88700159A81 /* SwiftUINavigation */; }; + B666587C2C63B88700159A81 /* SwiftUINavigationCore in Frameworks */ = {isa = PBXBuildFile; productRef = B666587B2C63B88700159A81 /* SwiftUINavigationCore */; }; + B69F7D862C8EE27400565448 /* AnyCodable in Frameworks */ = {isa = PBXBuildFile; productRef = B69F7D852C8EE27400565448 /* AnyCodable */; }; + B6B3698A2C64F4B30033C307 /* Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B369892C64F4B30033C307 /* Navigation.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 6AC6A3082BA18313006CE8D9 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 18CC627A2CC7A8B5009F7CDE /* powersync-kotlin */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "powersync-kotlin"; path = "../powersync-kotlin"; sourceTree = SOURCE_ROOT; }; + 6A4AD3842B9EE763005CBFD4 /* SupabaseConnector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupabaseConnector.swift; sourceTree = ""; }; + 6A4AD3882B9EEB21005CBFD4 /* _Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _Secrets.swift; sourceTree = ""; }; + 6A4AD38F2B9EF775005CBFD4 /* ErrorText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorText.swift; sourceTree = ""; }; + 6A7315842B9854220004CB17 /* PowerSyncExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PowerSyncExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 6A7315872B9854220004CB17 /* PowerSyncExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerSyncExampleApp.swift; sourceTree = ""; }; + 6A73158B2B9854240004CB17 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 6A73158E2B9854240004CB17 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 6A7315BA2B98BDD30004CB17 /* PowerSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerSyncManager.swift; sourceTree = ""; }; + 6A9669032B9EE6FA00B05DCF /* SignInScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInScreen.swift; sourceTree = ""; }; + 6ABD78662B9F2B4800558A41 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; + 6ABD786A2B9F2C1500558A41 /* TodoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListView.swift; sourceTree = ""; }; + 6ABD78772B9F2D2800558A41 /* Schema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Schema.swift; sourceTree = ""; }; + 6ABD78792B9F2D8300558A41 /* TodoListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoListRow.swift; sourceTree = ""; }; + 6ABD787B2B9F2E6700558A41 /* Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debug.swift; sourceTree = ""; }; + 6ABD787F2B9F2F1300558A41 /* AddListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddListView.swift; sourceTree = ""; }; + B65C4D6C2C60D38B00176007 /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; + B65C4D702C60D7D800176007 /* SignUpScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpScreen.swift; sourceTree = ""; }; + B65C4D722C60D7EB00176007 /* TodosScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosScreen.swift; sourceTree = ""; }; + B666585A2C620C3900159A81 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + B666585C2C620E9E00159A81 /* WifiIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WifiIcon.swift; sourceTree = ""; }; + B666585E2C62115300159A81 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = ""; }; + B66658602C62179E00159A81 /* ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = ""; }; + B66658622C621CA700159A81 /* AddTodoListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTodoListView.swift; sourceTree = ""; }; + B66658642C62314B00159A81 /* Lists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lists.swift; sourceTree = ""; }; + B66658662C62315400159A81 /* Todos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Todos.swift; sourceTree = ""; }; + B6B369892C64F4B30033C307 /* Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = ""; }; + B6F4210E2BC42F450005D0D0 /* cstubs.bc */ = {isa = PBXFileReference; lastKnownFileType = file; path = cstubs.bc; sourceTree = ""; }; + B6F421102BC42F450005D0D0 /* manifest.properties */ = {isa = PBXFileReference; lastKnownFileType = text; path = manifest.properties; sourceTree = ""; }; + B6F421122BC42F450005D0D0 /* cstubs.bc */ = {isa = PBXFileReference; lastKnownFileType = file; path = cstubs.bc; sourceTree = ""; }; + B6F421142BC42F450005D0D0 /* manifest.properties */ = {isa = PBXFileReference; lastKnownFileType = text; path = manifest.properties; sourceTree = ""; }; + B6F421162BC42F450005D0D0 /* core-cinterop-powersync-sqlite-core.klib */ = {isa = PBXFileReference; lastKnownFileType = file; path = "core-cinterop-powersync-sqlite-core.klib"; sourceTree = ""; }; + B6F421172BC42F450005D0D0 /* core-cinterop-sqlite.klib */ = {isa = PBXFileReference; lastKnownFileType = file; path = "core-cinterop-sqlite.klib"; sourceTree = ""; }; + B6F421192BC42F450005D0D0 /* core.klib */ = {isa = PBXFileReference; lastKnownFileType = file; path = core.klib; sourceTree = ""; }; + B6F4211D2BC42F450005D0D0 /* cstubs.bc */ = {isa = PBXFileReference; lastKnownFileType = file; path = cstubs.bc; sourceTree = ""; }; + B6F4211F2BC42F450005D0D0 /* manifest.properties */ = {isa = PBXFileReference; lastKnownFileType = text; path = manifest.properties; sourceTree = ""; }; + B6F421212BC42F450005D0D0 /* cstubs.bc */ = {isa = PBXFileReference; lastKnownFileType = file; path = cstubs.bc; sourceTree = ""; }; + B6F421232BC42F450005D0D0 /* manifest.properties */ = {isa = PBXFileReference; lastKnownFileType = text; path = manifest.properties; sourceTree = ""; }; + B6F421252BC42F450005D0D0 /* core-cinterop-powersync-sqlite-core.klib */ = {isa = PBXFileReference; lastKnownFileType = file; path = "core-cinterop-powersync-sqlite-core.klib"; sourceTree = ""; }; + B6F421262BC42F450005D0D0 /* core-cinterop-sqlite.klib */ = {isa = PBXFileReference; lastKnownFileType = file; path = "core-cinterop-sqlite.klib"; sourceTree = ""; }; + B6F421282BC42F450005D0D0 /* core.klib */ = {isa = PBXFileReference; lastKnownFileType = file; path = core.klib; sourceTree = ""; }; + B6F4212C2BC42F450005D0D0 /* cstubs.bc */ = {isa = PBXFileReference; lastKnownFileType = file; path = cstubs.bc; sourceTree = ""; }; + B6F4212E2BC42F450005D0D0 /* manifest.properties */ = {isa = PBXFileReference; lastKnownFileType = text; path = manifest.properties; sourceTree = ""; }; + B6F421302BC42F450005D0D0 /* cstubs.bc */ = {isa = PBXFileReference; lastKnownFileType = file; path = cstubs.bc; sourceTree = ""; }; + B6F421322BC42F450005D0D0 /* manifest.properties */ = {isa = PBXFileReference; lastKnownFileType = text; path = manifest.properties; sourceTree = ""; }; + B6F421342BC42F450005D0D0 /* core-cinterop-powersync-sqlite-core.klib */ = {isa = PBXFileReference; lastKnownFileType = file; path = "core-cinterop-powersync-sqlite-core.klib"; sourceTree = ""; }; + B6F421352BC42F450005D0D0 /* core-cinterop-sqlite.klib */ = {isa = PBXFileReference; lastKnownFileType = file; path = "core-cinterop-sqlite.klib"; sourceTree = ""; }; + B6F421372BC42F450005D0D0 /* core.klib */ = {isa = PBXFileReference; lastKnownFileType = file; path = core.klib; sourceTree = ""; }; + B6F4213D2BC42F5B0005D0D0 /* sqlite3.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = sqlite3.c; path = "../powersync-kotlin/core/build/interop/sqlite/sqlite3.c"; sourceTree = ""; }; + B6F421402BC430B60005D0D0 /* sqlite3.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sqlite3.h; path = "../powersync-kotlin/core/build/interop/sqlite/sqlite3.h"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 6A7315812B9854220004CB17 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B66658772C63B7BB00159A81 /* IdentifiedCollections in Frameworks */, + B69F7D862C8EE27400565448 /* AnyCodable in Frameworks */, + B666587C2C63B88700159A81 /* SwiftUINavigationCore in Frameworks */, + 6A9669022B9EE69500B05DCF /* Supabase in Frameworks */, + 6A9669002B9EE4FE00B05DCF /* PostgREST in Frameworks */, + 18F30B2A2CCA4CD900A58917 /* PowerSyncSwift in Frameworks */, + 6A9668FE2B9EE4FE00B05DCF /* Auth in Frameworks */, + B666587A2C63B88700159A81 /* SwiftUINavigation in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6A73157B2B9854220004CB17 = { + isa = PBXGroup; + children = ( + 6A7315862B9854220004CB17 /* PowerSyncExample */, + 6A7315852B9854220004CB17 /* Products */, + 6A7315B52B9857AD0004CB17 /* Frameworks */, + AE7193DCE091DC4BD17BE54E /* Pods */, + 18CC627A2CC7A8B5009F7CDE /* powersync-kotlin */, + ); + sourceTree = ""; + }; + 6A7315852B9854220004CB17 /* Products */ = { + isa = PBXGroup; + children = ( + 6A7315842B9854220004CB17 /* PowerSyncExample.app */, + ); + name = Products; + sourceTree = ""; + }; + 6A7315862B9854220004CB17 /* PowerSyncExample */ = { + isa = PBXGroup; + children = ( + B65C4D6F2C60D58500176007 /* PowerSync */, + B65C4D6E2C60D52E00176007 /* Components */, + B65C4D6B2C60D36700176007 /* Screens */, + 6A4AD3882B9EEB21005CBFD4 /* _Secrets.swift */, + 6A73158B2B9854240004CB17 /* Assets.xcassets */, + 6ABD787B2B9F2E6700558A41 /* Debug.swift */, + B666585A2C620C3900159A81 /* Constants.swift */, + B6B369892C64F4B30033C307 /* Navigation.swift */, + 6A4AD38F2B9EF775005CBFD4 /* ErrorText.swift */, + 6A7315872B9854220004CB17 /* PowerSyncExampleApp.swift */, + 6A73158D2B9854240004CB17 /* Preview Content */, + 6ABD78662B9F2B4800558A41 /* RootView.swift */, + ); + path = PowerSyncExample; + sourceTree = ""; + }; + 6A73158D2B9854240004CB17 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 6A73158E2B9854240004CB17 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 6A7315B52B9857AD0004CB17 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B6F421402BC430B60005D0D0 /* sqlite3.h */, + B6F4213D2BC42F5B0005D0D0 /* sqlite3.c */, + B6F4213C2BC42F450005D0D0 /* classes */, + ); + name = Frameworks; + sourceTree = ""; + }; + AE7193DCE091DC4BD17BE54E /* Pods */ = { + isa = PBXGroup; + children = ( + ); + path = Pods; + sourceTree = ""; + }; + B65C4D6B2C60D36700176007 /* Screens */ = { + isa = PBXGroup; + children = ( + 6A9669032B9EE6FA00B05DCF /* SignInScreen.swift */, + B65C4D6C2C60D38B00176007 /* HomeScreen.swift */, + B65C4D702C60D7D800176007 /* SignUpScreen.swift */, + B65C4D722C60D7EB00176007 /* TodosScreen.swift */, + ); + path = Screens; + sourceTree = ""; + }; + B65C4D6E2C60D52E00176007 /* Components */ = { + isa = PBXGroup; + children = ( + 6ABD78792B9F2D8300558A41 /* TodoListRow.swift */, + 6ABD786A2B9F2C1500558A41 /* TodoListView.swift */, + B66658622C621CA700159A81 /* AddTodoListView.swift */, + B666585C2C620E9E00159A81 /* WifiIcon.swift */, + B666585E2C62115300159A81 /* ListRow.swift */, + B66658602C62179E00159A81 /* ListView.swift */, + 6ABD787F2B9F2F1300558A41 /* AddListView.swift */, + ); + path = Components; + sourceTree = ""; + }; + B65C4D6F2C60D58500176007 /* PowerSync */ = { + isa = PBXGroup; + children = ( + 6A7315BA2B98BDD30004CB17 /* PowerSyncManager.swift */, + 6A4AD3842B9EE763005CBFD4 /* SupabaseConnector.swift */, + 6ABD78772B9F2D2800558A41 /* Schema.swift */, + B66658642C62314B00159A81 /* Lists.swift */, + B66658662C62315400159A81 /* Todos.swift */, + ); + path = PowerSync; + sourceTree = ""; + }; + B6F4210F2BC42F450005D0D0 /* natives */ = { + isa = PBXGroup; + children = ( + B6F4210E2BC42F450005D0D0 /* cstubs.bc */, + ); + path = natives; + sourceTree = ""; + }; + B6F421112BC42F450005D0D0 /* core-cinterop-powersync-sqlite-core.klib-build */ = { + isa = PBXGroup; + children = ( + B6F4210F2BC42F450005D0D0 /* natives */, + B6F421102BC42F450005D0D0 /* manifest.properties */, + ); + path = "core-cinterop-powersync-sqlite-core.klib-build"; + sourceTree = ""; + }; + B6F421132BC42F450005D0D0 /* natives */ = { + isa = PBXGroup; + children = ( + B6F421122BC42F450005D0D0 /* cstubs.bc */, + ); + path = natives; + sourceTree = ""; + }; + B6F421152BC42F450005D0D0 /* core-cinterop-sqlite.klib-build */ = { + isa = PBXGroup; + children = ( + B6F421132BC42F450005D0D0 /* natives */, + B6F421142BC42F450005D0D0 /* manifest.properties */, + ); + path = "core-cinterop-sqlite.klib-build"; + sourceTree = ""; + }; + B6F421182BC42F450005D0D0 /* cinterop */ = { + isa = PBXGroup; + children = ( + B6F421112BC42F450005D0D0 /* core-cinterop-powersync-sqlite-core.klib-build */, + B6F421152BC42F450005D0D0 /* core-cinterop-sqlite.klib-build */, + B6F421162BC42F450005D0D0 /* core-cinterop-powersync-sqlite-core.klib */, + B6F421172BC42F450005D0D0 /* core-cinterop-sqlite.klib */, + ); + path = cinterop; + sourceTree = ""; + }; + B6F4211A2BC42F450005D0D0 /* klib */ = { + isa = PBXGroup; + children = ( + B6F421192BC42F450005D0D0 /* core.klib */, + ); + path = klib; + sourceTree = ""; + }; + B6F4211B2BC42F450005D0D0 /* main */ = { + isa = PBXGroup; + children = ( + B6F421182BC42F450005D0D0 /* cinterop */, + B6F4211A2BC42F450005D0D0 /* klib */, + ); + path = main; + sourceTree = ""; + }; + B6F4211C2BC42F450005D0D0 /* iosArm64 */ = { + isa = PBXGroup; + children = ( + B6F4211B2BC42F450005D0D0 /* main */, + ); + path = iosArm64; + sourceTree = ""; + }; + B6F4211E2BC42F450005D0D0 /* natives */ = { + isa = PBXGroup; + children = ( + B6F4211D2BC42F450005D0D0 /* cstubs.bc */, + ); + path = natives; + sourceTree = ""; + }; + B6F421202BC42F450005D0D0 /* core-cinterop-powersync-sqlite-core.klib-build */ = { + isa = PBXGroup; + children = ( + B6F4211E2BC42F450005D0D0 /* natives */, + B6F4211F2BC42F450005D0D0 /* manifest.properties */, + ); + path = "core-cinterop-powersync-sqlite-core.klib-build"; + sourceTree = ""; + }; + B6F421222BC42F450005D0D0 /* natives */ = { + isa = PBXGroup; + children = ( + B6F421212BC42F450005D0D0 /* cstubs.bc */, + ); + path = natives; + sourceTree = ""; + }; + B6F421242BC42F450005D0D0 /* core-cinterop-sqlite.klib-build */ = { + isa = PBXGroup; + children = ( + B6F421222BC42F450005D0D0 /* natives */, + B6F421232BC42F450005D0D0 /* manifest.properties */, + ); + path = "core-cinterop-sqlite.klib-build"; + sourceTree = ""; + }; + B6F421272BC42F450005D0D0 /* cinterop */ = { + isa = PBXGroup; + children = ( + B6F421202BC42F450005D0D0 /* core-cinterop-powersync-sqlite-core.klib-build */, + B6F421242BC42F450005D0D0 /* core-cinterop-sqlite.klib-build */, + B6F421252BC42F450005D0D0 /* core-cinterop-powersync-sqlite-core.klib */, + B6F421262BC42F450005D0D0 /* core-cinterop-sqlite.klib */, + ); + path = cinterop; + sourceTree = ""; + }; + B6F421292BC42F450005D0D0 /* klib */ = { + isa = PBXGroup; + children = ( + B6F421282BC42F450005D0D0 /* core.klib */, + ); + path = klib; + sourceTree = ""; + }; + B6F4212A2BC42F450005D0D0 /* main */ = { + isa = PBXGroup; + children = ( + B6F421272BC42F450005D0D0 /* cinterop */, + B6F421292BC42F450005D0D0 /* klib */, + ); + path = main; + sourceTree = ""; + }; + B6F4212B2BC42F450005D0D0 /* iosSimulatorArm64 */ = { + isa = PBXGroup; + children = ( + B6F4212A2BC42F450005D0D0 /* main */, + ); + path = iosSimulatorArm64; + sourceTree = ""; + }; + B6F4212D2BC42F450005D0D0 /* natives */ = { + isa = PBXGroup; + children = ( + B6F4212C2BC42F450005D0D0 /* cstubs.bc */, + ); + path = natives; + sourceTree = ""; + }; + B6F4212F2BC42F450005D0D0 /* core-cinterop-powersync-sqlite-core.klib-build */ = { + isa = PBXGroup; + children = ( + B6F4212D2BC42F450005D0D0 /* natives */, + B6F4212E2BC42F450005D0D0 /* manifest.properties */, + ); + path = "core-cinterop-powersync-sqlite-core.klib-build"; + sourceTree = ""; + }; + B6F421312BC42F450005D0D0 /* natives */ = { + isa = PBXGroup; + children = ( + B6F421302BC42F450005D0D0 /* cstubs.bc */, + ); + path = natives; + sourceTree = ""; + }; + B6F421332BC42F450005D0D0 /* core-cinterop-sqlite.klib-build */ = { + isa = PBXGroup; + children = ( + B6F421312BC42F450005D0D0 /* natives */, + B6F421322BC42F450005D0D0 /* manifest.properties */, + ); + path = "core-cinterop-sqlite.klib-build"; + sourceTree = ""; + }; + B6F421362BC42F450005D0D0 /* cinterop */ = { + isa = PBXGroup; + children = ( + B6F4212F2BC42F450005D0D0 /* core-cinterop-powersync-sqlite-core.klib-build */, + B6F421332BC42F450005D0D0 /* core-cinterop-sqlite.klib-build */, + B6F421342BC42F450005D0D0 /* core-cinterop-powersync-sqlite-core.klib */, + B6F421352BC42F450005D0D0 /* core-cinterop-sqlite.klib */, + ); + path = cinterop; + sourceTree = ""; + }; + B6F421382BC42F450005D0D0 /* klib */ = { + isa = PBXGroup; + children = ( + B6F421372BC42F450005D0D0 /* core.klib */, + ); + path = klib; + sourceTree = ""; + }; + B6F421392BC42F450005D0D0 /* main */ = { + isa = PBXGroup; + children = ( + B6F421362BC42F450005D0D0 /* cinterop */, + B6F421382BC42F450005D0D0 /* klib */, + ); + path = main; + sourceTree = ""; + }; + B6F4213A2BC42F450005D0D0 /* iosX64 */ = { + isa = PBXGroup; + children = ( + B6F421392BC42F450005D0D0 /* main */, + ); + path = iosX64; + sourceTree = ""; + }; + B6F4213B2BC42F450005D0D0 /* kotlin */ = { + isa = PBXGroup; + children = ( + B6F4211C2BC42F450005D0D0 /* iosArm64 */, + B6F4212B2BC42F450005D0D0 /* iosSimulatorArm64 */, + B6F4213A2BC42F450005D0D0 /* iosX64 */, + ); + path = kotlin; + sourceTree = ""; + }; + B6F4213C2BC42F450005D0D0 /* classes */ = { + isa = PBXGroup; + children = ( + B6F4213B2BC42F450005D0D0 /* kotlin */, + ); + name = classes; + path = "../powersync-kotlin/core/build/classes"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 6A7315832B9854220004CB17 /* PowerSyncExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6A7315922B9854240004CB17 /* Build configuration list for PBXNativeTarget "PowerSyncExample" */; + buildPhases = ( + 6A7315802B9854220004CB17 /* Sources */, + 6A7315812B9854220004CB17 /* Frameworks */, + 6A7315822B9854220004CB17 /* Resources */, + 6AC6A3082BA18313006CE8D9 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PowerSyncExample; + packageProductDependencies = ( + 6A9668FD2B9EE4FE00B05DCF /* Auth */, + 6A9668FF2B9EE4FE00B05DCF /* PostgREST */, + 6A9669012B9EE69500B05DCF /* Supabase */, + B66658762C63B7BB00159A81 /* IdentifiedCollections */, + B66658792C63B88700159A81 /* SwiftUINavigation */, + B666587B2C63B88700159A81 /* SwiftUINavigationCore */, + B69F7D852C8EE27400565448 /* AnyCodable */, + 18F30B292CCA4CD900A58917 /* PowerSyncSwift */, + ); + productName = PowerSyncExample; + productReference = 6A7315842B9854220004CB17 /* PowerSyncExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 6A73157C2B9854220004CB17 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1520; + LastUpgradeCheck = 1520; + TargetAttributes = { + 6A7315832B9854220004CB17 = { + CreatedOnToolsVersion = 15.2; + LastSwiftMigration = 1540; + }; + }; + }; + buildConfigurationList = 6A73157F2B9854220004CB17 /* Build configuration list for PBXProject "PowerSyncExample" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 6A73157B2B9854220004CB17; + packageReferences = ( + 6A9668FC2B9EE4FE00B05DCF /* XCRemoteSwiftPackageReference "supabase-swift" */, + B66658752C63B7BB00159A81 /* XCRemoteSwiftPackageReference "swift-identified-collections" */, + B66658782C63B88700159A81 /* XCRemoteSwiftPackageReference "swiftui-navigation" */, + B69F7D842C8EE27300565448 /* XCRemoteSwiftPackageReference "AnyCodable" */, + 18F30B282CCA4B3B00A58917 /* XCLocalSwiftPackageReference "../../powersync-swift" */, + ); + productRefGroup = 6A7315852B9854220004CB17 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 6A7315832B9854220004CB17 /* PowerSyncExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 6A7315822B9854220004CB17 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6A73158F2B9854240004CB17 /* Preview Assets.xcassets in Resources */, + 6A73158C2B9854240004CB17 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6A7315802B9854220004CB17 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6ABD787C2B9F2E6700558A41 /* Debug.swift in Sources */, + B666585B2C620C3900159A81 /* Constants.swift in Sources */, + 6ABD78802B9F2F1300558A41 /* AddListView.swift in Sources */, + 6A4AD3892B9EEB21005CBFD4 /* _Secrets.swift in Sources */, + B65C4D712C60D7D800176007 /* SignUpScreen.swift in Sources */, + B6B3698A2C64F4B30033C307 /* Navigation.swift in Sources */, + 6ABD786B2B9F2C1500558A41 /* TodoListView.swift in Sources */, + 6A4AD3852B9EE763005CBFD4 /* SupabaseConnector.swift in Sources */, + B65C4D732C60D7EB00176007 /* TodosScreen.swift in Sources */, + 6ABD787A2B9F2D8300558A41 /* TodoListRow.swift in Sources */, + B66658652C62314B00159A81 /* Lists.swift in Sources */, + 6A4AD3902B9EF775005CBFD4 /* ErrorText.swift in Sources */, + B66658672C62315400159A81 /* Todos.swift in Sources */, + 6ABD78672B9F2B4800558A41 /* RootView.swift in Sources */, + B66658612C62179E00159A81 /* ListView.swift in Sources */, + 6ABD78782B9F2D2800558A41 /* Schema.swift in Sources */, + B65C4D6D2C60D38B00176007 /* HomeScreen.swift in Sources */, + 6A7315882B9854220004CB17 /* PowerSyncExampleApp.swift in Sources */, + B666585F2C62115300159A81 /* ListRow.swift in Sources */, + B66658632C621CA700159A81 /* AddTodoListView.swift in Sources */, + B666585D2C620E9E00159A81 /* WifiIcon.swift in Sources */, + 6A9669042B9EE6FA00B05DCF /* SignInScreen.swift in Sources */, + 6A7315BB2B98BDD30004CB17 /* PowerSyncManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 6A7315902B9854240004CB17 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = "-lsqlite3"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 6A7315912B9854240004CB17 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + OTHER_LDFLAGS = "-lsqlite3"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 6A7315932B9854240004CB17 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLY_RULES_IN_COPY_HEADERS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"PowerSyncExample/Preview Content\""; + DEVELOPMENT_TEAM = 6WA62GTJNA; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = com.powersync.PowerSyncExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "PowerSyncExample/PowerSyncExample-Bridging-Header.h"; + "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6A7315942B9854240004CB17 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLY_RULES_IN_COPY_HEADERS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"PowerSyncExample/Preview Content\""; + DEVELOPMENT_TEAM = 6WA62GTJNA; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = com.powersync.PowerSyncExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "PowerSyncExample/PowerSyncExample-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6A73157F2B9854220004CB17 /* Build configuration list for PBXProject "PowerSyncExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6A7315902B9854240004CB17 /* Debug */, + 6A7315912B9854240004CB17 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6A7315922B9854240004CB17 /* Build configuration list for PBXNativeTarget "PowerSyncExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6A7315932B9854240004CB17 /* Debug */, + 6A7315942B9854240004CB17 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 18F30B282CCA4B3B00A58917 /* XCLocalSwiftPackageReference "../../powersync-swift" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../powersync-swift"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 6A9668FC2B9EE4FE00B05DCF /* XCRemoteSwiftPackageReference "supabase-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/supabase-community/supabase-swift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.4.0; + }; + }; + B66658752C63B7BB00159A81 /* XCRemoteSwiftPackageReference "swift-identified-collections" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-identified-collections"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.0; + }; + }; + B66658782C63B88700159A81 /* XCRemoteSwiftPackageReference "swiftui-navigation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swiftui-navigation"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.5.4; + }; + }; + B69F7D842C8EE27300565448 /* XCRemoteSwiftPackageReference "AnyCodable" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Flight-School/AnyCodable"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.6.7; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 18F30B292CCA4CD900A58917 /* PowerSyncSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 18F30B282CCA4B3B00A58917 /* XCLocalSwiftPackageReference "../../powersync-swift" */; + productName = PowerSyncSwift; + }; + 6A9668FD2B9EE4FE00B05DCF /* Auth */ = { + isa = XCSwiftPackageProductDependency; + package = 6A9668FC2B9EE4FE00B05DCF /* XCRemoteSwiftPackageReference "supabase-swift" */; + productName = Auth; + }; + 6A9668FF2B9EE4FE00B05DCF /* PostgREST */ = { + isa = XCSwiftPackageProductDependency; + package = 6A9668FC2B9EE4FE00B05DCF /* XCRemoteSwiftPackageReference "supabase-swift" */; + productName = PostgREST; + }; + 6A9669012B9EE69500B05DCF /* Supabase */ = { + isa = XCSwiftPackageProductDependency; + package = 6A9668FC2B9EE4FE00B05DCF /* XCRemoteSwiftPackageReference "supabase-swift" */; + productName = Supabase; + }; + B66658762C63B7BB00159A81 /* IdentifiedCollections */ = { + isa = XCSwiftPackageProductDependency; + package = B66658752C63B7BB00159A81 /* XCRemoteSwiftPackageReference "swift-identified-collections" */; + productName = IdentifiedCollections; + }; + B66658792C63B88700159A81 /* SwiftUINavigation */ = { + isa = XCSwiftPackageProductDependency; + package = B66658782C63B88700159A81 /* XCRemoteSwiftPackageReference "swiftui-navigation" */; + productName = SwiftUINavigation; + }; + B666587B2C63B88700159A81 /* SwiftUINavigationCore */ = { + isa = XCSwiftPackageProductDependency; + package = B66658782C63B88700159A81 /* XCRemoteSwiftPackageReference "swiftui-navigation" */; + productName = SwiftUINavigationCore; + }; + B69F7D852C8EE27400565448 /* AnyCodable */ = { + isa = XCSwiftPackageProductDependency; + package = B69F7D842C8EE27300565448 /* XCRemoteSwiftPackageReference "AnyCodable" */; + productName = AnyCodable; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 6A73157C2B9854220004CB17 /* Project object */; +} diff --git a/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..2fdd706 --- /dev/null +++ b/Demo/PowerSyncExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,141 @@ +{ + "originHash" : "5d7fb7f47b01e814cbc6b4a65dfe62c7af5a96a435a0288b747750c370fcd28a", + "pins" : [ + { + "identity" : "anycodable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flight-School/AnyCodable", + "state" : { + "revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05", + "version" : "0.6.7" + } + }, + { + "identity" : "powersync-kotlin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/powersync-ja/powersync-kotlin.git", + "state" : { + "revision" : "4186fa9a2004a4bc85a22c3f37bce4f3ebd4ff81", + "version" : "1.0.0-BETA5.0" + } + }, + { + "identity" : "powersync-sqlite-core-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/powersync-ja/powersync-sqlite-core-swift.git", + "state" : { + "revision" : "6aaa0606d8053fe2e2f57015a8a275c0440ee643", + "version" : "0.3.4" + } + }, + { + "identity" : "supabase-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/supabase-community/supabase-swift.git", + "state" : { + "revision" : "8f5b94f6a7a35305ccc1726f2f8f9d415ee2ec50", + "version" : "2.20.4" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "bc92c4b27f9a84bfb498cdbfdf35d5a357e9161f", + "version" : "1.5.6" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "6054df64b55186f08b6d0fd87152081b8ad8d613", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "21f7878f2b39d46fd8ba2b06459ccb431cdf876c", + "version" : "3.8.1" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", + "version" : "1.3.3" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "2f5ab6e091dd032b63dacbda052405756010dc3b", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, + { + "identity" : "swiftui-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swiftui-navigation", + "state" : { + "revision" : "e628806aeaa9efe25c1abcd97931a7c498fab281", + "version" : "1.5.5" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "770f990d3e4eececb57ac04a6076e22f8c97daeb", + "version" : "1.4.2" + } + } + ], + "version" : 3 +} diff --git a/Demo/PowerSyncExample.xcodeproj/xcshareddata/xcschemes/PowerSyncExample.xcscheme b/Demo/PowerSyncExample.xcodeproj/xcshareddata/xcschemes/PowerSyncExample.xcscheme new file mode 100644 index 0000000..50e13c1 --- /dev/null +++ b/Demo/PowerSyncExample.xcodeproj/xcshareddata/xcschemes/PowerSyncExample.xcscheme @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/PowerSyncExample/.ci/pre_build.sh b/Demo/PowerSyncExample/.ci/pre_build.sh new file mode 100644 index 0000000..309bae6 --- /dev/null +++ b/Demo/PowerSyncExample/.ci/pre_build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cp _Secrets.swift Secrets.swift diff --git a/Demo/PowerSyncExample/Assets.xcassets/AccentColor.colorset/Contents.json b/Demo/PowerSyncExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Demo/PowerSyncExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/PowerSyncExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/Demo/PowerSyncExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/Demo/PowerSyncExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/PowerSyncExample/Assets.xcassets/Contents.json b/Demo/PowerSyncExample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Demo/PowerSyncExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/PowerSyncExample/Components/AddListView.swift b/Demo/PowerSyncExample/Components/AddListView.swift new file mode 100644 index 0000000..0b591bc --- /dev/null +++ b/Demo/PowerSyncExample/Components/AddListView.swift @@ -0,0 +1,38 @@ +import SwiftUI + +struct AddListView: View { + @Environment(PowerSyncManager.self) private var powerSync + + @Binding var newList: NewListContent + let completion: (Result) -> Void + + var body: some View { + Section { + TextField("Name", text: $newList.name) + Button("Save") { + Task.detached { + do { + try await powerSync.insertList(newList) + completion(.success(true)) + } catch { + completion(.failure(error)) + throw error + } + } + } + } + } +} + +#Preview { + AddListView( + newList: .constant( + .init( + name: "", + ownerId: "", + createdAt: "" + ) + ) + ) { _ in + }.environment(PowerSyncManager()) +} diff --git a/Demo/PowerSyncExample/Components/AddTodoListView.swift b/Demo/PowerSyncExample/Components/AddTodoListView.swift new file mode 100644 index 0000000..321dd70 --- /dev/null +++ b/Demo/PowerSyncExample/Components/AddTodoListView.swift @@ -0,0 +1,41 @@ +import Foundation +import SwiftUI + +struct AddTodoListView: View { + @Environment(PowerSyncManager.self) private var powerSync + + @Binding var newTodo: NewTodo + let listId: String + let completion: (Result) -> Void + + var body: some View { + Section { + TextField("Description", text: $newTodo.description) + Button("Save") { + Task.detached { + do { + try await powerSync.insertTodo(newTodo, listId) + completion(.success(true)) + } catch { + completion(.failure(error)) + throw error + } + } + } + } + } +} + +#Preview { + AddTodoListView( + newTodo: .constant( + .init( + listId: UUID().uuidString.lowercased(), + isComplete: false, + description: "" + ) + ), + listId: UUID().uuidString.lowercased() + ){ _ in + }.environment(PowerSyncManager()) +} diff --git a/Demo/PowerSyncExample/Components/ListRow.swift b/Demo/PowerSyncExample/Components/ListRow.swift new file mode 100644 index 0000000..e25504a --- /dev/null +++ b/Demo/PowerSyncExample/Components/ListRow.swift @@ -0,0 +1,26 @@ +import SwiftUI +import Foundation + +struct ListRow: View { + let list: ListContent + + var body: some View { + HStack { + Text(list.name) + Spacer() + .buttonStyle(.plain) + } + } +} + + +#Preview { + ListRow( + list: .init( + id: UUID().uuidString.lowercased(), + name: "name", + createdAt: "", + ownerId: UUID().uuidString.lowercased() + ) + ) +} diff --git a/Demo/PowerSyncExample/Components/ListView.swift b/Demo/PowerSyncExample/Components/ListView.swift new file mode 100644 index 0000000..e2fcc50 --- /dev/null +++ b/Demo/PowerSyncExample/Components/ListView.swift @@ -0,0 +1,94 @@ +import SwiftUI +import IdentifiedCollections +import SwiftUINavigation + +struct ListView: View { + @Environment(PowerSyncManager.self) private var powerSync + + @State private var lists: IdentifiedArrayOf = [] + @State private var error: Error? + @State private var newList: NewListContent? + @State private var editing: Bool = false + + var body: some View { + List { + if let error { + ErrorText(error) + } + + IfLet($newList) { $newList in + AddListView(newList: $newList) { result in + withAnimation { + self.newList = nil + } + } + } + + ForEach(lists) { list in + NavigationLink(destination: TodosScreen( + listId: list.id + )) { + ListRow(list: list) + } + } + .onDelete { indexSet in + Task { + await handleDelete(at: indexSet) + } + } + } + .animation(.default, value: lists) + .navigationTitle("Lists") + .toolbar { + ToolbarItem(placement: .primaryAction) { + if (newList == nil) { + Button { + withAnimation { + newList = .init( + name: "", + ownerId: "", + createdAt: "" + ) + } + } label: { + Label("Add", systemImage: "plus") + } + } else { + Button("Cancel", role: .cancel) { + withAnimation { + newList = nil + } + } + } + } + } + .task { + Task { + await powerSync.watchLists { ls in + withAnimation { + self.lists = IdentifiedArrayOf(uniqueElements: ls) + } + } + } + } + } + + func handleDelete(at offset: IndexSet) async { + do { + error = nil + let listsToDelete = offset.map { lists[$0] } + + try await powerSync.deleteList(id: listsToDelete[0].id) + + } catch { + self.error = error + } + } +} + +#Preview { + NavigationStack { + ListView() + .environment(PowerSyncManager()) + } +} diff --git a/Demo/PowerSyncExample/Components/TodoListRow.swift b/Demo/PowerSyncExample/Components/TodoListRow.swift new file mode 100644 index 0000000..2bbf184 --- /dev/null +++ b/Demo/PowerSyncExample/Components/TodoListRow.swift @@ -0,0 +1,36 @@ +import SwiftUI + +struct TodoListRow: View { + let todo: Todo + let completeTapped: () -> Void + + var body: some View { + HStack { + Text(todo.description) + Spacer() + Button { + completeTapped() + } label: { + Image(systemName: todo.isComplete ? "checkmark.circle.fill" : "circle") + } + .buttonStyle(.plain) + } + } +} + + +#Preview { + TodoListRow( + todo: .init( + id: UUID().uuidString.lowercased(), + listId: UUID().uuidString.lowercased(), + photoId: nil, + description: "description", + isComplete: false, + createdAt: "", + completedAt: nil, + createdBy: UUID().uuidString.lowercased(), + completedBy: nil + ) + ) {} +} diff --git a/Demo/PowerSyncExample/Components/TodoListView.swift b/Demo/PowerSyncExample/Components/TodoListView.swift new file mode 100644 index 0000000..dc9d7ec --- /dev/null +++ b/Demo/PowerSyncExample/Components/TodoListView.swift @@ -0,0 +1,107 @@ +import SwiftUI +import IdentifiedCollections +import SwiftUINavigation + +struct TodoListView: View { + @Environment(PowerSyncManager.self) private var powerSync + let listId: String + + @State private var todos: IdentifiedArrayOf = [] + @State private var error: Error? + @State private var newTodo: NewTodo? + @State private var editing: Bool = false + + var body: some View { + List { + if let error { + ErrorText(error) + } + + IfLet($newTodo) { $newTodo in + AddTodoListView(newTodo: $newTodo, listId: listId) { result in + withAnimation { + self.newTodo = nil + } + } + } + + ForEach(todos) { todo in + TodoListRow(todo: todo) { + Task { + await toggleCompletion(of: todo) + } + } + } + .onDelete { indexSet in + Task { + await delete(at: indexSet) + } + } + } + .animation(.default, value: todos) + .navigationTitle("Todos") + .toolbar { + ToolbarItem(placement: .primaryAction) { + if (newTodo == nil) { + Button { + withAnimation { + newTodo = .init( + listId: listId, + isComplete: false, + description: "" + ) + } + } label: { + Label("Add", systemImage: "plus") + } + } else { + Button("Cancel", role: .cancel) { + withAnimation { + newTodo = nil + } + } + } + } + } + .task { + Task { + await powerSync.watchTodos(listId) { tds in + withAnimation { + self.todos = IdentifiedArrayOf(uniqueElements: tds) + } + } + } + } + } + + func toggleCompletion(of todo: Todo) async { + var updatedTodo = todo + updatedTodo.isComplete.toggle() + do { + error = nil + try await powerSync.updateTodo(updatedTodo) + } catch { + self.error = error + } + } + + func delete(at offset: IndexSet) async { + do { + error = nil + let todosToDelete = offset.map { todos[$0] } + + try await powerSync.deleteTodo(id: todosToDelete[0].id) + + } catch { + self.error = error + } + } +} + +#Preview { + NavigationStack { + TodoListView( + listId: UUID().uuidString.lowercased() + ).environment(PowerSyncManager()) + } +} diff --git a/Demo/PowerSyncExample/Components/WifiIcon.swift b/Demo/PowerSyncExample/Components/WifiIcon.swift new file mode 100644 index 0000000..0911f48 --- /dev/null +++ b/Demo/PowerSyncExample/Components/WifiIcon.swift @@ -0,0 +1,22 @@ +import Foundation +import SwiftUI + + +struct WifiIcon: View { + let isConnected: Bool + + var body: some View { + let iconName = isConnected ? "wifi" : "wifi.slash" + let description = isConnected ? "Online" : "Offline" + + Image(systemName: iconName) + .accessibility(label: Text(description)) + } +} + +#Preview { + VStack { + WifiIcon(isConnected: true) + WifiIcon(isConnected: false) + } +} diff --git a/Demo/PowerSyncExample/Constants.swift b/Demo/PowerSyncExample/Constants.swift new file mode 100644 index 0000000..89a08bb --- /dev/null +++ b/Demo/PowerSyncExample/Constants.swift @@ -0,0 +1,5 @@ +import Foundation + +enum Constants { + static let redirectToURL = URL(string: "com.powersync.PowerSyncExample://")! +} diff --git a/Demo/PowerSyncExample/Debug.swift b/Demo/PowerSyncExample/Debug.swift new file mode 100644 index 0000000..1df657f --- /dev/null +++ b/Demo/PowerSyncExample/Debug.swift @@ -0,0 +1,19 @@ +import Foundation + +func debug( + _ message: @autoclosure () -> String, + function: String = #function, + file: String = #file, + line: UInt = #line +) { + assert( + { + let fileHandle = FileHandle.standardError + + let logLine = "[\(function) \(file.split(separator: "/").last!):\(line)] \(message())\n" + fileHandle.write(Data(logLine.utf8)) + + return true + }() + ) +} diff --git a/Demo/PowerSyncExample/ErrorText.swift b/Demo/PowerSyncExample/ErrorText.swift new file mode 100644 index 0000000..a01c5c5 --- /dev/null +++ b/Demo/PowerSyncExample/ErrorText.swift @@ -0,0 +1,21 @@ +import SwiftUI + +struct ErrorText: View { + let error: Error + + init(_ error: Error) { + self.error = error + } + + var body: some View { + Text(error.localizedDescription) + .foregroundColor(.red) + .font(.footnote) + } +} + +struct ErrorText_Previews: PreviewProvider { + static var previews: some View { + ErrorText(NSError()) + } +} diff --git a/Demo/PowerSyncExample/Navigation.swift b/Demo/PowerSyncExample/Navigation.swift new file mode 100644 index 0000000..a8e0802 --- /dev/null +++ b/Demo/PowerSyncExample/Navigation.swift @@ -0,0 +1,17 @@ +import SwiftUI + +enum Route: Hashable { + case home + case signIn + case signUp +} + +@Observable +class AuthModel { + var isAuthenticated = false +} + +@Observable +class NavigationModel { + var path = NavigationPath() +} diff --git a/Demo/PowerSyncExample/PowerSync/Lists.swift b/Demo/PowerSyncExample/PowerSync/Lists.swift new file mode 100644 index 0000000..42c41fd --- /dev/null +++ b/Demo/PowerSyncExample/PowerSync/Lists.swift @@ -0,0 +1,22 @@ +import Foundation +import PowerSync + +struct ListContent: Identifiable, Hashable, Decodable { + let id: String + var name: String + var createdAt: String + var ownerId: String + + enum CodingKeys: String, CodingKey { + case id + case name + case createdAt = "created_at" + case ownerId = "owner_id" + } +} + +struct NewListContent: Encodable { + var name: String + var ownerId: String + var createdAt: String +} diff --git a/Demo/PowerSyncExample/PowerSync/PowerSyncManager.swift b/Demo/PowerSyncExample/PowerSync/PowerSyncManager.swift new file mode 100644 index 0000000..5ff8800 --- /dev/null +++ b/Demo/PowerSyncExample/PowerSync/PowerSyncManager.swift @@ -0,0 +1,130 @@ +import Foundation +import PowerSyncSwift + +@Observable +@MainActor +class PowerSyncManager { + let connector = SupabaseConnector() + let schema = AppSchema + var db: PowerSyncDatabaseProtocol! + + func openDb() { + db = PowerSyncDatabase(schema: schema, dbFilename: "powersync-swift.sqlite") + } + + // openDb must be called before connect + func connect() async { + do { + try await db.connect(connector: connector, crudThrottleMs: 1000, retryDelayMs:5000, params: [:]) + } catch { + print("Unexpected error: \(error.localizedDescription)") // Catches any other error + } + } + + func version() async -> String { + do { + return try await db.getPowerSyncVersion() + } catch { + return error.localizedDescription + } + } + + func signOut() async throws -> Void { + try await db.disconnectAndClear(clearLocal: true) + try await connector.client.auth.signOut() + } + + func watchLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { + for await lists in self.db.watch( + sql: "SELECT * FROM \(LISTS_TABLE)", + parameters: [], + mapper: { cursor in + ListContent( + id: cursor.getString(index: 0)!, + name: cursor.getString(index: 1)!, + createdAt: cursor.getString(index: 2)!, + ownerId: cursor.getString(index: 3)! + ) + } + ) { + callback(lists) + } + } + + func insertList(_ list: NewListContent) async throws { + _ = try await self.db.execute( + sql: "INSERT INTO \(LISTS_TABLE) (id, created_at, name, owner_id) VALUES (uuid(), datetime(), ?, ?)", + parameters: [list.name, connector.currentUserID] + ) + } + + func deleteList(id: String) async throws { + try await db.writeTransaction(callback: { transaction in + _ = try await transaction.execute( + sql: "DELETE FROM \(LISTS_TABLE) WHERE id = ?", + parameters: [id] + ) + _ = try await transaction.execute( + sql: "DELETE FROM \(TODOS_TABLE) WHERE list_id = ?", + parameters: [id] + ) + return + }) + } + + func watchTodos(_ listId: String, _ callback: @escaping (_ todos: [Todo]) -> Void ) async { + for await todos in self.db.watch( + sql: "SELECT * FROM \(TODOS_TABLE) WHERE list_id = ?", + parameters: [listId], + mapper: { cursor in + return Todo( + id: cursor.getString(index: 0)!, + listId: cursor.getString(index: 1)!, + photoId: cursor.getString(index: 2), + description: cursor.getString(index: 3)!, + isComplete: cursor.getBoolean(index: 4)! as! Bool, + createdAt: cursor.getString(index: 5), + completedAt: cursor.getString(index: 6), + createdBy: cursor.getString(index: 7), + completedBy: cursor.getString(index: 8) + ) + } + ) { + callback(todos) + } + } + + func insertTodo(_ todo: NewTodo, _ listId: String) async throws { + _ = try await self.db.execute( + sql: "INSERT INTO \(TODOS_TABLE) (id, created_at, created_by, description, list_id, completed) VALUES (uuid(), datetime(), ?, ?, ?, ?)", + parameters: [connector.currentUserID, todo.description, listId, todo.isComplete] + ) + } + + func updateTodo(_ todo: Todo) async throws { + // Do this to avoid needing to handle date time from Swift to Kotlin + if(todo.isComplete) { + _ = try await self.db.execute( + sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = datetime(), completed_by = ? WHERE id = ?", + parameters: [todo.description, todo.isComplete, connector.currentUserID, todo.id] + ) + } else { + _ = try await self.db.execute( + sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = NULL, completed_by = NULL WHERE id = ?", + parameters: [todo.description, todo.isComplete, todo.id] + ) + } + } + + func deleteTodo(id: String) async throws { + try await db.writeTransaction(callback: { transaction in + _ = try await transaction.execute( + sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?", + parameters: [id] + ) + return + }) + } +} + + diff --git a/Demo/PowerSyncExample/PowerSync/Schema.swift b/Demo/PowerSyncExample/PowerSync/Schema.swift new file mode 100644 index 0000000..78f3068 --- /dev/null +++ b/Demo/PowerSyncExample/PowerSync/Schema.swift @@ -0,0 +1,47 @@ +import Foundation +import PowerSync + +let LISTS_TABLE = "lists" +let TODOS_TABLE = "todos" + +let lists = Table( + name: LISTS_TABLE, + columns: [ + // ID column is automatically included + Column(name: "name", type: ColumnType.text), + Column(name: "created_at", type: ColumnType.text), + Column(name: "owner_id", type: ColumnType.text) + ], + indexes: [], + localOnly: false, + insertOnly: false, + viewNameOverride: LISTS_TABLE +) + +let todos = Table( + name: TODOS_TABLE, + // ID column is automatically included + columns: [ + Column(name: "list_id", type: ColumnType.text), + Column(name: "photo_id", type: ColumnType.text), + Column(name: "description", type: ColumnType.text), + // 0 or 1 to represent false or true + Column(name: "completed", type: ColumnType.integer), + Column(name: "created_at", type: ColumnType.text), + Column(name: "completed_at", type: ColumnType.text), + Column(name: "created_by", type: ColumnType.text), + Column(name: "completed_by", type: ColumnType.text) + + ], + indexes: [ + Index( + name: "list_id", + columns: [IndexedColumn(column: "list_id", ascending: true, columnDefinition: nil, type: nil)] + ) + ], + localOnly: false, + insertOnly: false, + viewNameOverride: TODOS_TABLE +) + +let AppSchema = Schema(tables: [lists, todos]) diff --git a/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift b/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift new file mode 100644 index 0000000..7549539 --- /dev/null +++ b/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift @@ -0,0 +1,89 @@ +import Auth +import SwiftUI +import Supabase +import PowerSyncSwift +import PowerSync +import AnyCodable + +@Observable +class SupabaseConnector: PowerSyncBackendConnector { + let powerSyncEndpoint: String = Secrets.powerSyncEndpoint + let client: SupabaseClient = SupabaseClient(supabaseURL: Secrets.supabaseURL, supabaseKey: Secrets.supabaseAnonKey) + var session: Session? + + @ObservationIgnored + private var observeAuthStateChangesTask: Task? + + override init() { + super.init() + + observeAuthStateChangesTask = Task { [weak self] in + guard let self = self else { return } + + for await (event, session) in self.client.auth.authStateChanges { + guard [.initialSession, .signedIn, .signedOut].contains(event) else { throw AuthError.sessionNotFound } + + self.session = session + } + } + } + + var currentUserID: String { + guard let id = session?.user.id else { + preconditionFailure("Required session.") + } + + return id.uuidString.lowercased() + } + + override func fetchCredentials() async throws -> PowerSyncCredentials? { + session = try await client.auth.session + + if (self.session == nil) { + throw AuthError.sessionNotFound + } + + let token = session!.accessToken + + // userId is for debugging purposes only + return PowerSyncCredentials(endpoint: self.powerSyncEndpoint, token: token, userId: currentUserID) + } + + override func uploadData(database: any PowerSyncDatabase) async throws { + + guard let transaction = try await database.getNextCrudTransaction() else { return } + + var lastEntry: CrudEntry? + do { + for entry in transaction.crud { + lastEntry = entry + let tableName = entry.table + + let table = client.from(tableName) + + switch entry.op { + case .put: + var data: [String: AnyCodable] = entry.opData?.mapValues { AnyCodable($0) } ?? [:] + data["id"] = AnyCodable(entry.id) + try await table.upsert(data).execute(); + case .patch: + guard let opData = entry.opData else { continue } + let encodableData = opData.mapValues { AnyCodable($0) } + try await table.update(encodableData).eq("id", value: entry.id).execute() + case .delete: + try await table.delete().eq( "id", value: entry.id).execute() + } + } + + try await transaction.complete.invoke(p1: nil) + + } catch { + print("Data upload error - retrying last entry: \(lastEntry!), \(error)") + throw error + } + } + + deinit { + observeAuthStateChangesTask?.cancel() + } +} diff --git a/Demo/PowerSyncExample/PowerSync/Todos.swift b/Demo/PowerSyncExample/PowerSync/Todos.swift new file mode 100644 index 0000000..dd53e31 --- /dev/null +++ b/Demo/PowerSyncExample/PowerSync/Todos.swift @@ -0,0 +1,38 @@ +import Foundation +import PowerSync + +struct Todo: Identifiable, Hashable, Decodable { + let id: String + var listId: String + var photoId: String? + var description: String + var isComplete: Bool = false + var createdAt: String? + var completedAt: String? + var createdBy: String? + var completedBy: String? + + enum CodingKeys: String, CodingKey { + case id + case listId = "list_id" + case isComplete = "completed" + case description + case createdAt = "created_at" + case completedAt = "completed_at" + case createdBy = "created_by" + case completedBy = "completed_by" + case photoId = "photo_id" + + } +} + +struct NewTodo: Encodable { + var listId: String + var isComplete: Bool = false + var description: String + var createdAt: String? + var completedAt: String? + var createdBy: String? + var completedBy: String? + var photoId: String? +} diff --git a/Demo/PowerSyncExample/PowerSyncExampleApp.swift b/Demo/PowerSyncExample/PowerSyncExampleApp.swift new file mode 100644 index 0000000..309f305 --- /dev/null +++ b/Demo/PowerSyncExample/PowerSyncExampleApp.swift @@ -0,0 +1,12 @@ +import SwiftUI +import PowerSync + +@main +struct PowerSyncExampleApp: App { + var body: some Scene { + WindowGroup { + RootView() + .environment(PowerSyncManager()) + } + } +} diff --git a/Demo/PowerSyncExample/Preview Content/Preview Assets.xcassets/Contents.json b/Demo/PowerSyncExample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Demo/PowerSyncExample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/PowerSyncExample/RootView.swift b/Demo/PowerSyncExample/RootView.swift new file mode 100644 index 0000000..50104e7 --- /dev/null +++ b/Demo/PowerSyncExample/RootView.swift @@ -0,0 +1,44 @@ +import Auth +import SwiftUI + +struct RootView: View { + @Environment(PowerSyncManager.self) var powerSync + + @State private var authModel = AuthModel() + @State private var navigationModel = NavigationModel() + + var body: some View { + NavigationStack(path: $navigationModel.path) { + Group { + if authModel.isAuthenticated { + HomeScreen() + } else { + SignInScreen() + } + } + .navigationDestination(for: Route.self) { route in + switch route { + case .home: + HomeScreen() + case .signIn: + SignInScreen() + case .signUp: + SignUpScreen() + } + } + } + .task { + if(powerSync.db == nil) { + powerSync.openDb() + } + } + .environment(authModel) + .environment(navigationModel) + } + +} + +#Preview { + RootView() + .environment(PowerSyncManager()) +} diff --git a/Demo/PowerSyncExample/Screens/HomeScreen.swift b/Demo/PowerSyncExample/Screens/HomeScreen.swift new file mode 100644 index 0000000..acf58c1 --- /dev/null +++ b/Demo/PowerSyncExample/Screens/HomeScreen.swift @@ -0,0 +1,39 @@ +import Foundation +import Auth +import SwiftUI + +struct HomeScreen: View { + @Environment(PowerSyncManager.self) private var powerSync + @Environment(AuthModel.self) private var authModel + @Environment(NavigationModel.self) private var navigationModel + + + var body: some View { + + ListView() + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Sign out") { + Task { + try await powerSync.signOut() + authModel.isAuthenticated = false + navigationModel.path = NavigationPath() + } + } + } + } + .task { + if(powerSync.db.currentStatus.connected == false) { + await powerSync.connect() + } + } + .navigationBarBackButtonHidden(true) + } +} + +#Preview { + NavigationStack{ + HomeScreen() + .environment(PowerSyncManager()) + } +} diff --git a/Demo/PowerSyncExample/Screens/SignInScreen.swift b/Demo/PowerSyncExample/Screens/SignInScreen.swift new file mode 100644 index 0000000..8e119fe --- /dev/null +++ b/Demo/PowerSyncExample/Screens/SignInScreen.swift @@ -0,0 +1,80 @@ +import SwiftUI + +private enum ActionState { + case idle + case inFlight + case result(Result) +} + +struct SignInScreen: View { + @Environment(PowerSyncManager.self) private var powerSync + @Environment(AuthModel.self) private var authModel + @Environment(NavigationModel.self) private var navigationModel + + @State private var email = "" + @State private var password = "" + @State private var actionState = ActionState.idle + + var body: some View { + Form { + Section { + TextField("Email", text: $email) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + + SecureField("Password", text: $password) + .textContentType(.password) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + } + + Section { + Button("Sign in") { + Task { + await signInButtonTapped() + } + } + } + + switch actionState { + case .idle: + EmptyView() + case .inFlight: + ProgressView() + case let .result(.failure(error)): + ErrorText(error) + case .result(.success): + Text("Sign in successful!") + } + + Section { + Button("Don't have an account? Sign up") { + navigationModel.path.append(Route.signUp) + } + } + } + } + + private func signInButtonTapped() async { + do { + actionState = .inFlight + try await powerSync.connector.client.auth.signIn(email: email, password: password) + actionState = .result(.success(())) + authModel.isAuthenticated = true + navigationModel.path = NavigationPath() + } catch { + withAnimation { + actionState = .result(.failure(error)) + } + } + } +} + +#Preview { + NavigationStack { + SignInScreen() + .environment(PowerSyncManager()) + } +} diff --git a/Demo/PowerSyncExample/Screens/SignUpScreen.swift b/Demo/PowerSyncExample/Screens/SignUpScreen.swift new file mode 100644 index 0000000..04d8c44 --- /dev/null +++ b/Demo/PowerSyncExample/Screens/SignUpScreen.swift @@ -0,0 +1,80 @@ +import SwiftUI + +private enum ActionState { + case idle + case inFlight + case result(Result) +} + +struct SignUpScreen: View { + @Environment(PowerSyncManager.self) private var powerSync + @Environment(AuthModel.self) private var authModel + @Environment(NavigationModel.self) private var navigationModel + + @State private var email = "" + @State private var password = "" + @State private var actionState = ActionState.idle + @State private var navigateToHome = false + + var body: some View { + Form { + Section { + TextField("Email", text: $email) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + + SecureField("Password", text: $password) + .textContentType(.password) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + } + + Section { + Button("Sign up") { + Task { + await signUpButtonTapped() + } + } + } + + switch actionState { + case .idle: + EmptyView() + case .inFlight: + ProgressView() + case let .result(.failure(error)): + ErrorText(error) + case .result(.success): + Text("Sign up successful!") + } + } + } + + + private func signUpButtonTapped() async { + do { + actionState = .inFlight + try await powerSync.connector.client.auth.signUp( + email: email, + password: password, + redirectTo: Constants.redirectToURL + ) + actionState = .result(.success(())) + authModel.isAuthenticated = true + navigationModel.path = NavigationPath() + } catch { + withAnimation { + actionState = .result(.failure(error)) + } + } + } +} + +#Preview { + NavigationStack { + SignUpScreen() + .environment(PowerSyncManager()) + } +} diff --git a/Demo/PowerSyncExample/Screens/TodosScreen.swift b/Demo/PowerSyncExample/Screens/TodosScreen.swift new file mode 100644 index 0000000..30774b0 --- /dev/null +++ b/Demo/PowerSyncExample/Screens/TodosScreen.swift @@ -0,0 +1,20 @@ +import Foundation +import SwiftUI + +struct TodosScreen: View { + let listId: String + + var body: some View { + TodoListView( + listId: listId + ) + } +} + +#Preview { + NavigationStack { + TodosScreen( + listId: UUID().uuidString.lowercased() + ) + } +} diff --git a/Demo/PowerSyncExample/_Secrets.swift b/Demo/PowerSyncExample/_Secrets.swift new file mode 100644 index 0000000..82ea2d8 --- /dev/null +++ b/Demo/PowerSyncExample/_Secrets.swift @@ -0,0 +1,8 @@ +import Foundation + +// Enter your Supabase and PowerSync project details. +enum Secrets { + static let powerSyncEndpoint = "https://654a826e1b70717c8dc85790.powersync.journeyapps.com" + static let supabaseURL = URL(string: "https://whlrfrfknkhckmffonio.supabase.co")! + static let supabaseAnonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndobHJmcmZrbmtoY2ttZmZvbmlvIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTkzODE0ODYsImV4cCI6MjAxNDk1NzQ4Nn0.gUTu3LDFetyda4Hd2gZLtv9o8pvCxEjDjm6DCVjrehw" +} diff --git a/Demo/README.md b/Demo/README.md new file mode 100644 index 0000000..8427629 --- /dev/null +++ b/Demo/README.md @@ -0,0 +1,47 @@ +# PowerSync Swift Demo App + +A Todo List app demonstrating the use of the PowerSync Swift SDK together with Supabase. + +The PowerSync Swift SDK is an extension of the [PowerSync Kotlin Multiplatform SDK](https://github.com/powersync-ja/powersync-kotlin), and uses the API tool [SKIE](https://skie.touchlab.co/) and KMMBridge to generate and publish a native Swift SDK. More details about this configuration can be found in our blog [here](https://www.powersync.com/blog/using-kotlin-multiplatform-with-kmmbridge-and-skie-to-publish-a-native-swift-sdk). + +The SDK reference for the PowerSync Swift SDK is available [here](https://docs.powersync.com/client-sdk-references/swift). + +## Alpha Release + +This SDK is currently in an alpha release and not suitable for production use, unless you have tested your use case(s) extensively. Breaking changes are still likely to occur. + +## Set up your Supabase and PowerSync projects + +To run this demo, you need Supabase and PowerSync projects. Detailed instructions for integrating PowerSync with Supabase can be found in [the integration guide](https://docs.powersync.com/integration-guides/supabase). + +Follow this guide to: + +1. Create and configure a Supabase project. +2. Create a new PowerSync instance, connecting to the database of the Supabase project. See instructions [here](https://docs.powersync.com/integration-guides/supabase-+-powersync#connect-powersync-to-your-supabase). +3. Deploy sync rules. + +## Configure The App + +Open the project in XCode. + +Open the “_Secrets” file and insert the credentials of your Supabase and PowerSync projects (more info can be found [here](https://docs.powersync.com/integration-guides/supabase-+-powersync#test-everything-using-our-demo-app)). + +### Finish XCode configuration + +1. Clear Swift caches + +```bash +rm -rf ~/Library/Caches/org.swift.swiftpm +rm -rf ~/Library/org.swift.swiftpm +``` + +2. In Xcode: + +- Reset Packages: File -> Packages -> Reset Package Caches +- Clean Build: Product -> Clean Build Folder. + +3. Enable CasePathMacros. We are using SwiftUI Navigation for the demo which requires this. + +## Run project + +Build the project, launch the app and sign in or register a new user. diff --git a/PowerSyncSwift/Package.resolved b/Package.resolved similarity index 100% rename from PowerSyncSwift/Package.resolved rename to Package.resolved diff --git a/PowerSyncSwift/Package.swift b/Package.swift similarity index 91% rename from PowerSyncSwift/Package.swift rename to Package.swift index ffb714b..a8e6a22 100644 --- a/PowerSyncSwift/Package.swift +++ b/Package.swift @@ -2,9 +2,10 @@ // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription +let packageName = "PowerSyncSwift" let package = Package( - name: "PowerSyncSwift", + name: packageName, platforms: [ .iOS(.v13), .macOS(.v10_13) @@ -12,7 +13,7 @@ let package = Package( products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( - name: "PowerSyncSwift", + name: packageName, targets: ["PowerSyncSwift"]), ], dependencies: [ @@ -23,7 +24,7 @@ let package = Package( // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( - name: "PowerSyncSwift", + name: packageName, dependencies: [ .product(name: "PowerSync", package: "powersync-kotlin"), .product(name: "PowerSyncSQLiteCore", package: "powersync-sqlite-core-swift") diff --git a/PowerSyncSwift/.gitignore b/PowerSyncSwift/.gitignore deleted file mode 100644 index 0023a53..0000000 --- a/PowerSyncSwift/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store -/.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc diff --git a/PowerSyncSwift/Sources/PowerSyncSwift/PowerSyncSwift.swift b/PowerSyncSwift/Sources/PowerSyncSwift/PowerSyncSwift.swift deleted file mode 100644 index ac50bf4..0000000 --- a/PowerSyncSwift/Sources/PowerSyncSwift/PowerSyncSwift.swift +++ /dev/null @@ -1,284 +0,0 @@ -import Foundation -import PowerSync - -typealias SuspendHandle = () async throws -> Any? - -class PowerSync { - let factory = DatabaseDriverFactory() - var db: PowerSyncDatabase! - - func openDb() { - db = PowerSyncDatabase(factory: factory, schema: schema, dbFilename: "powersync-swift.sqlite") - } - - func watch (_ sql: String, parameters: [String: Any] = [:], mapper: @escaping (Cursor) throws -> T) async -> AnyPublisher<[T], Never> { - } - - func execute(_ sql: String, parameters: [String: Any] = [:]) async throws { - try await db.execute(sql: sql, parameters: parameters) - } - - func writeTransaction(_ queryHandle: @escaping () async throws -> Any) async throws -> Any? { - try await db.writeTransaction(callback: SuspendTaskWrapper(queryHandle)) - } - - func readTransaction(_ queryHandle: @escaping () async throws -> Any) async throws -> Any? { - try await db.readTransaction(callback: SuspendTaskWrapper(queryHandle)) - } - - - - func insertList(_ list: NewListContent) async throws { - try await self.db.execute( - sql: "INSERT INTO \(LISTS_TABLE) (id, created_at, name, owner_id) VALUES (uuid(), datetime(), ?, ?)", - parameters: [list.name, connector.currentUserID] - ) - } - - func deleteList(id: String) async throws { - try await db.writeTransaction(callback: SuspendTaskWrapper { - try await self.db.execute( - sql: "DELETE FROM \(LISTS_TABLE) WHERE id = ?", - parameters: [id] - ) - try await self.db.execute( - sql: "DELETE FROM \(TODOS_TABLE) WHERE list_id = ?", - parameters: [id] - ) - return - }) - } - - func watchTodos(_ listId: String, _ cb: @escaping (_ todos: [Todo]) -> Void ) async { - for await todos in self.db.watch( - sql: "SELECT * FROM \(TODOS_TABLE) WHERE list_id = ?", - parameters: [listId], - mapper: { cursor in - return Todo( - id: cursor.getString(index: 0)!, - listId: cursor.getString(index: 1)!, - photoId: cursor.getString(index: 2), - description: cursor.getString(index: 3)!, - isComplete: cursor.getBoolean(index: 4)! as! Bool, - createdAt: cursor.getString(index: 5), - completedAt: cursor.getString(index: 6), - createdBy: cursor.getString(index: 7), - completedBy: cursor.getString(index: 8) - ) - } - ) { - cb(todos as! [Todo]) - } - } - - func insertTodo(_ todo: NewTodo, _ listId: String) async throws { - try await self.db.execute( - sql: "INSERT INTO \(TODOS_TABLE) (id, created_at, created_by, description, list_id, completed) VALUES (uuid(), datetime(), ?, ?, ?, ?)", - parameters: [connector.currentUserID, todo.description, listId, todo.isComplete] - ) - } - - func updateTodo(_ todo: Todo) async throws { - // Do this to avoid needing to handle date time from Swift to Kotlin - if(todo.isComplete) { - try await self.db.execute( - sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = datetime(), completed_by = ? WHERE id = ?", - parameters: [todo.description, todo.isComplete, connector.currentUserID, todo.id] - ) - } else { - try await self.db.execute( - sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = NULL, completed_by = NULL WHERE id = ?", - parameters: [todo.description, todo.isComplete, todo.id] - ) - } - } - - func deleteTodo(id: String) async throws { - try await db.writeTransaction(callback: SuspendTaskWrapper { - try await self.db.execute( - sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?", - parameters: [id] - ) - return - }) - } -} - -class SuspendTaskWrapper: KotlinSuspendFunction1 { - let handle: () async throws -> Any - - init(_ handle: @escaping () async throws -> Any) { - self.handle = handle - } - - @MainActor - func invoke(p1: Any?, completionHandler: @escaping (Any?, Error?) -> Void) { - Task { - do { - let result = try await self.handle() - completionHandler(result, nil) - } catch { - completionHandler(nil, error) - } - } - } -} -import Foundation -import PowerSync - -typealias SuspendHandle = () async throws -> Any? - -@Observable -@MainActor -class PowerSync { - let factory = DatabaseDriverFactory() - let connector = SupabaseConnector() - let schema = AppSchema - var db: PowerSyncDatabase! - - func openDb() { - db = PowerSyncDatabase(factory: factory, schema: schema, dbFilename: "powersync-swift.sqlite") - } - - // openDb must be called before connect - func connect() async { - do { - try await db.connect(connector: connector, crudThrottleMs: 1000, retryDelayMs:5000, params: [:]) - } catch { - print("Unexpected error: \(error.localizedDescription)") // Catches any other error - } - } - - func version() async -> String { - do { - return try await db.getPowerSyncVersion() - } catch { - return error.localizedDescription - } - } - - func signOut() async throws -> Void { - try await db.disconnectAndClear(clearLocal: true) - try await connector.client.auth.signOut() - } - - func writeTransaction(_ queryHandle: @escaping () async throws -> Any) async throws -> Any? { - try await db.writeTransaction(callback: SuspendTaskWrapper(queryHandle)) - } - - func readTransaction(_ queryHandle: @escaping () async throws -> Any) async throws -> Any? { - try await db.readTransaction(callback: SuspendTaskWrapper(queryHandle)) - } - - func watchLists(_ cb: @escaping (_ lists: [ListContent]) -> Void ) async { - for await lists in self.db.watch( - sql: "SELECT * FROM \(LISTS_TABLE)", - parameters: [], - mapper: { cursor in - ListContent( - id: cursor.getString(index: 0)!, - name: cursor.getString(index: 1)!, - createdAt: cursor.getString(index: 2)!, - ownerId: cursor.getString(index: 3)! - ) - } - ) { - cb(lists as! [ListContent]) - } - } - - func insertList(_ list: NewListContent) async throws { - try await self.db.execute( - sql: "INSERT INTO \(LISTS_TABLE) (id, created_at, name, owner_id) VALUES (uuid(), datetime(), ?, ?)", - parameters: [list.name, connector.currentUserID] - ) - } - - func deleteList(id: String) async throws { - try await db.writeTransaction(callback: SuspendTaskWrapper { - try await self.db.execute( - sql: "DELETE FROM \(LISTS_TABLE) WHERE id = ?", - parameters: [id] - ) - try await self.db.execute( - sql: "DELETE FROM \(TODOS_TABLE) WHERE list_id = ?", - parameters: [id] - ) - return - }) - } - - func watchTodos(_ listId: String, _ cb: @escaping (_ todos: [Todo]) -> Void ) async { - for await todos in self.db.watch( - sql: "SELECT * FROM \(TODOS_TABLE) WHERE list_id = ?", - parameters: [listId], - mapper: { cursor in - return Todo( - id: cursor.getString(index: 0)!, - listId: cursor.getString(index: 1)!, - photoId: cursor.getString(index: 2), - description: cursor.getString(index: 3)!, - isComplete: cursor.getBoolean(index: 4)! as! Bool, - createdAt: cursor.getString(index: 5), - completedAt: cursor.getString(index: 6), - createdBy: cursor.getString(index: 7), - completedBy: cursor.getString(index: 8) - ) - } - ) { - cb(todos as! [Todo]) - } - } - - func insertTodo(_ todo: NewTodo, _ listId: String) async throws { - try await self.db.execute( - sql: "INSERT INTO \(TODOS_TABLE) (id, created_at, created_by, description, list_id, completed) VALUES (uuid(), datetime(), ?, ?, ?, ?)", - parameters: [connector.currentUserID, todo.description, listId, todo.isComplete] - ) - } - - func updateTodo(_ todo: Todo) async throws { - // Do this to avoid needing to handle date time from Swift to Kotlin - if(todo.isComplete) { - try await self.db.execute( - sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = datetime(), completed_by = ? WHERE id = ?", - parameters: [todo.description, todo.isComplete, connector.currentUserID, todo.id] - ) - } else { - try await self.db.execute( - sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = NULL, completed_by = NULL WHERE id = ?", - parameters: [todo.description, todo.isComplete, todo.id] - ) - } - } - - func deleteTodo(id: String) async throws { - try await db.writeTransaction(callback: SuspendTaskWrapper { - try await self.db.execute( - sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?", - parameters: [id] - ) - return - }) - } -} - -class SuspendTaskWrapper: KotlinSuspendFunction1 { - let handle: () async throws -> Any - - init(_ handle: @escaping () async throws -> Any) { - self.handle = handle - } - - @MainActor - func invoke(p1: Any?, completionHandler: @escaping (Any?, Error?) -> Void) { - Task { - do { - let result = try await self.handle() - completionHandler(result, nil) - } catch { - completionHandler(nil, error) - } - } - } -} diff --git a/Sources/PowerSyncSwift/KotlinTypes.swift b/Sources/PowerSyncSwift/KotlinTypes.swift new file mode 100644 index 0000000..01de628 --- /dev/null +++ b/Sources/PowerSyncSwift/KotlinTypes.swift @@ -0,0 +1,13 @@ +import PowerSync + +public typealias Schema = PowerSync.Schema +typealias KmpPowerSyncDatabase = PowerSync.PowerSyncDatabase +public typealias PowerSyncBackendConnector = PowerSync.PowerSyncBackendConnector +public typealias CrudEntry = PowerSync.CrudEntry +public typealias CrudBatch = PowerSync.CrudBatch +public typealias SyncStatus = PowerSync.SyncStatus +public typealias SqlCursor = PowerSync.RuntimeSqlCursor +public typealias JsonParam = PowerSync.JsonParam +public typealias CrudTransaction = PowerSync.CrudTransaction +public typealias PowerSyncCredentials = PowerSync.PowerSyncCredentials + diff --git a/Sources/PowerSyncSwift/PowerSyncDatabase.swift b/Sources/PowerSyncSwift/PowerSyncDatabase.swift new file mode 100644 index 0000000..9c3491b --- /dev/null +++ b/Sources/PowerSyncSwift/PowerSyncDatabase.swift @@ -0,0 +1,21 @@ +import Foundation + +/// Default database filename +public let DEFAULT_DB_FILENAME = "powersync.db" + +/// Creates a PowerSyncDatabase instance +/// - Parameters: +/// - schema: The database schema +/// - dbFilename: The database filename. Defaults to "powersync.db" +/// - Returns: A configured PowerSyncDatabase instance +public func PowerSyncDatabase( + schema: Schema, + dbFilename: String = DEFAULT_DB_FILENAME +) -> PowerSyncDatabaseProtocol { + + + return PowerSyncDatabaseImpl( + schema: schema, + dbFilename: dbFilename + ) +} diff --git a/Sources/PowerSyncSwift/PowerSyncDatabaseImpl.swift b/Sources/PowerSyncSwift/PowerSyncDatabaseImpl.swift new file mode 100644 index 0000000..45ebef3 --- /dev/null +++ b/Sources/PowerSyncSwift/PowerSyncDatabaseImpl.swift @@ -0,0 +1,173 @@ +import Foundation +import PowerSync + +/// Implementation of PowerSyncDatabaseProtocol that initially wraps the KMP implementation +/// and allows for gradual migration to pure Swift code +final class PowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { + private let kmpDatabase: PowerSync.PowerSyncDatabase + + var currentStatus: SyncStatus { + get { kmpDatabase.currentStatus } + } + + init( + schema: Schema, + dbFilename: String + ) { + let factory = PowerSync.DatabaseDriverFactory() + self.kmpDatabase = PowerSyncDatabase( + factory: factory, + schema: schema, + dbFilename: dbFilename + ) + } + + + func waitForFirstSync() async throws { + try await kmpDatabase.waitForFirstSync() + } + + func connect( + connector: PowerSyncBackendConnector, + crudThrottleMs: Int64 = 1000, + retryDelayMs: Int64 = 5000, + params: [String: JsonParam?] = [:] + ) async throws { + // Convert Swift types to KMP types + try await kmpDatabase.connect( + connector: connector, + crudThrottleMs: crudThrottleMs, + retryDelayMs: retryDelayMs, + params: params + ) + } + + func getCrudBatch(limit: Int32 = 100) async throws -> CrudBatch? { + try await kmpDatabase.getCrudBatch(limit: limit) + } + + func getNextCrudTransaction() async throws -> CrudTransaction? { + try await kmpDatabase.getNextCrudTransaction() + } + + func getPowerSyncVersion() async throws -> String { + try await kmpDatabase.getPowerSyncVersion() + } + + func disconnect() async throws { + try await kmpDatabase.disconnect() + } + + func disconnectAndClear(clearLocal: Bool = true) async throws { + try await kmpDatabase.disconnectAndClear(clearLocal: clearLocal) + } + + func execute(sql: String, parameters: [Any]?) async throws -> Int64 { + Int64(try await kmpDatabase.execute(sql: sql, parameters: parameters)) + } + + func get( + sql: String, + parameters: [Any]?, + mapper: @escaping (SqlCursor) -> RowType + ) async throws -> RowType { + try await kmpDatabase.get( + sql: sql, + parameters: parameters, + mapper: mapper + ) as! RowType + } + + func getAll( + sql: String, + parameters: [Any]?, + mapper: @escaping (SqlCursor) -> RowType + ) async throws -> [RowType] { + try await kmpDatabase.getAll( + sql: sql, + parameters: parameters, + mapper: mapper + ) as! [RowType] + } + + func getOptional( + sql: String, + parameters: [Any]?, + mapper: @escaping (SqlCursor) -> RowType + ) async throws -> RowType? { + try await kmpDatabase.getOptional( + sql: sql, + parameters: parameters, + mapper: mapper + ) as! RowType? + } + + func watch( + sql: String, + parameters: [Any]?, + mapper: @escaping (SqlCursor) -> RowType + ) -> AsyncStream<[RowType]> { + AsyncStream { continuation in + Task { + for await values in self.kmpDatabase.watch( + sql: sql, + parameters: parameters, + mapper: mapper + ) { + continuation.yield(values as! [RowType]) + } + continuation.finish() + } + } + } + + func writeTransaction(callback: @escaping (any PowerSyncTransactionProtocol) async throws -> R) async throws -> R { + let wrappedCallback = SuspendTaskWrapper { [kmpDatabase] in + // Create a wrapper that converts the KMP transaction to our Swift protocol + if let kmpTransaction = kmpDatabase as? PowerSyncTransactionProtocol { + return try await callback(kmpTransaction) + } else { + throw PowerSyncError.invalidTransaction + } + } + + return try await kmpDatabase.writeTransaction(callback: wrappedCallback) as! R + } + + func readTransaction(callback: @escaping (any PowerSyncTransactionProtocol) async throws -> R) async throws -> R { + let wrappedCallback = SuspendTaskWrapper { [kmpDatabase] in + // Create a wrapper that converts the KMP transaction to our Swift protocol + if let kmpTransaction = kmpDatabase as? PowerSyncTransactionProtocol { + return try await callback(kmpTransaction) + } else { + throw PowerSyncError.invalidTransaction + } + } + + return try await kmpDatabase.readTransaction(callback: wrappedCallback) as! R + } +} + +enum PowerSyncError: Error { + case invalidTransaction +} + +class SuspendTaskWrapper: KotlinSuspendFunction1 { + let handle: () async throws -> Any + + init(_ handle: @escaping () async throws -> Any) { + self.handle = handle + } + + @MainActor + func invoke(p1: Any?, completionHandler: @escaping (Any?, Error?) -> Void) { + Task { + do { + let result = try await self.handle() + completionHandler(result, nil) + } catch { + completionHandler(nil, error) + } + } + } +} diff --git a/Sources/PowerSyncSwift/PowerSyncDatabaseProtocol.swift b/Sources/PowerSyncSwift/PowerSyncDatabaseProtocol.swift new file mode 100644 index 0000000..a5e5813 --- /dev/null +++ b/Sources/PowerSyncSwift/PowerSyncDatabaseProtocol.swift @@ -0,0 +1,93 @@ +import Foundation + +/// A PowerSync managed database. +/// +/// Use one instance per database file. +/// +/// Use `PowerSyncDatabase.connect` to connect to the PowerSync service, to keep the local database in sync with the remote database. +/// +/// All changes to local tables are automatically recorded, whether connected or not. Once connected, the changes are uploaded. +public protocol PowerSyncDatabaseProtocol: Queries { + /// The current sync status. + var currentStatus: SyncStatus { get } + + /// Wait for the first sync to occur + func waitForFirstSync() async throws + + /// Connect to the PowerSync service, and keep the databases in sync. + /// + /// The connection is automatically re-opened if it fails for any reason. + /// + /// - Parameters: + /// - connector: The PowerSyncBackendConnector to use + /// - crudThrottleMs: Time between CRUD operations. Defaults to 1000ms. + /// - retryDelayMs: Delay between retries after failure. Defaults to 5000ms. + /// - params: Sync parameters from the client + /// + /// Example usage: + /// ```swift + /// let params: [String: JsonParam] = [ + /// "name": .string("John Doe"), + /// "age": .number(30), + /// "isStudent": .boolean(false) + /// ] + /// + /// try await connect( + /// connector: connector, + /// crudThrottleMs: 2000, + /// retryDelayMs: 10000, + /// params: params + /// ) + /// ``` + func connect( + connector: PowerSyncBackendConnector, + crudThrottleMs: Int64, + retryDelayMs: Int64, + params: [String: JsonParam?] + ) async throws + + /// Get a batch of crud data to upload. + /// + /// Returns nil if there is no data to upload. + /// + /// Use this from the `PowerSyncBackendConnector.uploadData` callback. + /// + /// Once the data have been successfully uploaded, call `CrudBatch.complete` before + /// requesting the next batch. + /// + /// - Parameter limit: Maximum number of updates to return in a single batch. Default is 100. + /// + /// This method does include transaction ids in the result, but does not group + /// data by transaction. One batch may contain data from multiple transactions, + /// and a single transaction may be split over multiple batches. + func getCrudBatch(limit: Int32) async throws -> CrudBatch? + + /// Get the next recorded transaction to upload. + /// + /// Returns nil if there is no data to upload. + /// + /// Use this from the `PowerSyncBackendConnector.uploadData` callback. + /// + /// Once the data have been successfully uploaded, call `CrudTransaction.complete` before + /// requesting the next transaction. + /// + /// Unlike `getCrudBatch`, this only returns data from a single transaction at a time. + /// All data for the transaction is loaded into memory. + func getNextCrudTransaction() async throws -> CrudTransaction? + + /// Convenience method to get the current version of PowerSync. + func getPowerSyncVersion() async throws -> String + + /// Close the sync connection. + /// + /// Use `connect` to connect again. + func disconnect() async throws + + /// Disconnect and clear the database. + /// Use this when logging out. + /// The database can still be queried after this is called, but the tables + /// would be empty. + /// + /// - Parameter clearLocal: Set to false to preserve data in local-only tables. + func disconnectAndClear(clearLocal: Bool) async throws +} diff --git a/Sources/PowerSyncSwift/PowerSyncSwift.swift b/Sources/PowerSyncSwift/PowerSyncSwift.swift new file mode 100644 index 0000000..ff25173 --- /dev/null +++ b/Sources/PowerSyncSwift/PowerSyncSwift.swift @@ -0,0 +1,106 @@ +import Foundation +//import PowerSync +// +//typealias SuspendHandle = () async throws -> Any? +// +//class PowerSync2 { +// let factory = DatabaseDriverFactory() +// var db: PowerSyncDatabase! +// +// func openDb() { +// db = PowerSyncDatabase(factory: factory, schema: schema, dbFilename: "powersync-swift.sqlite") +// } +// +// func watch (_ sql: String, parameters: [String: Any] = [:], mapper: @escaping (Cursor) throws -> T) async -> AnyPublisher<[T], Never> { +// } +// +// func execute(_ sql: String, parameters: [String: Any] = [:]) async throws { +// try await db.execute(sql: sql, parameters: parameters) +// } +// +// func writeTransaction(_ queryHandle: @escaping () async throws -> Any) async throws -> Any? { +// try await db.writeTransaction(callback: SuspendTaskWrapper(queryHandle)) +// } +// +// func readTransaction(_ queryHandle: @escaping () async throws -> Any) async throws -> Any? { +// try await db.readTransaction(callback: SuspendTaskWrapper(queryHandle)) +// } +// +// +// +// func insertList(_ list: NewListContent) async throws { +// try await self.db.execute( +// sql: "INSERT INTO \(LISTS_TABLE) (id, created_at, name, owner_id) VALUES (uuid(), datetime(), ?, ?)", +// parameters: [list.name, connector.currentUserID] +// ) +// } +// +// func deleteList(id: String) async throws { +// try await db.writeTransaction(callback: SuspendTaskWrapper { +// try await self.db.execute( +// sql: "DELETE FROM \(LISTS_TABLE) WHERE id = ?", +// parameters: [id] +// ) +// try await self.db.execute( +// sql: "DELETE FROM \(TODOS_TABLE) WHERE list_id = ?", +// parameters: [id] +// ) +// return +// }) +// } +// +// func watchTodos(_ listId: String, _ cb: @escaping (_ todos: [Todo]) -> Void ) async { +// for await todos in self.db.watch( +// sql: "SELECT * FROM \(TODOS_TABLE) WHERE list_id = ?", +// parameters: [listId], +// mapper: { cursor in +// return Todo( +// id: cursor.getString(index: 0)!, +// listId: cursor.getString(index: 1)!, +// photoId: cursor.getString(index: 2), +// description: cursor.getString(index: 3)!, +// isComplete: cursor.getBoolean(index: 4)! as! Bool, +// createdAt: cursor.getString(index: 5), +// completedAt: cursor.getString(index: 6), +// createdBy: cursor.getString(index: 7), +// completedBy: cursor.getString(index: 8) +// ) +// } +// ) { +// cb(todos as! [Todo]) +// } +// } +// +// func insertTodo(_ todo: NewTodo, _ listId: String) async throws { +// try await self.db.execute( +// sql: "INSERT INTO \(TODOS_TABLE) (id, created_at, created_by, description, list_id, completed) VALUES (uuid(), datetime(), ?, ?, ?, ?)", +// parameters: [connector.currentUserID, todo.description, listId, todo.isComplete] +// ) +// } +// +// func updateTodo(_ todo: Todo) async throws { +// // Do this to avoid needing to handle date time from Swift to Kotlin +// if(todo.isComplete) { +// try await self.db.execute( +// sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = datetime(), completed_by = ? WHERE id = ?", +// parameters: [todo.description, todo.isComplete, connector.currentUserID, todo.id] +// ) +// } else { +// try await self.db.execute( +// sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = NULL, completed_by = NULL WHERE id = ?", +// parameters: [todo.description, todo.isComplete, todo.id] +// ) +// } +// } +// +// func deleteTodo(id: String) async throws { +// try await db.writeTransaction(callback: SuspendTaskWrapper { +// try await self.db.execute( +// sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?", +// parameters: [id] +// ) +// return +// }) +// } +//} +// diff --git a/Sources/PowerSyncSwift/PowerSyncTransactionProtocol.swift b/Sources/PowerSyncSwift/PowerSyncTransactionProtocol.swift new file mode 100644 index 0000000..b4b4d81 --- /dev/null +++ b/Sources/PowerSyncSwift/PowerSyncTransactionProtocol.swift @@ -0,0 +1,29 @@ +public protocol PowerSyncTransactionProtocol { + /// Execute a write query and return the number of affected rows + func execute( + sql: String, + parameters: [Any]? + ) async throws -> Int64 + + /// Execute a read-only query and return a single optional result + func getOptional( + sql: String, + parameters: [Any]?, + mapper: @escaping (SqlCursor) -> RowType + ) async throws -> RowType? + + /// Execute a read-only query and return all results + func getAll( + sql: String, + parameters: [Any]?, + mapper: @escaping (SqlCursor) -> RowType + ) async throws -> [RowType] + + /// Execute a read-only query and return a single result + /// Throws if no result is found + func get( + sql: String, + parameters: [Any]?, + mapper: @escaping (SqlCursor) -> RowType + ) async throws -> RowType +} diff --git a/Sources/PowerSyncSwift/QueriesProtocol.swift b/Sources/PowerSyncSwift/QueriesProtocol.swift new file mode 100644 index 0000000..2aa69ee --- /dev/null +++ b/Sources/PowerSyncSwift/QueriesProtocol.swift @@ -0,0 +1,44 @@ +import Foundation +import Combine + +public protocol Queries { + /// Execute a write query (INSERT, UPDATE, DELETE) + func execute(sql: String, parameters: [Any]?) async throws -> Int64 + + /// Execute a read-only (SELECT) query and return a single result. + /// If there is no result, throws an IllegalArgumentException. + /// See `getOptional` for queries where the result might be empty. + func get( + sql: String, + parameters: [Any]?, + mapper: @escaping (SqlCursor) -> RowType + ) async throws -> RowType + + /// Execute a read-only (SELECT) query and return the results. + func getAll( + sql: String, + parameters: [Any]?, + mapper: @escaping (SqlCursor) -> RowType + ) async throws -> [RowType] + + /// Execute a read-only (SELECT) query and return a single optional result. + func getOptional( + sql: String, + parameters: [Any]?, + mapper: @escaping (SqlCursor) -> RowType + ) async throws -> RowType? + + /// Execute a read-only (SELECT) query every time the source tables are modified + /// and return the results as an array in a Publisher. + func watch( + sql: String, + parameters: [Any]?, + mapper: @escaping (SqlCursor) -> RowType + ) -> AsyncStream<[RowType]> + + /// Execute a write transaction with the given callback + func writeTransaction(callback: @escaping (PowerSyncTransactionProtocol) async throws -> R) async throws -> R + + /// Execute a read transaction with the given callback + func readTransaction(callback: @escaping (PowerSyncTransactionProtocol) async throws -> R) async throws -> R +} diff --git a/PowerSyncSwift/Tests/PowerSyncSwiftTests/PowerSyncSwiftTests.swift b/Tests/PowerSyncSwiftTests/PowerSyncSwiftTests.swift similarity index 100% rename from PowerSyncSwift/Tests/PowerSyncSwiftTests/PowerSyncSwiftTests.swift rename to Tests/PowerSyncSwiftTests/PowerSyncSwiftTests.swift From ef78731dd3aa619303dd8fbbeaa4f07949fb3f0d Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Fri, 25 Oct 2024 13:06:23 +0200 Subject: [PATCH 03/11] chore: add tests --- .github/workflows/build_and_test.yaml | 22 ++ .../Components/AddListView.swift | 4 +- .../Components/AddTodoListView.swift | 4 +- .../PowerSync/PowerSyncManager.swift | 10 +- Demo/PowerSyncExample/PowerSync/Schema.swift | 40 ++-- .../PowerSync/SupabaseConnector.swift | 6 +- .../PowerSyncSwift/Kotlin/KotlinAdapter.swift | 64 ++++++ .../KotlinPowerSyncDatabaseImpl.swift} | 10 +- .../{ => Kotlin}/KotlinTypes.swift | 2 - .../PowerSyncSwift/PowerSyncDatabase.swift | 4 +- .../PowerSyncDatabaseProtocol.swift | 24 +++ Sources/PowerSyncSwift/PowerSyncSwift.swift | 106 --------- Sources/PowerSyncSwift/Schema/Column.swift | 38 ++++ Sources/PowerSyncSwift/Schema/Index.swift | 44 ++++ .../PowerSyncSwift/Schema/IndexedColumn.swift | 27 +++ Sources/PowerSyncSwift/Schema/Schema.swift | 33 +++ Sources/PowerSyncSwift/Schema/Table.swift | 135 ++++++++++++ .../PowerSyncSwiftTests.swift | 6 - .../Schema/ColumnTests.swift | 66 ++++++ .../Schema/IndexTests.swift | 95 ++++++++ .../Schema/IndexedColumnTests.swift | 65 ++++++ .../Schema/SchemaTests.swift | 110 ++++++++++ .../Schema/TableTests.swift | 204 ++++++++++++++++++ 23 files changed, 960 insertions(+), 159 deletions(-) create mode 100644 .github/workflows/build_and_test.yaml create mode 100644 Sources/PowerSyncSwift/Kotlin/KotlinAdapter.swift rename Sources/PowerSyncSwift/{PowerSyncDatabaseImpl.swift => Kotlin/KotlinPowerSyncDatabaseImpl.swift} (93%) rename Sources/PowerSyncSwift/{ => Kotlin}/KotlinTypes.swift (82%) delete mode 100644 Sources/PowerSyncSwift/PowerSyncSwift.swift create mode 100644 Sources/PowerSyncSwift/Schema/Column.swift create mode 100644 Sources/PowerSyncSwift/Schema/Index.swift create mode 100644 Sources/PowerSyncSwift/Schema/IndexedColumn.swift create mode 100644 Sources/PowerSyncSwift/Schema/Schema.swift create mode 100644 Sources/PowerSyncSwift/Schema/Table.swift delete mode 100644 Tests/PowerSyncSwiftTests/PowerSyncSwiftTests.swift create mode 100644 Tests/PowerSyncSwiftTests/Schema/ColumnTests.swift create mode 100644 Tests/PowerSyncSwiftTests/Schema/IndexTests.swift create mode 100644 Tests/PowerSyncSwiftTests/Schema/IndexedColumnTests.swift create mode 100644 Tests/PowerSyncSwiftTests/Schema/SchemaTests.swift create mode 100644 Tests/PowerSyncSwiftTests/Schema/TableTests.swift diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml new file mode 100644 index 0000000..1f395d7 --- /dev/null +++ b/.github/workflows/build_and_test.yaml @@ -0,0 +1,22 @@ +name: Build and test + +on: + push + +jobs: + build: + name: Swift ${{ matrix.swift }} on ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + swift: ["5.7", "6.0"] + runs-on: ${{ matrix.os }} + steps: + - uses: swift-actions/setup-swift@v2 + with: + swift-version: ${{ matrix.swift }} + - uses: actions/checkout@v4 + - name: Build + run: swift build + - name: Run tests + run: swift test diff --git a/Demo/PowerSyncExample/Components/AddListView.swift b/Demo/PowerSyncExample/Components/AddListView.swift index 0b591bc..5792ab4 100644 --- a/Demo/PowerSyncExample/Components/AddListView.swift +++ b/Demo/PowerSyncExample/Components/AddListView.swift @@ -13,9 +13,9 @@ struct AddListView: View { Task.detached { do { try await powerSync.insertList(newList) - completion(.success(true)) + await completion(.success(true)) } catch { - completion(.failure(error)) + await completion(.failure(error)) throw error } } diff --git a/Demo/PowerSyncExample/Components/AddTodoListView.swift b/Demo/PowerSyncExample/Components/AddTodoListView.swift index 321dd70..615efae 100644 --- a/Demo/PowerSyncExample/Components/AddTodoListView.swift +++ b/Demo/PowerSyncExample/Components/AddTodoListView.swift @@ -15,9 +15,9 @@ struct AddTodoListView: View { Task.detached { do { try await powerSync.insertTodo(newTodo, listId) - completion(.success(true)) + await completion(.success(true)) } catch { - completion(.failure(error)) + await completion(.failure(error)) throw error } } diff --git a/Demo/PowerSyncExample/PowerSync/PowerSyncManager.swift b/Demo/PowerSyncExample/PowerSync/PowerSyncManager.swift index 5ff8800..1253742 100644 --- a/Demo/PowerSyncExample/PowerSync/PowerSyncManager.swift +++ b/Demo/PowerSyncExample/PowerSync/PowerSyncManager.swift @@ -8,14 +8,14 @@ class PowerSyncManager { let schema = AppSchema var db: PowerSyncDatabaseProtocol! + // openDb must be called before connect func openDb() { db = PowerSyncDatabase(schema: schema, dbFilename: "powersync-swift.sqlite") } - - // openDb must be called before connect + func connect() async { do { - try await db.connect(connector: connector, crudThrottleMs: 1000, retryDelayMs:5000, params: [:]) + try await db.connect(connector: connector) } catch { print("Unexpected error: \(error.localizedDescription)") // Catches any other error } @@ -30,12 +30,12 @@ class PowerSyncManager { } func signOut() async throws -> Void { - try await db.disconnectAndClear(clearLocal: true) + try await db.disconnectAndClear() try await connector.client.auth.signOut() } func watchLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { - for await lists in self.db.watch( + for await lists in self.db.watch<[ListContent]>( sql: "SELECT * FROM \(LISTS_TABLE)", parameters: [], mapper: { cursor in diff --git a/Demo/PowerSyncExample/PowerSync/Schema.swift b/Demo/PowerSyncExample/PowerSync/Schema.swift index 78f3068..f49bc99 100644 --- a/Demo/PowerSyncExample/PowerSync/Schema.swift +++ b/Demo/PowerSyncExample/PowerSync/Schema.swift @@ -1,5 +1,5 @@ import Foundation -import PowerSync +import PowerSyncSwift let LISTS_TABLE = "lists" let TODOS_TABLE = "todos" @@ -8,40 +8,32 @@ let lists = Table( name: LISTS_TABLE, columns: [ // ID column is automatically included - Column(name: "name", type: ColumnType.text), - Column(name: "created_at", type: ColumnType.text), - Column(name: "owner_id", type: ColumnType.text) - ], - indexes: [], - localOnly: false, - insertOnly: false, - viewNameOverride: LISTS_TABLE + .text("name"), + .text("created_at"), + .text("owner_id") + ] ) let todos = Table( name: TODOS_TABLE, // ID column is automatically included columns: [ - Column(name: "list_id", type: ColumnType.text), - Column(name: "photo_id", type: ColumnType.text), - Column(name: "description", type: ColumnType.text), + Column.text("list_id"), + Column.text("photo_id"), + Column.text("description"), // 0 or 1 to represent false or true - Column(name: "completed", type: ColumnType.integer), - Column(name: "created_at", type: ColumnType.text), - Column(name: "completed_at", type: ColumnType.text), - Column(name: "created_by", type: ColumnType.text), - Column(name: "completed_by", type: ColumnType.text) - + Column.integer("completed"), + Column.text("created_at"), + Column.text("completed_at"), + Column.text("created_by"), + Column.text("completed_by") ], indexes: [ Index( name: "list_id", - columns: [IndexedColumn(column: "list_id", ascending: true, columnDefinition: nil, type: nil)] + columns: [IndexedColumn.ascending("list_id")] ) - ], - localOnly: false, - insertOnly: false, - viewNameOverride: TODOS_TABLE + ] ) -let AppSchema = Schema(tables: [lists, todos]) +let AppSchema = Schema(lists, todos) diff --git a/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift b/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift index 7549539..b8333b4 100644 --- a/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift +++ b/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift @@ -21,7 +21,7 @@ class SupabaseConnector: PowerSyncBackendConnector { guard let self = self else { return } for await (event, session) in self.client.auth.authStateChanges { - guard [.initialSession, .signedIn, .signedOut].contains(event) else { throw AuthError.sessionNotFound } + guard [.initialSession, .signedIn, .signedOut].contains(event) else { throw AuthError.sessionMissing } self.session = session } @@ -40,7 +40,7 @@ class SupabaseConnector: PowerSyncBackendConnector { session = try await client.auth.session if (self.session == nil) { - throw AuthError.sessionNotFound + throw AuthError.sessionMissing } let token = session!.accessToken @@ -49,7 +49,7 @@ class SupabaseConnector: PowerSyncBackendConnector { return PowerSyncCredentials(endpoint: self.powerSyncEndpoint, token: token, userId: currentUserID) } - override func uploadData(database: any PowerSyncDatabase) async throws { + override func uploadData(database: PowerSyncDatabase) async throws { guard let transaction = try await database.getNextCrudTransaction() else { return } diff --git a/Sources/PowerSyncSwift/Kotlin/KotlinAdapter.swift b/Sources/PowerSyncSwift/Kotlin/KotlinAdapter.swift new file mode 100644 index 0000000..096bc02 --- /dev/null +++ b/Sources/PowerSyncSwift/Kotlin/KotlinAdapter.swift @@ -0,0 +1,64 @@ +import PowerSync + +internal struct KotlinAdapter { + struct Index { + static func toKotlin(_ index: IndexProtocol) -> PowerSync.Index { + PowerSync.Index( + name: index.name, + columns: index.columns.map { IndexedColumn.toKotlin($0) } + ) + } + } + + struct IndexedColumn { + static func toKotlin(_ column: IndexedColumnProtocol) -> PowerSync.IndexedColumn { + return PowerSync.IndexedColumn( + column: column.column, + ascending: column.ascending, + columnDefinition: nil, + type: nil + ) + } + } + + struct Table { + static func toKotlin(_ table: TableProtocol) -> PowerSync.Table { + PowerSync.Table( + name: table.name, + columns: table.columns.map {Column.toKotlin($0)}, + indexes: table.indexes.map { Index.toKotlin($0) }, + localOnly: table.localOnly, + insertOnly: table.insertOnly, + viewNameOverride: table.viewNameOverride + ) + } + } + + struct Column { + static func toKotlin(_ column: any ColumnProtocol) -> PowerSync.Column { + PowerSync.Column( + name: column.name, + type: columnType(from: column.type) + ) + } + + private static func columnType(from swiftType: ColumnData) -> PowerSync.ColumnType { + switch swiftType { + case .text: + return PowerSync.ColumnType.text + case .integer: + return PowerSync.ColumnType.integer + case .real: + return PowerSync.ColumnType.real + } + } + } + + struct Schema { + static func toKotlin(_ schema: SchemaProtocol) -> PowerSync.Schema { + PowerSync.Schema( + tables: schema.tables.map { Table.toKotlin($0) } + ) + } + } +} diff --git a/Sources/PowerSyncSwift/PowerSyncDatabaseImpl.swift b/Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift similarity index 93% rename from Sources/PowerSyncSwift/PowerSyncDatabaseImpl.swift rename to Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift index 45ebef3..6fd3b22 100644 --- a/Sources/PowerSyncSwift/PowerSyncDatabaseImpl.swift +++ b/Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -1,9 +1,7 @@ import Foundation import PowerSync -/// Implementation of PowerSyncDatabaseProtocol that initially wraps the KMP implementation -/// and allows for gradual migration to pure Swift code -final class PowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { +final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { private let kmpDatabase: PowerSync.PowerSyncDatabase var currentStatus: SyncStatus { @@ -17,12 +15,11 @@ final class PowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { let factory = PowerSync.DatabaseDriverFactory() self.kmpDatabase = PowerSyncDatabase( factory: factory, - schema: schema, + schema: KotlinAdapter.Schema.toKotlin(schema), dbFilename: dbFilename ) } - func waitForFirstSync() async throws { try await kmpDatabase.waitForFirstSync() } @@ -33,7 +30,6 @@ final class PowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { retryDelayMs: Int64 = 5000, params: [String: JsonParam?] = [:] ) async throws { - // Convert Swift types to KMP types try await kmpDatabase.connect( connector: connector, crudThrottleMs: crudThrottleMs, @@ -63,7 +59,7 @@ final class PowerSyncDatabaseImpl: PowerSyncDatabaseProtocol { } func execute(sql: String, parameters: [Any]?) async throws -> Int64 { - Int64(try await kmpDatabase.execute(sql: sql, parameters: parameters)) + Int64(truncating: try await kmpDatabase.execute(sql: sql, parameters: parameters)) } func get( diff --git a/Sources/PowerSyncSwift/KotlinTypes.swift b/Sources/PowerSyncSwift/Kotlin/KotlinTypes.swift similarity index 82% rename from Sources/PowerSyncSwift/KotlinTypes.swift rename to Sources/PowerSyncSwift/Kotlin/KotlinTypes.swift index 01de628..1477e48 100644 --- a/Sources/PowerSyncSwift/KotlinTypes.swift +++ b/Sources/PowerSyncSwift/Kotlin/KotlinTypes.swift @@ -1,7 +1,5 @@ import PowerSync -public typealias Schema = PowerSync.Schema -typealias KmpPowerSyncDatabase = PowerSync.PowerSyncDatabase public typealias PowerSyncBackendConnector = PowerSync.PowerSyncBackendConnector public typealias CrudEntry = PowerSync.CrudEntry public typealias CrudBatch = PowerSync.CrudBatch diff --git a/Sources/PowerSyncSwift/PowerSyncDatabase.swift b/Sources/PowerSyncSwift/PowerSyncDatabase.swift index 9c3491b..600e346 100644 --- a/Sources/PowerSyncSwift/PowerSyncDatabase.swift +++ b/Sources/PowerSyncSwift/PowerSyncDatabase.swift @@ -8,13 +8,13 @@ public let DEFAULT_DB_FILENAME = "powersync.db" /// - schema: The database schema /// - dbFilename: The database filename. Defaults to "powersync.db" /// - Returns: A configured PowerSyncDatabase instance +@MainActor public func PowerSyncDatabase( schema: Schema, dbFilename: String = DEFAULT_DB_FILENAME ) -> PowerSyncDatabaseProtocol { - - return PowerSyncDatabaseImpl( + return KotlinPowerSyncDatabaseImpl( schema: schema, dbFilename: dbFilename ) diff --git a/Sources/PowerSyncSwift/PowerSyncDatabaseProtocol.swift b/Sources/PowerSyncSwift/PowerSyncDatabaseProtocol.swift index a5e5813..296ac66 100644 --- a/Sources/PowerSyncSwift/PowerSyncDatabaseProtocol.swift +++ b/Sources/PowerSyncSwift/PowerSyncDatabaseProtocol.swift @@ -91,3 +91,27 @@ public protocol PowerSyncDatabaseProtocol: Queries { /// - Parameter clearLocal: Set to false to preserve data in local-only tables. func disconnectAndClear(clearLocal: Bool) async throws } + +public extension PowerSyncDatabaseProtocol { + func connect( + connector: PowerSyncBackendConnector, + crudThrottleMs: Int64 = 1000, + retryDelayMs: Int64 = 5000, + params: [String: JsonParam?] = [:] + ) async throws { + try await connect( + connector: connector, + crudThrottleMs: crudThrottleMs, + retryDelayMs: retryDelayMs, + params: params + ) + } + + func disconnectAndClear(clearLocal: Bool = true) async throws { + try await disconnectAndClear(clearLocal: clearLocal) + } + + func getCrudBatch(limit: Int32 = 100) async throws -> CrudBatch? { + try await getCrudBatch(limit: 100) + } +} diff --git a/Sources/PowerSyncSwift/PowerSyncSwift.swift b/Sources/PowerSyncSwift/PowerSyncSwift.swift deleted file mode 100644 index ff25173..0000000 --- a/Sources/PowerSyncSwift/PowerSyncSwift.swift +++ /dev/null @@ -1,106 +0,0 @@ -import Foundation -//import PowerSync -// -//typealias SuspendHandle = () async throws -> Any? -// -//class PowerSync2 { -// let factory = DatabaseDriverFactory() -// var db: PowerSyncDatabase! -// -// func openDb() { -// db = PowerSyncDatabase(factory: factory, schema: schema, dbFilename: "powersync-swift.sqlite") -// } -// -// func watch (_ sql: String, parameters: [String: Any] = [:], mapper: @escaping (Cursor) throws -> T) async -> AnyPublisher<[T], Never> { -// } -// -// func execute(_ sql: String, parameters: [String: Any] = [:]) async throws { -// try await db.execute(sql: sql, parameters: parameters) -// } -// -// func writeTransaction(_ queryHandle: @escaping () async throws -> Any) async throws -> Any? { -// try await db.writeTransaction(callback: SuspendTaskWrapper(queryHandle)) -// } -// -// func readTransaction(_ queryHandle: @escaping () async throws -> Any) async throws -> Any? { -// try await db.readTransaction(callback: SuspendTaskWrapper(queryHandle)) -// } -// -// -// -// func insertList(_ list: NewListContent) async throws { -// try await self.db.execute( -// sql: "INSERT INTO \(LISTS_TABLE) (id, created_at, name, owner_id) VALUES (uuid(), datetime(), ?, ?)", -// parameters: [list.name, connector.currentUserID] -// ) -// } -// -// func deleteList(id: String) async throws { -// try await db.writeTransaction(callback: SuspendTaskWrapper { -// try await self.db.execute( -// sql: "DELETE FROM \(LISTS_TABLE) WHERE id = ?", -// parameters: [id] -// ) -// try await self.db.execute( -// sql: "DELETE FROM \(TODOS_TABLE) WHERE list_id = ?", -// parameters: [id] -// ) -// return -// }) -// } -// -// func watchTodos(_ listId: String, _ cb: @escaping (_ todos: [Todo]) -> Void ) async { -// for await todos in self.db.watch( -// sql: "SELECT * FROM \(TODOS_TABLE) WHERE list_id = ?", -// parameters: [listId], -// mapper: { cursor in -// return Todo( -// id: cursor.getString(index: 0)!, -// listId: cursor.getString(index: 1)!, -// photoId: cursor.getString(index: 2), -// description: cursor.getString(index: 3)!, -// isComplete: cursor.getBoolean(index: 4)! as! Bool, -// createdAt: cursor.getString(index: 5), -// completedAt: cursor.getString(index: 6), -// createdBy: cursor.getString(index: 7), -// completedBy: cursor.getString(index: 8) -// ) -// } -// ) { -// cb(todos as! [Todo]) -// } -// } -// -// func insertTodo(_ todo: NewTodo, _ listId: String) async throws { -// try await self.db.execute( -// sql: "INSERT INTO \(TODOS_TABLE) (id, created_at, created_by, description, list_id, completed) VALUES (uuid(), datetime(), ?, ?, ?, ?)", -// parameters: [connector.currentUserID, todo.description, listId, todo.isComplete] -// ) -// } -// -// func updateTodo(_ todo: Todo) async throws { -// // Do this to avoid needing to handle date time from Swift to Kotlin -// if(todo.isComplete) { -// try await self.db.execute( -// sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = datetime(), completed_by = ? WHERE id = ?", -// parameters: [todo.description, todo.isComplete, connector.currentUserID, todo.id] -// ) -// } else { -// try await self.db.execute( -// sql: "UPDATE \(TODOS_TABLE) SET description = ?, completed = ?, completed_at = NULL, completed_by = NULL WHERE id = ?", -// parameters: [todo.description, todo.isComplete, todo.id] -// ) -// } -// } -// -// func deleteTodo(id: String) async throws { -// try await db.writeTransaction(callback: SuspendTaskWrapper { -// try await self.db.execute( -// sql: "DELETE FROM \(TODOS_TABLE) WHERE id = ?", -// parameters: [id] -// ) -// return -// }) -// } -//} -// diff --git a/Sources/PowerSyncSwift/Schema/Column.swift b/Sources/PowerSyncSwift/Schema/Column.swift new file mode 100644 index 0000000..6ba1d20 --- /dev/null +++ b/Sources/PowerSyncSwift/Schema/Column.swift @@ -0,0 +1,38 @@ +import Foundation +import PowerSync + +public protocol ColumnProtocol: Equatable { + var name: String { get } + var type: ColumnData { get } +} + +public enum ColumnData { + case text + case integer + case real +} + +public struct Column: ColumnProtocol { + public let name: String + public let type: ColumnData + + public init( + name: String, + type: ColumnData + ) { + self.name = name + self.type = type + } + + public static func text(_ name: String) -> Column { + Column(name: name, type: .text) + } + + public static func integer(_ name: String) -> Column { + Column(name: name, type: .integer) + } + + public static func real(_ name: String) -> Column { + Column(name: name, type: .real) + } +} diff --git a/Sources/PowerSyncSwift/Schema/Index.swift b/Sources/PowerSyncSwift/Schema/Index.swift new file mode 100644 index 0000000..11bf75d --- /dev/null +++ b/Sources/PowerSyncSwift/Schema/Index.swift @@ -0,0 +1,44 @@ +import Foundation +import PowerSync + +public protocol IndexProtocol { + var name: String { get } + var columns: [IndexedColumnProtocol] { get } +} + +public struct Index: IndexProtocol { + public let name: String + public let columns: [IndexedColumnProtocol] + + public init( + name: String, + columns: [IndexedColumnProtocol] + ) { + self.name = name + self.columns = columns + } + + public init( + name: String, + _ columns: IndexedColumnProtocol... + ) { + self.init(name: name, columns: columns) + } + + public static func ascending( + name: String, + columns: [String] + ) -> Index { + return Index( + name: name, + columns: columns.map { IndexedColumn.ascending($0) } + ) + } + + public static func ascending( + name: String, + column: String + ) -> Index { + return ascending(name: name, columns: [column]) + } +} diff --git a/Sources/PowerSyncSwift/Schema/IndexedColumn.swift b/Sources/PowerSyncSwift/Schema/IndexedColumn.swift new file mode 100644 index 0000000..8d5bd17 --- /dev/null +++ b/Sources/PowerSyncSwift/Schema/IndexedColumn.swift @@ -0,0 +1,27 @@ +import Foundation + +public protocol IndexedColumnProtocol { + var column: String { get } + var ascending: Bool { get } +} + +public struct IndexedColumn: IndexedColumnProtocol { + public let column: String + public let ascending: Bool + + public init( + column: String, + ascending: Bool = true + ) { + self.column = column + self.ascending = ascending + } + + public static func ascending(_ column: String) -> IndexedColumn { + IndexedColumn(column: column, ascending: true) + } + + public static func descending(_ column: String) -> IndexedColumn { + IndexedColumn(column: column, ascending: false) + } +} diff --git a/Sources/PowerSyncSwift/Schema/Schema.swift b/Sources/PowerSyncSwift/Schema/Schema.swift new file mode 100644 index 0000000..7ac4597 --- /dev/null +++ b/Sources/PowerSyncSwift/Schema/Schema.swift @@ -0,0 +1,33 @@ +public protocol SchemaProtocol { + var tables: [Table] { get } + func validate() throws +} + +public struct Schema: SchemaProtocol { + public let tables: [Table] + + public init(tables: [Table]) { + self.tables = tables + } + + // Convenience initializer with variadic parameters + public init(_ tables: Table...) { + self.init(tables: tables) + } + + public func validate() throws { + var tableNames = Set() + + for table in tables { + if !tableNames.insert(table.name).inserted { + throw SchemaError.duplicateTableName(table.name) + } + try table.validate() + } + } +} + +public enum SchemaError: Error { + case duplicateTableName(String) +} + diff --git a/Sources/PowerSyncSwift/Schema/Table.swift b/Sources/PowerSyncSwift/Schema/Table.swift new file mode 100644 index 0000000..a6bddef --- /dev/null +++ b/Sources/PowerSyncSwift/Schema/Table.swift @@ -0,0 +1,135 @@ +import Foundation + +public protocol TableProtocol { + var name: String { get } + var columns: [Column] { get } + var indexes: [Index] { get } + var localOnly: Bool { get } + var insertOnly: Bool { get } + var viewNameOverride: String? { get } + var viewName: String { get } +} + +private let MAX_AMOUNT_OF_COLUMNS = 63 + +public struct Table: TableProtocol { + public let name: String + public let columns: [Column] + public let indexes: [Index] + public let localOnly: Bool + public let insertOnly: Bool + public let viewNameOverride: String? + + public var viewName: String { + viewNameOverride ?? name + } + + internal var internalName: String { + localOnly ? "ps_data_local__\(name)" : "ps_data__\(name)" + } + + private let invalidSqliteCharacters = try! NSRegularExpression( + pattern: #"["'%,.#\s\[\]]"#, + options: [] + ) + + public init( + name: String, + columns: [Column], + indexes: [Index] = [], + localOnly: Bool = false, + insertOnly: Bool = false, + viewNameOverride: String? = nil + ) { + self.name = name + self.columns = columns + self.indexes = indexes + self.localOnly = localOnly + self.insertOnly = insertOnly + self.viewNameOverride = viewNameOverride + } + + private func hasInvalidSqliteCharacters(_ string: String) -> Bool { + let range = NSRange(location: 0, length: string.utf16.count) + return invalidSqliteCharacters.firstMatch(in: string, options: [], range: range) != nil + } + + public func validate() throws { + if columns.count > MAX_AMOUNT_OF_COLUMNS { + throw TableError.tooManyColumns(tableName: name, count: columns.count) + } + + if let viewNameOverride = viewNameOverride, + hasInvalidSqliteCharacters(viewNameOverride) { + throw TableError.invalidViewName(viewName: viewNameOverride) + } + + var columnNames = Set(["id"]) + + for column in columns { + if column.name == "id" { + throw TableError.customIdColumn(tableName: name) + } + + if columnNames.contains(column.name) { + throw TableError.duplicateColumn( + tableName: name, + columnName: column.name + ) + } + + if hasInvalidSqliteCharacters(column.name) { + throw TableError.invalidColumnName( + tableName: name, + columnName: column.name + ) + } + + columnNames.insert(column.name) + } + + // Check indexes + var indexNames = Set() + + for index in indexes { + if indexNames.contains(index.name) { + throw TableError.duplicateIndex( + tableName: name, + indexName: index.name + ) + } + + if hasInvalidSqliteCharacters(index.name) { + throw TableError.invalidIndexName( + tableName: name, + indexName: index.name + ) + } + + // Check index columns exist in table + for indexColumn in index.columns { + if !columnNames.contains(indexColumn.column) { + throw TableError.columnNotFound( + tableName: name, + columnName: indexColumn.column, + indexName: index.name + ) + } + } + + indexNames.insert(index.name) + } + } +} + +public enum TableError: Error { + case tooManyColumns(tableName: String, count: Int) + case invalidTableName(tableName: String) + case invalidViewName(viewName: String) + case invalidColumnName(tableName: String, columnName: String) + case duplicateColumn(tableName: String, columnName: String) + case customIdColumn(tableName: String) + case duplicateIndex(tableName: String, indexName: String) + case invalidIndexName(tableName: String, indexName: String) + case columnNotFound(tableName: String, columnName: String, indexName: String) +} diff --git a/Tests/PowerSyncSwiftTests/PowerSyncSwiftTests.swift b/Tests/PowerSyncSwiftTests/PowerSyncSwiftTests.swift deleted file mode 100644 index 05accdb..0000000 --- a/Tests/PowerSyncSwiftTests/PowerSyncSwiftTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Testing -@testable import PowerSyncSwift - -@Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. -} diff --git a/Tests/PowerSyncSwiftTests/Schema/ColumnTests.swift b/Tests/PowerSyncSwiftTests/Schema/ColumnTests.swift new file mode 100644 index 0000000..6f4c3cc --- /dev/null +++ b/Tests/PowerSyncSwiftTests/Schema/ColumnTests.swift @@ -0,0 +1,66 @@ +import XCTest +@testable import PowerSyncSwift + +final class ColumnTests: XCTestCase { + + func testColumnInitialization() { + let name = "testColumn" + let type = ColumnData.text + + let column = Column(name: name, type: type) + + XCTAssertEqual(column.name, name) + XCTAssertEqual(column.type, type) + } + + func testTextColumnFactory() { + let name = "textColumn" + let column = Column.text(name) + + XCTAssertEqual(column.name, name) + XCTAssertEqual(column.type, .text) + } + + func testIntegerColumnFactory() { + let name = "intColumn" + let column = Column.integer(name) + + XCTAssertEqual(column.name, name) + XCTAssertEqual(column.type, .integer) + } + + func testRealColumnFactory() { + let name = "realColumn" + let column = Column.real(name) + + XCTAssertEqual(column.name, name) + XCTAssertEqual(column.type, .real) + } + + func testEmptyColumnName() { + let column = Column(name: "", type: .text) + XCTAssertEqual(column.name, "") + } + + func testColumnDataTypeEquality() { + XCTAssertEqual(ColumnData.text, ColumnData.text) + XCTAssertEqual(ColumnData.integer, ColumnData.integer) + XCTAssertEqual(ColumnData.real, ColumnData.real) + + XCTAssertNotEqual(ColumnData.text, ColumnData.integer) + XCTAssertNotEqual(ColumnData.text, ColumnData.real) + XCTAssertNotEqual(ColumnData.integer, ColumnData.real) + } + + func testMultipleColumnCreation() { + let columns = [ + Column.text("name"), + Column.integer("age"), + Column.real("score") + ] + + XCTAssertEqual(columns[0].type, .text) + XCTAssertEqual(columns[1].type, .integer) + XCTAssertEqual(columns[2].type, .real) + } +} diff --git a/Tests/PowerSyncSwiftTests/Schema/IndexTests.swift b/Tests/PowerSyncSwiftTests/Schema/IndexTests.swift new file mode 100644 index 0000000..3f1c377 --- /dev/null +++ b/Tests/PowerSyncSwiftTests/Schema/IndexTests.swift @@ -0,0 +1,95 @@ +import XCTest +@testable import PowerSyncSwift + +final class IndexTests: XCTestCase { + + private func makeIndexedColumn(_ name: String) -> IndexedColumnProtocol { + return IndexedColumn.ascending(name) + } + + func testBasicInitialization() { + let name = "test_index" + let columns: [IndexedColumnProtocol] = [ + makeIndexedColumn("column1"), + makeIndexedColumn("column2") + ] + + let index = Index(name: name, columns: columns) + + XCTAssertEqual(index.name, name) + XCTAssertEqual(index.columns.count, 2) + XCTAssertEqual((index.columns[0] as? IndexedColumn)?.column, "column1") + XCTAssertEqual((index.columns[1] as? IndexedColumn)?.column, "column2") + } + + func testVariadicInitialization() { + let name = "test_index" + let column1 = makeIndexedColumn("column1") + let column2 = makeIndexedColumn("column2") + + let index = Index(name: name, column1, column2) + + XCTAssertEqual(index.name, name) + XCTAssertEqual(index.columns.count, 2) + XCTAssertEqual((index.columns[0]).column, "column1") + XCTAssertEqual((index.columns[1]).column, "column2") + } + + func testAscendingFactoryWithMultipleColumns() { + let name = "test_index" + let columnNames = ["column1", "column2", "column3"] + + let index = Index.ascending(name: name, columns: columnNames) + + XCTAssertEqual(index.name, name) + XCTAssertEqual(index.columns.count, 3) + + // Verify each column is correctly created + for (i, columnName) in columnNames.enumerated() { + let indexedColumn = index.columns[i] + XCTAssertEqual(indexedColumn.column, columnName) + XCTAssertTrue(indexedColumn.ascending) + } + } + + func testAscendingFactoryWithSingleColumn() { + let name = "test_index" + let columnName = "column1" + + let index = Index.ascending(name: name, column: columnName) + + XCTAssertEqual(index.name, name) + XCTAssertEqual(index.columns.count, 1) + + let indexedColumn = index.columns[0] + XCTAssertEqual(indexedColumn.column, columnName) + XCTAssertTrue(indexedColumn.ascending) + } + + func testMixedColumnTypes() { + let name = "mixed_index" + let columns: [IndexedColumnProtocol] = [ + IndexedColumn.ascending("column1"), + IndexedColumn.descending("column2"), + IndexedColumn.ascending("column3") + ] + + let index = Index(name: name, columns: columns) + + XCTAssertEqual(index.name, name) + XCTAssertEqual(index.columns.count, 3) + + let col1 = index.columns[0] + let col2 = index.columns[1] + let col3 = index.columns[2] + + XCTAssertEqual(col1.column, "column1") + XCTAssertTrue(col1.ascending) + + XCTAssertEqual(col2.column, "column2") + XCTAssertFalse(col2.ascending) + + XCTAssertEqual(col3.column, "column3") + XCTAssertTrue(col3.ascending) + } +} diff --git a/Tests/PowerSyncSwiftTests/Schema/IndexedColumnTests.swift b/Tests/PowerSyncSwiftTests/Schema/IndexedColumnTests.swift new file mode 100644 index 0000000..43a355b --- /dev/null +++ b/Tests/PowerSyncSwiftTests/Schema/IndexedColumnTests.swift @@ -0,0 +1,65 @@ +import XCTest +@testable import PowerSyncSwift + +final class IndexedColumnTests: XCTestCase { + + func testBasicInitialization() { + let column = IndexedColumn(column: "test", ascending: true) + + XCTAssertEqual(column.column, "test") + XCTAssertTrue(column.ascending) + } + + func testDefaultAscendingValue() { + let column = IndexedColumn(column: "test") + XCTAssertTrue(column.ascending) + } + + func testDescendingInitialization() { + let column = IndexedColumn(column: "test", ascending: false) + + XCTAssertEqual(column.column, "test") + XCTAssertFalse(column.ascending) + } + + func testIgnoresOptionalParameters() { + let column = IndexedColumn( + column: "test", + ascending: true + ) + + XCTAssertEqual(column.column, "test") + XCTAssertTrue(column.ascending) + } + + func testAscendingFactory() { + let column = IndexedColumn.ascending("test") + + XCTAssertEqual(column.column, "test") + XCTAssertTrue(column.ascending) + } + + func testDescendingFactory() { + let column = IndexedColumn.descending("test") + + XCTAssertEqual(column.column, "test") + XCTAssertFalse(column.ascending) + } + + func testMultipleInstances() { + let columns = [ + IndexedColumn.ascending("first"), + IndexedColumn.descending("second"), + IndexedColumn(column: "third") + ] + + XCTAssertEqual(columns[0].column, "first") + XCTAssertTrue(columns[0].ascending) + + XCTAssertEqual(columns[1].column, "second") + XCTAssertFalse(columns[1].ascending) + + XCTAssertEqual(columns[2].column, "third") + XCTAssertTrue(columns[2].ascending) + } +} diff --git a/Tests/PowerSyncSwiftTests/Schema/SchemaTests.swift b/Tests/PowerSyncSwiftTests/Schema/SchemaTests.swift new file mode 100644 index 0000000..eb8fecf --- /dev/null +++ b/Tests/PowerSyncSwiftTests/Schema/SchemaTests.swift @@ -0,0 +1,110 @@ +import XCTest +@testable import PowerSyncSwift + +final class SchemaTests: XCTestCase { + private func makeValidTable(name: String) -> Table { + return Table( + name: name, + columns: [ + Column.text("name"), + Column.integer("age") + ] + ) + } + + private func makeInvalidTable() -> Table { + // Table with invalid column name + return Table( + name: "test", + columns: [ + Column.text("invalid name") + ] + ) + } + + func testArrayInitialization() { + let tables = [ + makeValidTable(name: "users"), + makeValidTable(name: "posts") + ] + + let schema = Schema(tables: tables) + + XCTAssertEqual(schema.tables.count, 2) + XCTAssertEqual(schema.tables[0].name, "users") + XCTAssertEqual(schema.tables[1].name, "posts") + } + + func testVariadicInitialization() { + let schema = Schema( + makeValidTable(name: "users"), + makeValidTable(name: "posts") + ) + + XCTAssertEqual(schema.tables.count, 2) + XCTAssertEqual(schema.tables[0].name, "users") + XCTAssertEqual(schema.tables[1].name, "posts") + } + + func testEmptySchemaInitialization() { + let schema = Schema(tables: []) + XCTAssertTrue(schema.tables.isEmpty) + XCTAssertNoThrow(try schema.validate()) + } + + func testDuplicateTableValidation() { + let schema = Schema( + makeValidTable(name: "users"), + makeValidTable(name: "users") + ) + + XCTAssertThrowsError(try schema.validate()) { error in + guard case SchemaError.duplicateTableName(let tableName) = error else { + XCTFail("Expected duplicateTableName error") + return + } + XCTAssertEqual(tableName, "users") + } + } + + func testCascadingTableValidation() { + let schema = Schema( + makeValidTable(name: "users"), + makeInvalidTable() + ) + + XCTAssertThrowsError(try schema.validate()) { error in + // The error should be from the Table validation + guard case TableError.invalidColumnName = error else { + XCTFail("Expected invalidColumnName error from Table validation") + return + } + } + } + + func testValidSchemaValidation() { + let schema = Schema( + makeValidTable(name: "users"), + makeValidTable(name: "posts"), + makeValidTable(name: "comments") + ) + + XCTAssertNoThrow(try schema.validate()) + } + + func testSingleTableSchema() { + let schema = Schema(makeValidTable(name: "users")) + XCTAssertEqual(schema.tables.count, 1) + XCTAssertNoThrow(try schema.validate()) + } + + func testTableAccess() { + let users = makeValidTable(name: "users") + let posts = makeValidTable(name: "posts") + + let schema = Schema(users, posts) + + XCTAssertEqual(schema.tables[0].name, users.name) + XCTAssertEqual(schema.tables[1].name, posts.name) + } +} diff --git a/Tests/PowerSyncSwiftTests/Schema/TableTests.swift b/Tests/PowerSyncSwiftTests/Schema/TableTests.swift new file mode 100644 index 0000000..af9bcd5 --- /dev/null +++ b/Tests/PowerSyncSwiftTests/Schema/TableTests.swift @@ -0,0 +1,204 @@ +import XCTest +@testable import PowerSyncSwift + +final class TableTests: XCTestCase { + + private func makeValidColumns() -> [Column] { + return [ + Column.text("name"), + Column.integer("age"), + Column.real("score") + ] + } + + private func makeValidIndex() -> Index { + return Index(name: "test_index", columns: [ + IndexedColumn(column: "name") + ]) + } + + func testBasicInitialization() { + let name = "users" + let columns = makeValidColumns() + let indexes = [makeValidIndex()] + + let table = Table( + name: name, + columns: columns, + indexes: indexes, + localOnly: true, + insertOnly: true, + viewNameOverride: "user_view" + ) + + XCTAssertEqual(table.name, name) + XCTAssertEqual(table.columns, columns) + XCTAssertEqual(table.indexes.count, indexes.count) + XCTAssertTrue(table.localOnly) + XCTAssertTrue(table.insertOnly) + XCTAssertEqual(table.viewNameOverride, "user_view") + } + + func testViewName() { + let table1 = Table(name: "users", columns: makeValidColumns()) + XCTAssertEqual(table1.viewName, "users") + + let table2 = Table(name: "users", columns: makeValidColumns(), viewNameOverride: "custom_view") + XCTAssertEqual(table2.viewName, "custom_view") + } + + func testInternalName() { + let localTable = Table(name: "users", columns: makeValidColumns(), localOnly: true) + XCTAssertEqual(localTable.internalName, "ps_data_local__users") + + let globalTable = Table(name: "users", columns: makeValidColumns(), localOnly: false) + XCTAssertEqual(globalTable.internalName, "ps_data__users") + } + + func testTooManyColumnsValidation() throws { + var manyColumns: [Column] = [] + for i in 0..<64 { + manyColumns.append(Column.text("column\(i)")) + } + + let table = Table(name: "test", columns: manyColumns) + + XCTAssertThrowsError(try table.validate()) { error in + guard case TableError.tooManyColumns(let tableName, let count) = error else { + XCTFail("Expected tooManyColumns error") + return + } + XCTAssertEqual(tableName, "test") + XCTAssertEqual(count, 64) + } + } + + func testInvalidViewNameValidation() { + let table = Table( + name: "test", + columns: makeValidColumns(), + viewNameOverride: "invalid name" + ) + + XCTAssertThrowsError(try table.validate()) { error in + guard case TableError.invalidViewName(let viewName) = error else { + XCTFail("Expected invalidViewName error") + return + } + XCTAssertEqual(viewName, "invalid name") + } + } + + func testCustomIdColumnValidation() { + let columns = [Column.text("id")] + let table = Table(name: "test", columns: columns) + + XCTAssertThrowsError(try table.validate()) { error in + guard case TableError.customIdColumn(let tableName) = error else { + XCTFail("Expected customIdColumn error") + return + } + XCTAssertEqual(tableName, "test") + } + } + + func testDuplicateColumnValidation() { + let columns = [ + Column.text("name"), + Column.text("name") + ] + let table = Table(name: "test", columns: columns) + + XCTAssertThrowsError(try table.validate()) { error in + guard case TableError.duplicateColumn(let tableName, let columnName) = error else { + XCTFail("Expected duplicateColumn error") + return + } + XCTAssertEqual(tableName, "test") + XCTAssertEqual(columnName, "name") + } + } + + func testInvalidColumnNameValidation() { + let columns = [Column.text("invalid name")] + let table = Table(name: "test", columns: columns) + + XCTAssertThrowsError(try table.validate()) { error in + guard case TableError.invalidColumnName(let tableName, let columnName) = error else { + XCTFail("Expected invalidColumnName error") + return + } + XCTAssertEqual(tableName, "test") + XCTAssertEqual(columnName, "invalid name") + } + } + + // MARK: - Index Validation Tests + + func testDuplicateIndexValidation() { + let index = Index(name: "test_index", columns: [IndexedColumn(column: "name")]) + let table = Table( + name: "test", + columns: [Column.text("name")], + indexes: [index, index] + ) + + XCTAssertThrowsError(try table.validate()) { error in + guard case TableError.duplicateIndex(let tableName, let indexName) = error else { + XCTFail("Expected duplicateIndex error") + return + } + XCTAssertEqual(tableName, "test") + XCTAssertEqual(indexName, "test_index") + } + } + + func testInvalidIndexNameValidation() { + let index = Index(name: "invalid index", columns: [IndexedColumn(column: "name")]) + let table = Table( + name: "test", + columns: [Column.text("name")], + indexes: [index] + ) + + XCTAssertThrowsError(try table.validate()) { error in + guard case TableError.invalidIndexName(let tableName, let indexName) = error else { + XCTFail("Expected invalidIndexName error") + return + } + XCTAssertEqual(tableName, "test") + XCTAssertEqual(indexName, "invalid index") + } + } + + func testColumnNotFoundInIndexValidation() { + let index = Index(name: "test_index", columns: [IndexedColumn(column: "nonexistent")]) + let table = Table( + name: "test", + columns: [Column.text("name")], + indexes: [index] + ) + + XCTAssertThrowsError(try table.validate()) { error in + guard case TableError.columnNotFound(let tableName, let columnName, let indexName) = error else { + XCTFail("Expected columnNotFound error") + return + } + XCTAssertEqual(tableName, "test") + XCTAssertEqual(columnName, "nonexistent") + XCTAssertEqual(indexName, "test_index") + } + } + + func testValidTableValidation() throws { + let table = Table( + name: "users", + columns: makeValidColumns(), + indexes: [makeValidIndex()], + localOnly: false, + insertOnly: false + ) + + XCTAssertNoThrow(try table.validate()) + } +} From 13f75f5c660e588a6df4fbce50d1743c591d046a Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Fri, 25 Oct 2024 13:13:11 +0200 Subject: [PATCH 04/11] chore: add tests --- .github/workflows/build_and_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 1f395d7..eb31dff 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -9,7 +9,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - swift: ["5.7", "6.0"] + swift: ["5.10.1"] runs-on: ${{ matrix.os }} steps: - uses: swift-actions/setup-swift@v2 From 68b2f9bbe40ad8c5c9c28962bb59e826992564db Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Fri, 25 Oct 2024 13:18:08 +0200 Subject: [PATCH 05/11] chore: add tests --- .github/workflows/build_and_test.yaml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index eb31dff..fe8c60d 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -5,16 +5,9 @@ on: jobs: build: - name: Swift ${{ matrix.swift }} on ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - swift: ["5.10.1"] - runs-on: ${{ matrix.os }} + name: Build and test + runs-on: macos-latest steps: - - uses: swift-actions/setup-swift@v2 - with: - swift-version: ${{ matrix.swift }} - uses: actions/checkout@v4 - name: Build run: swift build From 28205e3345bd8d2c7f1edfb54e319d8d9eccba76 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Fri, 25 Oct 2024 14:10:28 +0200 Subject: [PATCH 06/11] fix: ci issue --- .github/workflows/build_and_test.yaml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index fe8c60d..bc464dd 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -10,6 +10,12 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build - run: swift build - - name: Run tests - run: swift test + run: | + xcodebuild build \ + -scheme PowerSyncSwift \ + -destination "platform=iOS Simulator,name=iPhone 16" + - name: Test + run: | + xcodebuild test \ + -scheme PowerSyncSwift \ + -destination "platform=iOS Simulator,name=iPhone 16" From e64a87b2bab1be583b4d04dc5c08e61cef187096 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Fri, 25 Oct 2024 14:13:11 +0200 Subject: [PATCH 07/11] fix: ci issue --- .github/workflows/build_and_test.yaml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index bc464dd..3db40b3 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -9,13 +9,6 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v4 - - name: Build + - name: Build and Test run: | - xcodebuild build \ - -scheme PowerSyncSwift \ - -destination "platform=iOS Simulator,name=iPhone 16" - - name: Test - run: | - xcodebuild test \ - -scheme PowerSyncSwift \ - -destination "platform=iOS Simulator,name=iPhone 16" + xcodebuild test -scheme PowerSyncSwift -destination "platform=iOS Simulator,name=iPhone 16" From 1bde9df03f672f44b007139e9eaef3708a68a014 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Fri, 25 Oct 2024 14:15:24 +0200 Subject: [PATCH 08/11] fix: ci issue --- .github/workflows/build_and_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 3db40b3..5271290 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -11,4 +11,4 @@ jobs: - uses: actions/checkout@v4 - name: Build and Test run: | - xcodebuild test -scheme PowerSyncSwift -destination "platform=iOS Simulator,name=iPhone 16" + xcodebuild test -scheme PowerSyncSwift -destination "platform=iOS Simulator,name=iPhone 15" From fe2d72eab911297e6124c48423964a5199127626 Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Fri, 25 Oct 2024 15:11:57 +0200 Subject: [PATCH 09/11] docs: README --- Demo/README.md | 8 -------- README.md | 10 +++++++++- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Demo/README.md b/Demo/README.md index 8427629..70d24e4 100644 --- a/Demo/README.md +++ b/Demo/README.md @@ -2,14 +2,6 @@ A Todo List app demonstrating the use of the PowerSync Swift SDK together with Supabase. -The PowerSync Swift SDK is an extension of the [PowerSync Kotlin Multiplatform SDK](https://github.com/powersync-ja/powersync-kotlin), and uses the API tool [SKIE](https://skie.touchlab.co/) and KMMBridge to generate and publish a native Swift SDK. More details about this configuration can be found in our blog [here](https://www.powersync.com/blog/using-kotlin-multiplatform-with-kmmbridge-and-skie-to-publish-a-native-swift-sdk). - -The SDK reference for the PowerSync Swift SDK is available [here](https://docs.powersync.com/client-sdk-references/swift). - -## Alpha Release - -This SDK is currently in an alpha release and not suitable for production use, unless you have tested your use case(s) extensively. Breaking changes are still likely to occur. - ## Set up your Supabase and PowerSync projects To run this demo, you need Supabase and PowerSync projects. Detailed instructions for integrating PowerSync with Supabase can be found in [the integration guide](https://docs.powersync.com/integration-guides/supabase). diff --git a/README.md b/README.md index adc739e..85a62ba 100644 --- a/README.md +++ b/README.md @@ -1 +1,9 @@ -# powersync-swift \ No newline at end of file +# PowerSync Swift + +The PowerSync Swift SDK is an extension of the [PowerSync Kotlin Multiplatform SDK](https://github.com/powersync-ja/powersync-kotlin), and uses the API tool [SKIE](https://skie.touchlab.co/) and KMMBridge to generate and publish a native Swift SDK. More details about this configuration can be found in our blog [here](https://www.powersync.com/blog/using-kotlin-multiplatform-with-kmmbridge-and-skie-to-publish-a-native-swift-sdk). + +The SDK reference for the PowerSync Swift SDK is available [here](https://docs.powersync.com/client-sdk-references/swift). + +## Alpha Release + +This SDK is currently in an alpha release and not suitable for production use, unless you have tested your use case(s) extensively. Breaking changes are still likely to occur. From c3a76a85f5743ead9aeb3f6905c0978658de219a Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Thu, 31 Oct 2024 16:48:42 +0200 Subject: [PATCH 10/11] chore: remove secrets --- Demo/PowerSyncExample/_Secrets.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Demo/PowerSyncExample/_Secrets.swift b/Demo/PowerSyncExample/_Secrets.swift index 82ea2d8..8c3d86e 100644 --- a/Demo/PowerSyncExample/_Secrets.swift +++ b/Demo/PowerSyncExample/_Secrets.swift @@ -2,7 +2,7 @@ import Foundation // Enter your Supabase and PowerSync project details. enum Secrets { - static let powerSyncEndpoint = "https://654a826e1b70717c8dc85790.powersync.journeyapps.com" - static let supabaseURL = URL(string: "https://whlrfrfknkhckmffonio.supabase.co")! - static let supabaseAnonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndobHJmcmZrbmtoY2ttZmZvbmlvIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTkzODE0ODYsImV4cCI6MjAxNDk1NzQ4Nn0.gUTu3LDFetyda4Hd2gZLtv9o8pvCxEjDjm6DCVjrehw" + static let powerSyncEndpoint = "https://your-id.powersync.journeyapps.com" + static let supabaseURL = URL(string: "https://your-id.supabase.co")! + static let supabaseAnonKey = "anon-key" } From b74c7dfdad15169d0e3699e3539a2dafe7b052ea Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Mon, 4 Nov 2024 12:52:01 +0200 Subject: [PATCH 11/11] chore: change name --- Demo/PowerSyncExample/Components/AddListView.swift | 6 +++--- Demo/PowerSyncExample/Components/AddTodoListView.swift | 6 +++--- Demo/PowerSyncExample/Components/ListView.swift | 8 ++++---- Demo/PowerSyncExample/Components/TodoListView.swift | 10 +++++----- .../{PowerSyncManager.swift => SystemManager.swift} | 3 +-- Demo/PowerSyncExample/PowerSyncExampleApp.swift | 2 +- Demo/PowerSyncExample/RootView.swift | 8 ++++---- Demo/PowerSyncExample/Screens/HomeScreen.swift | 10 +++++----- Demo/PowerSyncExample/Screens/SignInScreen.swift | 6 +++--- Demo/PowerSyncExample/Screens/SignUpScreen.swift | 6 +++--- 10 files changed, 32 insertions(+), 33 deletions(-) rename Demo/PowerSyncExample/PowerSync/{PowerSyncManager.swift => SystemManager.swift} (99%) diff --git a/Demo/PowerSyncExample/Components/AddListView.swift b/Demo/PowerSyncExample/Components/AddListView.swift index 5792ab4..ed125d7 100644 --- a/Demo/PowerSyncExample/Components/AddListView.swift +++ b/Demo/PowerSyncExample/Components/AddListView.swift @@ -1,7 +1,7 @@ import SwiftUI struct AddListView: View { - @Environment(PowerSyncManager.self) private var powerSync + @Environment(SystemManager.self) private var system @Binding var newList: NewListContent let completion: (Result) -> Void @@ -12,7 +12,7 @@ struct AddListView: View { Button("Save") { Task.detached { do { - try await powerSync.insertList(newList) + try await system.insertList(newList) await completion(.success(true)) } catch { await completion(.failure(error)) @@ -34,5 +34,5 @@ struct AddListView: View { ) ) ) { _ in - }.environment(PowerSyncManager()) + }.environment(SystemManager()) } diff --git a/Demo/PowerSyncExample/Components/AddTodoListView.swift b/Demo/PowerSyncExample/Components/AddTodoListView.swift index 615efae..b8eb9d1 100644 --- a/Demo/PowerSyncExample/Components/AddTodoListView.swift +++ b/Demo/PowerSyncExample/Components/AddTodoListView.swift @@ -2,7 +2,7 @@ import Foundation import SwiftUI struct AddTodoListView: View { - @Environment(PowerSyncManager.self) private var powerSync + @Environment(SystemManager.self) private var system @Binding var newTodo: NewTodo let listId: String @@ -14,7 +14,7 @@ struct AddTodoListView: View { Button("Save") { Task.detached { do { - try await powerSync.insertTodo(newTodo, listId) + try await system.insertTodo(newTodo, listId) await completion(.success(true)) } catch { await completion(.failure(error)) @@ -37,5 +37,5 @@ struct AddTodoListView: View { ), listId: UUID().uuidString.lowercased() ){ _ in - }.environment(PowerSyncManager()) + }.environment(SystemManager()) } diff --git a/Demo/PowerSyncExample/Components/ListView.swift b/Demo/PowerSyncExample/Components/ListView.swift index e2fcc50..3f1c4a3 100644 --- a/Demo/PowerSyncExample/Components/ListView.swift +++ b/Demo/PowerSyncExample/Components/ListView.swift @@ -3,7 +3,7 @@ import IdentifiedCollections import SwiftUINavigation struct ListView: View { - @Environment(PowerSyncManager.self) private var powerSync + @Environment(SystemManager.self) private var system @State private var lists: IdentifiedArrayOf = [] @State private var error: Error? @@ -64,7 +64,7 @@ struct ListView: View { } .task { Task { - await powerSync.watchLists { ls in + await system.watchLists { ls in withAnimation { self.lists = IdentifiedArrayOf(uniqueElements: ls) } @@ -78,7 +78,7 @@ struct ListView: View { error = nil let listsToDelete = offset.map { lists[$0] } - try await powerSync.deleteList(id: listsToDelete[0].id) + try await system.deleteList(id: listsToDelete[0].id) } catch { self.error = error @@ -89,6 +89,6 @@ struct ListView: View { #Preview { NavigationStack { ListView() - .environment(PowerSyncManager()) + .environment(SystemManager()) } } diff --git a/Demo/PowerSyncExample/Components/TodoListView.swift b/Demo/PowerSyncExample/Components/TodoListView.swift index dc9d7ec..e92dfdf 100644 --- a/Demo/PowerSyncExample/Components/TodoListView.swift +++ b/Demo/PowerSyncExample/Components/TodoListView.swift @@ -3,7 +3,7 @@ import IdentifiedCollections import SwiftUINavigation struct TodoListView: View { - @Environment(PowerSyncManager.self) private var powerSync + @Environment(SystemManager.self) private var system let listId: String @State private var todos: IdentifiedArrayOf = [] @@ -65,7 +65,7 @@ struct TodoListView: View { } .task { Task { - await powerSync.watchTodos(listId) { tds in + await system.watchTodos(listId) { tds in withAnimation { self.todos = IdentifiedArrayOf(uniqueElements: tds) } @@ -79,7 +79,7 @@ struct TodoListView: View { updatedTodo.isComplete.toggle() do { error = nil - try await powerSync.updateTodo(updatedTodo) + try await system.updateTodo(updatedTodo) } catch { self.error = error } @@ -90,7 +90,7 @@ struct TodoListView: View { error = nil let todosToDelete = offset.map { todos[$0] } - try await powerSync.deleteTodo(id: todosToDelete[0].id) + try await system.deleteTodo(id: todosToDelete[0].id) } catch { self.error = error @@ -102,6 +102,6 @@ struct TodoListView: View { NavigationStack { TodoListView( listId: UUID().uuidString.lowercased() - ).environment(PowerSyncManager()) + ).environment(SystemManager()) } } diff --git a/Demo/PowerSyncExample/PowerSync/PowerSyncManager.swift b/Demo/PowerSyncExample/PowerSync/SystemManager.swift similarity index 99% rename from Demo/PowerSyncExample/PowerSync/PowerSyncManager.swift rename to Demo/PowerSyncExample/PowerSync/SystemManager.swift index 1253742..8479008 100644 --- a/Demo/PowerSyncExample/PowerSync/PowerSyncManager.swift +++ b/Demo/PowerSyncExample/PowerSync/SystemManager.swift @@ -2,8 +2,7 @@ import Foundation import PowerSyncSwift @Observable -@MainActor -class PowerSyncManager { +class SystemManager { let connector = SupabaseConnector() let schema = AppSchema var db: PowerSyncDatabaseProtocol! diff --git a/Demo/PowerSyncExample/PowerSyncExampleApp.swift b/Demo/PowerSyncExample/PowerSyncExampleApp.swift index 309f305..5113dd8 100644 --- a/Demo/PowerSyncExample/PowerSyncExampleApp.swift +++ b/Demo/PowerSyncExample/PowerSyncExampleApp.swift @@ -6,7 +6,7 @@ struct PowerSyncExampleApp: App { var body: some Scene { WindowGroup { RootView() - .environment(PowerSyncManager()) + .environment(SystemManager()) } } } diff --git a/Demo/PowerSyncExample/RootView.swift b/Demo/PowerSyncExample/RootView.swift index 50104e7..9450aa1 100644 --- a/Demo/PowerSyncExample/RootView.swift +++ b/Demo/PowerSyncExample/RootView.swift @@ -2,7 +2,7 @@ import Auth import SwiftUI struct RootView: View { - @Environment(PowerSyncManager.self) var powerSync + @Environment(SystemManager.self) var system @State private var authModel = AuthModel() @State private var navigationModel = NavigationModel() @@ -28,8 +28,8 @@ struct RootView: View { } } .task { - if(powerSync.db == nil) { - powerSync.openDb() + if(system.db == nil) { + system.openDb() } } .environment(authModel) @@ -40,5 +40,5 @@ struct RootView: View { #Preview { RootView() - .environment(PowerSyncManager()) + .environment(SystemManager()) } diff --git a/Demo/PowerSyncExample/Screens/HomeScreen.swift b/Demo/PowerSyncExample/Screens/HomeScreen.swift index acf58c1..6a4da2f 100644 --- a/Demo/PowerSyncExample/Screens/HomeScreen.swift +++ b/Demo/PowerSyncExample/Screens/HomeScreen.swift @@ -3,7 +3,7 @@ import Auth import SwiftUI struct HomeScreen: View { - @Environment(PowerSyncManager.self) private var powerSync + @Environment(SystemManager.self) private var system @Environment(AuthModel.self) private var authModel @Environment(NavigationModel.self) private var navigationModel @@ -15,7 +15,7 @@ struct HomeScreen: View { ToolbarItem(placement: .cancellationAction) { Button("Sign out") { Task { - try await powerSync.signOut() + try await system.signOut() authModel.isAuthenticated = false navigationModel.path = NavigationPath() } @@ -23,8 +23,8 @@ struct HomeScreen: View { } } .task { - if(powerSync.db.currentStatus.connected == false) { - await powerSync.connect() + if(system.db.currentStatus.connected == false) { + await system.connect() } } .navigationBarBackButtonHidden(true) @@ -34,6 +34,6 @@ struct HomeScreen: View { #Preview { NavigationStack{ HomeScreen() - .environment(PowerSyncManager()) + .environment(SystemManager()) } } diff --git a/Demo/PowerSyncExample/Screens/SignInScreen.swift b/Demo/PowerSyncExample/Screens/SignInScreen.swift index 8e119fe..40e7c95 100644 --- a/Demo/PowerSyncExample/Screens/SignInScreen.swift +++ b/Demo/PowerSyncExample/Screens/SignInScreen.swift @@ -7,7 +7,7 @@ private enum ActionState { } struct SignInScreen: View { - @Environment(PowerSyncManager.self) private var powerSync + @Environment(SystemManager.self) private var system @Environment(AuthModel.self) private var authModel @Environment(NavigationModel.self) private var navigationModel @@ -60,7 +60,7 @@ struct SignInScreen: View { private func signInButtonTapped() async { do { actionState = .inFlight - try await powerSync.connector.client.auth.signIn(email: email, password: password) + try await system.connector.client.auth.signIn(email: email, password: password) actionState = .result(.success(())) authModel.isAuthenticated = true navigationModel.path = NavigationPath() @@ -75,6 +75,6 @@ struct SignInScreen: View { #Preview { NavigationStack { SignInScreen() - .environment(PowerSyncManager()) + .environment(SystemManager()) } } diff --git a/Demo/PowerSyncExample/Screens/SignUpScreen.swift b/Demo/PowerSyncExample/Screens/SignUpScreen.swift index 04d8c44..bf9bb5e 100644 --- a/Demo/PowerSyncExample/Screens/SignUpScreen.swift +++ b/Demo/PowerSyncExample/Screens/SignUpScreen.swift @@ -7,7 +7,7 @@ private enum ActionState { } struct SignUpScreen: View { - @Environment(PowerSyncManager.self) private var powerSync + @Environment(SystemManager.self) private var system @Environment(AuthModel.self) private var authModel @Environment(NavigationModel.self) private var navigationModel @@ -56,7 +56,7 @@ struct SignUpScreen: View { private func signUpButtonTapped() async { do { actionState = .inFlight - try await powerSync.connector.client.auth.signUp( + try await system.connector.client.auth.signUp( email: email, password: password, redirectTo: Constants.redirectToURL @@ -75,6 +75,6 @@ struct SignUpScreen: View { #Preview { NavigationStack { SignUpScreen() - .environment(PowerSyncManager()) + .environment(SystemManager()) } }