diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml new file mode 100644 index 0000000..5271290 --- /dev/null +++ b/.github/workflows/build_and_test.yaml @@ -0,0 +1,14 @@ +name: Build and test + +on: + push + +jobs: + build: + name: Build and test + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Build and Test + run: | + xcodebuild test -scheme PowerSyncSwift -destination "platform=iOS Simulator,name=iPhone 15" 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..ed125d7 --- /dev/null +++ b/Demo/PowerSyncExample/Components/AddListView.swift @@ -0,0 +1,38 @@ +import SwiftUI + +struct AddListView: View { + @Environment(SystemManager.self) private var system + + @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 system.insertList(newList) + await completion(.success(true)) + } catch { + await completion(.failure(error)) + throw error + } + } + } + } + } +} + +#Preview { + AddListView( + newList: .constant( + .init( + name: "", + ownerId: "", + createdAt: "" + ) + ) + ) { _ in + }.environment(SystemManager()) +} diff --git a/Demo/PowerSyncExample/Components/AddTodoListView.swift b/Demo/PowerSyncExample/Components/AddTodoListView.swift new file mode 100644 index 0000000..b8eb9d1 --- /dev/null +++ b/Demo/PowerSyncExample/Components/AddTodoListView.swift @@ -0,0 +1,41 @@ +import Foundation +import SwiftUI + +struct AddTodoListView: View { + @Environment(SystemManager.self) private var system + + @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 system.insertTodo(newTodo, listId) + await completion(.success(true)) + } catch { + await completion(.failure(error)) + throw error + } + } + } + } + } +} + +#Preview { + AddTodoListView( + newTodo: .constant( + .init( + listId: UUID().uuidString.lowercased(), + isComplete: false, + description: "" + ) + ), + listId: UUID().uuidString.lowercased() + ){ _ in + }.environment(SystemManager()) +} 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..3f1c4a3 --- /dev/null +++ b/Demo/PowerSyncExample/Components/ListView.swift @@ -0,0 +1,94 @@ +import SwiftUI +import IdentifiedCollections +import SwiftUINavigation + +struct ListView: View { + @Environment(SystemManager.self) private var system + + @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 system.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 system.deleteList(id: listsToDelete[0].id) + + } catch { + self.error = error + } + } +} + +#Preview { + NavigationStack { + ListView() + .environment(SystemManager()) + } +} 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..e92dfdf --- /dev/null +++ b/Demo/PowerSyncExample/Components/TodoListView.swift @@ -0,0 +1,107 @@ +import SwiftUI +import IdentifiedCollections +import SwiftUINavigation + +struct TodoListView: View { + @Environment(SystemManager.self) private var system + 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 system.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 system.updateTodo(updatedTodo) + } catch { + self.error = error + } + } + + func delete(at offset: IndexSet) async { + do { + error = nil + let todosToDelete = offset.map { todos[$0] } + + try await system.deleteTodo(id: todosToDelete[0].id) + + } catch { + self.error = error + } + } +} + +#Preview { + NavigationStack { + TodoListView( + listId: UUID().uuidString.lowercased() + ).environment(SystemManager()) + } +} 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/Schema.swift b/Demo/PowerSyncExample/PowerSync/Schema.swift new file mode 100644 index 0000000..f49bc99 --- /dev/null +++ b/Demo/PowerSyncExample/PowerSync/Schema.swift @@ -0,0 +1,39 @@ +import Foundation +import PowerSyncSwift + +let LISTS_TABLE = "lists" +let TODOS_TABLE = "todos" + +let lists = Table( + name: LISTS_TABLE, + columns: [ + // ID column is automatically included + .text("name"), + .text("created_at"), + .text("owner_id") + ] +) + +let todos = Table( + name: TODOS_TABLE, + // ID column is automatically included + columns: [ + Column.text("list_id"), + Column.text("photo_id"), + Column.text("description"), + // 0 or 1 to represent false or true + 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.ascending("list_id")] + ) + ] +) + +let AppSchema = Schema(lists, todos) diff --git a/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift b/Demo/PowerSyncExample/PowerSync/SupabaseConnector.swift new file mode 100644 index 0000000..b8333b4 --- /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.sessionMissing } + + 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.sessionMissing + } + + let token = session!.accessToken + + // userId is for debugging purposes only + return PowerSyncCredentials(endpoint: self.powerSyncEndpoint, token: token, userId: currentUserID) + } + + override func uploadData(database: 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/SystemManager.swift b/Demo/PowerSyncExample/PowerSync/SystemManager.swift new file mode 100644 index 0000000..8479008 --- /dev/null +++ b/Demo/PowerSyncExample/PowerSync/SystemManager.swift @@ -0,0 +1,129 @@ +import Foundation +import PowerSyncSwift + +@Observable +class SystemManager { + let connector = SupabaseConnector() + let schema = AppSchema + var db: PowerSyncDatabaseProtocol! + + // openDb must be called before connect + func openDb() { + db = PowerSyncDatabase(schema: schema, dbFilename: "powersync-swift.sqlite") + } + + func connect() async { + do { + try await db.connect(connector: connector) + } 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() + try await connector.client.auth.signOut() + } + + func watchLists(_ callback: @escaping (_ lists: [ListContent]) -> Void ) async { + for await lists in self.db.watch<[ListContent]>( + 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/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..5113dd8 --- /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(SystemManager()) + } + } +} 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..9450aa1 --- /dev/null +++ b/Demo/PowerSyncExample/RootView.swift @@ -0,0 +1,44 @@ +import Auth +import SwiftUI + +struct RootView: View { + @Environment(SystemManager.self) var system + + @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(system.db == nil) { + system.openDb() + } + } + .environment(authModel) + .environment(navigationModel) + } + +} + +#Preview { + RootView() + .environment(SystemManager()) +} diff --git a/Demo/PowerSyncExample/Screens/HomeScreen.swift b/Demo/PowerSyncExample/Screens/HomeScreen.swift new file mode 100644 index 0000000..6a4da2f --- /dev/null +++ b/Demo/PowerSyncExample/Screens/HomeScreen.swift @@ -0,0 +1,39 @@ +import Foundation +import Auth +import SwiftUI + +struct HomeScreen: View { + @Environment(SystemManager.self) private var system + @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 system.signOut() + authModel.isAuthenticated = false + navigationModel.path = NavigationPath() + } + } + } + } + .task { + if(system.db.currentStatus.connected == false) { + await system.connect() + } + } + .navigationBarBackButtonHidden(true) + } +} + +#Preview { + NavigationStack{ + HomeScreen() + .environment(SystemManager()) + } +} diff --git a/Demo/PowerSyncExample/Screens/SignInScreen.swift b/Demo/PowerSyncExample/Screens/SignInScreen.swift new file mode 100644 index 0000000..40e7c95 --- /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(SystemManager.self) private var system + @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 system.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(SystemManager()) + } +} diff --git a/Demo/PowerSyncExample/Screens/SignUpScreen.swift b/Demo/PowerSyncExample/Screens/SignUpScreen.swift new file mode 100644 index 0000000..bf9bb5e --- /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(SystemManager.self) private var system + @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 system.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(SystemManager()) + } +} 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..8c3d86e --- /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://your-id.powersync.journeyapps.com" + static let supabaseURL = URL(string: "https://your-id.supabase.co")! + static let supabaseAnonKey = "anon-key" +} diff --git a/Demo/README.md b/Demo/README.md new file mode 100644 index 0000000..70d24e4 --- /dev/null +++ b/Demo/README.md @@ -0,0 +1,39 @@ +# PowerSync Swift Demo App + +A Todo List app demonstrating the use of the PowerSync Swift SDK together with Supabase. + +## 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/Package.resolved b/Package.resolved new file mode 100644 index 0000000..b070bf8 --- /dev/null +++ b/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/Package.swift b/Package.swift new file mode 100644 index 0000000..a8e6a22 --- /dev/null +++ b/Package.swift @@ -0,0 +1,37 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription +let packageName = "PowerSyncSwift" + +let package = Package( + name: packageName, + platforms: [ + .iOS(.v13), + .macOS(.v10_13) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: packageName, + 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: packageName, + dependencies: [ + .product(name: "PowerSync", package: "powersync-kotlin"), + .product(name: "PowerSyncSQLiteCore", package: "powersync-sqlite-core-swift") + ]), + .testTarget( + name: "PowerSyncSwiftTests", + dependencies: ["PowerSyncSwift"] + ), + ] +) 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. 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/Kotlin/KotlinPowerSyncDatabaseImpl.swift b/Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift new file mode 100644 index 0000000..6fd3b22 --- /dev/null +++ b/Sources/PowerSyncSwift/Kotlin/KotlinPowerSyncDatabaseImpl.swift @@ -0,0 +1,169 @@ +import Foundation +import PowerSync + +final class KotlinPowerSyncDatabaseImpl: 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: KotlinAdapter.Schema.toKotlin(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 { + 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(truncating: 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/Kotlin/KotlinTypes.swift b/Sources/PowerSyncSwift/Kotlin/KotlinTypes.swift new file mode 100644 index 0000000..1477e48 --- /dev/null +++ b/Sources/PowerSyncSwift/Kotlin/KotlinTypes.swift @@ -0,0 +1,11 @@ +import PowerSync + +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..600e346 --- /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 +@MainActor +public func PowerSyncDatabase( + schema: Schema, + dbFilename: String = DEFAULT_DB_FILENAME +) -> PowerSyncDatabaseProtocol { + + return KotlinPowerSyncDatabaseImpl( + schema: schema, + dbFilename: dbFilename + ) +} diff --git a/Sources/PowerSyncSwift/PowerSyncDatabaseProtocol.swift b/Sources/PowerSyncSwift/PowerSyncDatabaseProtocol.swift new file mode 100644 index 0000000..296ac66 --- /dev/null +++ b/Sources/PowerSyncSwift/PowerSyncDatabaseProtocol.swift @@ -0,0 +1,117 @@ +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 +} + +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/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/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/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()) + } +}