├── Mintfile ├── docs └── resources │ ├── EmulatorPort.png │ ├── swiftpm_step1.png │ ├── swiftpm_step2.png │ ├── swiftpm_step3.png │ ├── swiftpm_step4.png │ ├── Extension_SignIn.png │ ├── NetworkCapabilities.png │ ├── StartEmulatorButton.png │ └── ConnectorYamlLocation.png ├── Tests ├── Integration │ ├── Resources │ │ └── fdc-kitchensink │ │ │ ├── firebase.json │ │ │ ├── .vscode │ │ │ └── settings.json │ │ │ ├── .firebaserc │ │ │ ├── dataconnect │ │ │ ├── dataconnect.yaml │ │ │ ├── default │ │ │ │ ├── connector.yaml │ │ │ │ ├── queries.gql │ │ │ │ └── mutations.gql │ │ │ └── schema │ │ │ │ └── schema.gql │ │ │ └── .gitignore │ ├── Emulator │ │ └── start-emulator.sh │ ├── IntegrationTestBase.swift │ ├── ConfigSetup.swift │ ├── PartialErrorsTest.swift │ └── Gen │ │ └── KitchenSink │ │ └── Sources │ │ └── KitchenSinkClient.swift ├── NoIntegration │ └── Readme ├── Unit │ ├── UserAgentTests.swift │ ├── ErrorsTypes.swift │ ├── LocalDateTests.swift │ ├── AnyValueCodableTests.swift │ └── HeaderTests.swift └── ShellExecutor │ └── TestExecutor.swift ├── Tools ├── TemplateProject │ ├── Resources │ │ └── demo-iosproject │ │ │ ├── firebase.json │ │ │ ├── .vscode │ │ │ └── settings.json │ │ │ ├── dataconnect │ │ │ ├── schema │ │ │ │ └── schema.gql │ │ │ ├── default │ │ │ │ ├── connector.yaml │ │ │ │ ├── queries.gql │ │ │ │ └── mutations.gql │ │ │ └── dataconnect.yaml │ │ │ ├── .firebaserc │ │ │ ├── GoogleService-Info-Template.plist │ │ │ └── .gitignore │ └── TemplateProject.swift └── SetupDevEnv │ └── SetupDevEnv.swift ├── Examples └── FriendlyFlix │ ├── FriendlyFlix-Hero.png │ ├── app │ ├── FriendlyFlix │ │ ├── FriendlyFlix │ │ │ ├── Assets.xcassets │ │ │ │ ├── Contents.json │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ ├── Preview Content │ │ │ │ └── Preview Assets.xcassets │ │ │ │ │ └── Contents.json │ │ │ ├── Utilities │ │ │ │ ├── String+Placeholder.swift │ │ │ │ ├── ButtonStyle+NoHighlight.swift │ │ │ │ ├── String+StringInterpolation.swift │ │ │ │ ├── View+Extension.swift │ │ │ │ └── Color+Hex.swift │ │ │ ├── Model │ │ │ │ ├── Mockable.swift │ │ │ │ └── Movie+DataConnect.swift │ │ │ ├── GoogleService-Info.plist │ │ │ ├── Features │ │ │ │ ├── Reviews │ │ │ │ │ ├── StarRatingView.swift │ │ │ │ │ └── MovieReviewCard.swift │ │ │ │ ├── MovieList │ │ │ │ │ └── MovieListScreen.swift │ │ │ │ ├── Authentication │ │ │ │ │ ├── AuthenticationToolbarButton.swift │ │ │ │ │ ├── AccountScreen.swift │ │ │ │ │ └── AuthenticationService.swift │ │ │ │ ├── Home │ │ │ │ │ ├── DetailsSection.swift │ │ │ │ │ └── MovieTeaserView.swift │ │ │ │ ├── Library │ │ │ │ │ └── LibraryScreen.swift │ │ │ │ └── Search │ │ │ │ │ └── SearchScreen.swift │ │ │ ├── App │ │ │ │ ├── RootView.swift │ │ │ │ └── FriendlyFlixApp.swift │ │ │ └── Views │ │ │ │ ├── MovieListSection.swift │ │ │ │ ├── MovieListRowView.swift │ │ │ │ └── MovieTileView.swift │ │ └── FriendlyFlix.xcodeproj │ │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── FriendlyFlix.xcscheme │ └── FriendlyFlixSDK │ │ ├── Package.swift │ │ └── Sources │ │ └── FriendlyFlixClient.swift │ ├── .firebase │ └── .graphqlrc │ ├── .editorconfig │ ├── dataconnect │ ├── movie-connector │ │ ├── connector.yaml │ │ └── mutations.gql │ ├── dataconnect.yaml │ ├── .dataconnect │ │ └── schema │ │ │ └── main │ │ │ └── implicit.gql │ └── schema │ │ └── schema.gql │ ├── firebase.json │ └── .swift-format ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── issue_report.md └── workflows │ ├── check.yml │ └── spm.yml ├── .swiftformat ├── .editorconfig ├── Sources ├── Internal │ ├── ServerResponse.swift │ ├── Logger │ │ ├── DataConnectMessageCode.swift │ │ └── DataConnectLogger.swift │ ├── QueryRefInternal.swift │ ├── Component.swift │ ├── HashUtils.swift │ ├── Version.swift │ ├── CodableHelpers.swift │ ├── CodableTimestamp.swift │ ├── OperationsManager.swift │ └── ProtoCodec.swift ├── DataSource.swift ├── Cache │ ├── DynamicCodingKey.swift │ ├── CacheProvider.swift │ ├── ResultTree.swift │ ├── CacheSettings.swift │ └── EntityDataObject.swift ├── OperationResult.swift ├── BaseOperationRef.swift ├── ConnectorConfig.swift ├── DataConnectPathSegment.swift ├── Queries │ ├── QueryFetchPolicy.swift │ ├── QueryRef.swift │ └── QueryRequest.swift ├── DataConnectSettings.swift ├── OptionalVarWrapper.swift ├── MutationRef.swift └── Scalars │ └── AnyValue.swift ├── setup-scripts.sh ├── Protos ├── google │ ├── api │ │ └── annotations.proto │ ├── protobuf │ │ ├── empty.proto │ │ └── struct.proto │ └── type │ │ └── latlng.proto ├── build_protos.sh ├── Readme.md ├── graphql_error.proto └── connector_service.proto ├── SwiftPackageManager.md ├── .gitignore └── CHANGELOG.md /Mintfile: -------------------------------------------------------------------------------- 1 | nicklockwood/SwiftFormat@0.54.0 2 | -------------------------------------------------------------------------------- /docs/resources/EmulatorPort.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/data-connect-ios-sdk/HEAD/docs/resources/EmulatorPort.png -------------------------------------------------------------------------------- /Tests/Integration/Resources/fdc-kitchensink/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataconnect": { 3 | "source": "dataconnect" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/resources/swiftpm_step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/data-connect-ios-sdk/HEAD/docs/resources/swiftpm_step1.png -------------------------------------------------------------------------------- /docs/resources/swiftpm_step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/data-connect-ios-sdk/HEAD/docs/resources/swiftpm_step2.png -------------------------------------------------------------------------------- /docs/resources/swiftpm_step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/data-connect-ios-sdk/HEAD/docs/resources/swiftpm_step3.png -------------------------------------------------------------------------------- /docs/resources/swiftpm_step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/data-connect-ios-sdk/HEAD/docs/resources/swiftpm_step4.png -------------------------------------------------------------------------------- /Tools/TemplateProject/Resources/demo-iosproject/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataconnect": { 3 | "source": "dataconnect" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/resources/Extension_SignIn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/data-connect-ios-sdk/HEAD/docs/resources/Extension_SignIn.png -------------------------------------------------------------------------------- /docs/resources/NetworkCapabilities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/data-connect-ios-sdk/HEAD/docs/resources/NetworkCapabilities.png -------------------------------------------------------------------------------- /docs/resources/StartEmulatorButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/data-connect-ios-sdk/HEAD/docs/resources/StartEmulatorButton.png -------------------------------------------------------------------------------- /Tests/Integration/Resources/fdc-kitchensink/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/target": true 4 | } 5 | } -------------------------------------------------------------------------------- /docs/resources/ConnectorYamlLocation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/data-connect-ios-sdk/HEAD/docs/resources/ConnectorYamlLocation.png -------------------------------------------------------------------------------- /Examples/FriendlyFlix/FriendlyFlix-Hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/data-connect-ios-sdk/HEAD/Examples/FriendlyFlix/FriendlyFlix-Hero.png -------------------------------------------------------------------------------- /Tools/TemplateProject/Resources/demo-iosproject/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/target": true 4 | } 5 | } -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/.firebase/.graphqlrc: -------------------------------------------------------------------------------- 1 | {"schema":["../dataconnect/schema/**/*.gql","../dataconnect/.dataconnect/**/*.gql"],"document":["../dataconnect/movie-connector/**/*.gql"]} -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tools/TemplateProject/Resources/demo-iosproject/dataconnect/schema/schema.gql: -------------------------------------------------------------------------------- 1 | 2 | 3 | type Item @table { 4 | id: UUID! 5 | name: String! 6 | desc: String 7 | #price: Float! 8 | } 9 | -------------------------------------------------------------------------------- /Tests/Integration/Resources/fdc-kitchensink/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "aa-scratchpad" 4 | }, 5 | "targets": {}, 6 | "etags": {}, 7 | "dataconnectEmulatorConfig": {} 8 | } -------------------------------------------------------------------------------- /Tools/TemplateProject/Resources/demo-iosproject/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "demo-iosproject" 4 | }, 5 | "targets": {}, 6 | "etags": {}, 7 | "dataconnectEmulatorConfig": {} 8 | } 9 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.swift] 2 | indent_style = space 3 | indent_size = 2 4 | tab_width = 2 5 | end_of_line = crlf 6 | insert_final_newline = true 7 | max_line_length = 100 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /Examples/FriendlyFlix/dataconnect/movie-connector/connector.yaml: -------------------------------------------------------------------------------- 1 | connectorId: friendly-flix 2 | authMode: PUBLIC 3 | generate: 4 | swiftSdk: 5 | outputDir: "../../app" 6 | package: "FriendlyFlixSDK" 7 | observablePublisher: observableMacro -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Firebase Data Connect 4 | url: https://github.com/firebase/firebase-ios-sdk/issues/new/choose 5 | about: Firebase Data Connect issues are tracked in the firebase-ios-sdk repository 6 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "emulators": { 3 | "auth": { 4 | "port": 9099 5 | }, 6 | "dataconnect": { 7 | "port": 9399 8 | }, 9 | "singleProjectMode": true 10 | }, 11 | "dataconnect": { 12 | "source": "./dataconnect" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Internal Issue 3 | about: Googlers may file issues here or at https://github.com/firebase/firebase-ios-sdk/issues/new/choose 4 | --- 5 | 9 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/dataconnect/dataconnect.yaml: -------------------------------------------------------------------------------- 1 | specVersion: "v1beta" 2 | serviceId: "dataconnect" 3 | location: "us-central1" 4 | schema: 5 | source: "./schema" 6 | datasource: 7 | postgresql: 8 | database: "fdcdb" 9 | cloudSql: 10 | instanceId: "fdc-sql" 11 | connectorDirs: ["./movie-connector"] 12 | -------------------------------------------------------------------------------- /Tools/TemplateProject/Resources/demo-iosproject/dataconnect/default/connector.yaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | connectorId: "Items" 4 | authMode: "PUBLIC" 5 | 6 | generate: 7 | swiftSdk: 8 | outputDir: "../../dataconnect-generated/" 9 | package: "ItemData" #Swift Package Name for Generated SDK 10 | # coreSdkPackageLocation: "/path/to/cloned/data-connect-ios-sdk" 11 | -------------------------------------------------------------------------------- /Tools/TemplateProject/Resources/demo-iosproject/dataconnect/dataconnect.yaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | specVersion: "v1" 5 | serviceId: "demo-iosproject" 6 | location: "us-central1" 7 | schema: 8 | source: "./schema" 9 | datasource: 10 | postgresql: 11 | database: "demo-iosproject" 12 | cloudSql: 13 | instanceId: "demo-iosproject" 14 | connectorDirs: ["./default"] 15 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # Formatting Options - Mimic Google style 2 | --indent 2 3 | --maxwidth 100 4 | --wrapparameters afterfirst 5 | 6 | # Disabled Rules 7 | 8 | # Too many of our swift files have simplistic examples. While technically 9 | # it's correct to remove the unused argument labels, it makes our examples 10 | # look wrong. 11 | --disable unusedArguments 12 | 13 | # We prefer trailing braces. 14 | --disable wrapMultilineStatementBraces 15 | -------------------------------------------------------------------------------- /Tools/TemplateProject/Resources/demo-iosproject/dataconnect/default/queries.gql: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # **IMPORTANT** 6 | # Before deploying to server, be sure to change the auth level to something higher than PUBLIC 7 | 8 | query GetItem($id: UUID!) @auth(level: PUBLIC) { 9 | item(id: $id) { 10 | id 11 | name 12 | desc 13 | #price 14 | } 15 | } 16 | 17 | query ListItems @auth(level: PUBLIC) { 18 | items { 19 | id 20 | name 21 | #price #Uncomment this to include price in the query 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Tools/TemplateProject/Resources/demo-iosproject/dataconnect/default/mutations.gql: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # **IMPORTANT** 6 | # Before deploying to server, be sure to change the auth level to something higher than PUBLIC 7 | 8 | 9 | # Mutation with price field 10 | # mutation CreateItem($id: UUID!, $name: String!, $desc: String, $price: Float!) @auth(level: PUBLIC) { 11 | 12 | 13 | mutation CreateItem($id: UUID!, $name: String!, $desc: String ) @auth(level: PUBLIC) { 14 | item_upsert(data: { 15 | id: $id, 16 | name: $name, 17 | desc: $desc, 18 | # price: $price 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # See https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties 2 | # for all available properties. 3 | 4 | # Top-most EditorConfig file for the firebase-ios-sdk repo. 5 | root = true 6 | 7 | # Defaults for all files 8 | [*] 9 | charset = utf-8 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | # ObjC and Swift files 15 | # See https://developer.apple.com/documentation/xcode-release-notes/xcode-16-release-notes#New-Features-in-Xcode-16-Beta 16 | # for the subset of properties supported by Xcode. 17 | [*.{h,m,mm,swift}] 18 | indent_style = space 19 | indent_size = 2 20 | max_line_length = 100 21 | -------------------------------------------------------------------------------- /Tests/NoIntegration/Readme: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | 17 | This is an empty folder temporarily created till we enable Integration tests in git. 18 | -------------------------------------------------------------------------------- /Sources/Internal/ServerResponse.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | struct ServerResponse { 18 | let jsonResults: String 19 | let maxAge: TimeInterval? 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Internal/Logger/DataConnectMessageCode.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | enum MessageCode: Int { 16 | // DataConnect Logging message code should be align with backend and is TBD 17 | case placeHolder = 0 18 | } 19 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tools/TemplateProject/Resources/demo-iosproject/GoogleService-Info-Template.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_KEY 6 | correct_api_key 7 | CLIENT_ID 8 | correct_client_id 9 | REVERSED_CLIENT_ID 10 | correct_reversed_client_id 11 | GOOGLE_APP_ID 12 | 1:123:ios:123abc 13 | GCM_SENDER_ID 14 | correct_gcm_sender_id 15 | PLIST_VERSION 16 | 1 17 | BUNDLE_ID 18 | com.google.firebase.dataconnect.DemoProject 19 | PROJECT_ID 20 | demo-iosproject 21 | 22 | 23 | -------------------------------------------------------------------------------- /setup-scripts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2021 Google LLC 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | git clone \ 15 | --depth 1 \ 16 | --no-checkout \ 17 | https://github.com/firebase/firebase-ios-sdk.git \ 18 | ; 19 | cd firebase-ios-sdk 20 | git checkout main -- scripts 21 | cd .. 22 | ln -s firebase-ios-sdk/scripts scripts 23 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/String+Placeholder.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | extension String { 18 | static func placeholder(length: Int) -> String { 19 | String(Array(repeating: "X", count: length)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Internal/QueryRefInternal.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 16 | protocol QueryRefInternal: QueryRef { 17 | var operationId: String { get } 18 | func publishCacheResultsToSubscribers(allowStale: Bool) async throws 19 | } 20 | -------------------------------------------------------------------------------- /Sources/DataSource.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Indicates the source of the query results data. 16 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 17 | public enum DataSource: Sendable { 18 | /// The query results are from server 19 | case server 20 | 21 | /// Query results are from cache 22 | case cache 23 | } 24 | -------------------------------------------------------------------------------- /Tests/Integration/Resources/fdc-kitchensink/dataconnect/dataconnect.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | 17 | specVersion: "v1alpha" 18 | serviceId: "fdc-kitchensink" 19 | location: "us-central1" 20 | schema: 21 | source: "./schema" 22 | datasource: 23 | postgresql: 24 | database: "kitchensink" 25 | cloudSql: 26 | instanceId: "fdc-test" 27 | connectorDirs: ["./default"] 28 | -------------------------------------------------------------------------------- /Sources/Cache/DynamicCodingKey.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Used for inline inline hydration of entity values 16 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 17 | struct DynamicCodingKey: CodingKey { 18 | var intValue: Int? 19 | let stringValue: String 20 | init?(intValue: Int) { return nil } 21 | init?(stringValue: String) { self.stringValue = stringValue } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/OperationResult.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Structure representing the value returned by operation calls - query or mutation 18 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 19 | public struct OperationResult: Sendable { 20 | public let data: ResultData? 21 | public let source: DataSource 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Internal/Component.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Class for registration with the Firebase component system, including userAgent functionality. 18 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 19 | @objc(FIRDataConnectComponent) class DataConnectComponent: NSObject { 20 | @objc class func sdkVersion() -> String { 21 | return Version.sdkVersion 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Model/Mockable.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | public protocol Mockable { 16 | associatedtype MockType 17 | 18 | static var mock: MockType { get } 19 | static var mockList: [MockType] { get } 20 | } 21 | 22 | public extension Mockable { 23 | static var mock: MockType { 24 | mockList[0] 25 | } 26 | 27 | static var mockList: [MockType] { 28 | [] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/ButtonStyle+NoHighlight.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftUI 16 | 17 | struct NoHighlightButtonStyle: ButtonStyle { 18 | func makeBody(configuration: Configuration) -> some View { 19 | configuration.label 20 | } 21 | } 22 | 23 | extension ButtonStyle where Self == NoHighlightButtonStyle { 24 | static var noHighlight: NoHighlightButtonStyle { NoHighlightButtonStyle() } 25 | } 26 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_KEY 6 | AIzaSyAzlj4APqi5S58nFtE52Da0fYBOHA2MhaY 7 | GCM_SENDER_ID 8 | 123456789000 9 | PLIST_VERSION 10 | 1 11 | BUNDLE_ID 12 | com.google.firebase.samples.FriendlyFlix 13 | PROJECT_ID 14 | pfr-fdc-friendlyflix-dev-01 15 | STORAGE_BUCKET 16 | mockproject-1234 17 | IS_ADS_ENABLED 18 | 19 | IS_ANALYTICS_ENABLED 20 | 21 | IS_APPINVITE_ENABLED 22 | 23 | IS_GCM_ENABLED 24 | 25 | IS_SIGNIN_ENABLED 26 | 27 | GOOGLE_APP_ID 28 | 1:123456789000:ios:f1bf012572b04063 29 | 30 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/String+StringInterpolation.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | extension String.StringInterpolation { 18 | mutating func appendInterpolation(format value: Int, using style: NumberFormatter.Style) { 19 | let formatter = NumberFormatter() 20 | formatter.numberStyle = style 21 | 22 | if let result = formatter.string(from: value as NSNumber) { 23 | appendLiteral(result) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Protos/google/api/annotations.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/api/http.proto"; 20 | import "google/protobuf/descriptor.proto"; 21 | 22 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "AnnotationsProto"; 25 | option java_package = "com.google.api"; 26 | option objc_class_prefix = "GAPI"; 27 | 28 | extend google.protobuf.MethodOptions { 29 | // See `HttpRule`. 30 | HttpRule http = 72295728; 31 | } 32 | -------------------------------------------------------------------------------- /Tests/Integration/Resources/fdc-kitchensink/dataconnect/default/connector.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | connectorId: "kitchen-sink" 17 | authMode: "PUBLIC" 18 | 19 | # If you are changing the fdc-kitchensink schema or operations 20 | # Adjust the outputDir to your local path 21 | # Copy the generated files from generated "Sources" folder 22 | # into the FirebaseDataConnect/Tests/Integration folder 23 | # In future, we can consider a Test only Swift build plugin to gen the files, to avoid this. 24 | generate: 25 | swiftSdk: 26 | outputDir: "../../../../Gen/" 27 | package: "KitchenSink" 28 | observablePublisher: "observableObject" 29 | -------------------------------------------------------------------------------- /Sources/Cache/CacheProvider.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | import FirebaseCore 18 | 19 | // Key to store cache provider in Codables userInfo object. 20 | let CacheProviderUserInfoKey = CodingUserInfoKey(rawValue: "fdc_cache_provider")! 21 | 22 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 23 | protocol CacheProvider { 24 | var cacheIdentifier: String { get } 25 | 26 | func resultTree(queryId: String) -> ResultTree? 27 | func setResultTree(queryId: String, tree: ResultTree) 28 | 29 | func entityData(_ entityGuid: String) -> EntityDataObject 30 | func updateEntityData(_ object: EntityDataObject) 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: main 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | check: 14 | runs-on: macos-latest 15 | env: 16 | MINT_PATH: ${{ github.workspace }}/mint 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-python@v5 21 | with: 22 | python-version: '3.11' 23 | 24 | - name: Cache Mint packages 25 | uses: actions/cache@v4 26 | with: 27 | path: ${{ env.MINT_PATH }} 28 | key: ${{ runner.os }}-mint-${{ hashFiles('**/Mintfile') }} 29 | restore-keys: ${{ runner.os }}-mint- 30 | 31 | - name: Setup Scripts Directory 32 | run: ./setup-scripts.sh 33 | 34 | - name: Setup check 35 | run: scripts/setup_check.sh 36 | 37 | - name: Style 38 | run: scripts/style.sh test-only 39 | 40 | - name: Whitespace 41 | run: scripts/check_whitespace.sh 42 | 43 | - name: Filename spaces 44 | run: scripts/check_filename_spaces.sh 45 | 46 | - name: Copyrights 47 | run: scripts/check_copyright.sh 48 | 49 | - name: Imports 50 | run: scripts/check_imports.swift 51 | -------------------------------------------------------------------------------- /Sources/Internal/HashUtils.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import CryptoKit 16 | import Foundation 17 | 18 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 19 | extension Data { 20 | var sha256String: String { 21 | let hashDigest = SHA256.hash(data: self) 22 | let hashString = hashDigest.compactMap { String(format: "%02x", $0) }.joined() 23 | return hashString 24 | } 25 | } 26 | 27 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 28 | extension String { 29 | var sha256: String { 30 | let digest = SHA256.hash(data: data(using: .utf8)!) 31 | let hashString = digest.compactMap { String(format: "%02x", $0) }.joined() 32 | return hashString 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Reviews/StarRatingView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftUI 16 | 17 | struct StarRatingView: View { 18 | var rating: Double 19 | 20 | var body: some View { 21 | HStack(spacing: 4) { 22 | ForEach(0 ..< 5) { index in 23 | Image(systemName: self.starType(for: index)) 24 | .foregroundColor(.yellow) 25 | } 26 | } 27 | } 28 | 29 | func starType(for index: Int) -> String { 30 | if rating > Double(index) + 0.75 { 31 | return "star.fill" 32 | } else if rating > Double(index) + 0.25 { 33 | return "star.lefthalf.fill" 34 | } else { 35 | return "star" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Cache/ResultTree.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 18 | struct ResultTree: Codable { 19 | // tree data - could be hydrated or dehydrated. 20 | let data: String 21 | 22 | // Local time when the entry was cached / updated 23 | let cachedAt: Date 24 | 25 | // Local time when the entry was read or updated 26 | var lastAccessed: Date 27 | 28 | var rootObject: EntityNode? 29 | 30 | func isStale(_ ttl: TimeInterval) -> Bool { 31 | let now = Date() 32 | return now.timeIntervalSince(cachedAt) > ttl 33 | } 34 | 35 | enum CodingKeys: String, CodingKey { 36 | case cachedAt = "ca" // cached at 37 | case lastAccessed = "la" // last accessed 38 | case data = "d" // data cached 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/BaseOperationRef.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | // notional protocol that denotes a variable. 18 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 19 | public protocol OperationVariable: Encodable, Hashable, Equatable, Sendable {} 20 | 21 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 22 | protocol OperationRequest: Hashable, Equatable, Sendable { 23 | associatedtype Variable: OperationVariable 24 | var operationName: String { get } // Name within Connector definition 25 | var variables: Variable? { get } 26 | } 27 | 28 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 29 | public protocol OperationRef: Hashable, Equatable { 30 | associatedtype ResultData: Decodable & Sendable 31 | 32 | func execute() async throws -> OperationResult 33 | } 34 | -------------------------------------------------------------------------------- /Protos/build_protos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2024 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | #This is a convenience script to build protos and generate Swift files 19 | #It requires the Swift grpc and proto plugins which are part of swift-grpc project 20 | #Script should be run from the folder containing this script 21 | 22 | protoc_path="protoc" 23 | sdk_folder="/Users/aashishp/Code/data-connect-ios-sdk" 24 | plugin_folder="/Users/aashishp/dev/protoc-grpc-swift-plugins-1.23.0/bin" 25 | 26 | 27 | protoc graphql_error.proto connector_service.proto \ 28 | --proto_path=$sdk_folder/Protos/ \ 29 | --plugin=$plugin_folder/protoc-gen-swift \ 30 | --swift_opt=Visibility=Public \ 31 | --swift_out=$sdk_folder/Sources/ProtoGen \ 32 | --plugin=$plugin_folder/protoc-gen-grpc-swift \ 33 | --grpc-swift_opt=Visibility=Public \ 34 | --grpc-swift_out=$sdk_folder/Sources/ProtoGen 35 | 36 | 37 | -------------------------------------------------------------------------------- /Sources/ConnectorConfig.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 18 | public struct ConnectorConfig: Hashable, Equatable, Sendable { 19 | public let serviceId: String 20 | public let location: String 21 | public let connector: String 22 | 23 | public init(serviceId: String, location: String, connector: String) { 24 | self.serviceId = serviceId 25 | self.location = location 26 | self.connector = connector 27 | } 28 | 29 | public func hash(into hasher: inout Hasher) { 30 | hasher.combine(serviceId) 31 | hasher.combine(location) 32 | hasher.combine(connector) 33 | } 34 | 35 | public static func == (lhs: Self, rhs: Self) -> Bool { 36 | return lhs.serviceId == rhs.serviceId && 37 | lhs.location == rhs.location && 38 | lhs.connector == rhs.connector 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/Integration/Emulator/start-emulator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2025 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Sets up Firebase Data Connect emulator to execute 18 | # integration tests. 19 | 20 | set -e 21 | 22 | # Get the absolute path to the directory containing this script. 23 | SCRIPT_DIR="$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)" 24 | TEMP_DIR="$(mktemp -d -t firebase-data-connect)" 25 | echo "Starting Firebase Data Connect emulator in ${TEMP_DIR}" 26 | cd "${TEMP_DIR}" 27 | 28 | EMULATOR_VERSION="1.8.3" 29 | EMULATOR_FILENAME="dataconnect-emulator-macos-v${EMULATOR_VERSION}" 30 | EMULATOR_URL="https://storage.googleapis.com/firemat-preview-drop/emulator/${EMULATOR_FILENAME}" 31 | echo "Downloading emulator from ${EMULATOR_URL}" 32 | 33 | curl -o "${EMULATOR_FILENAME}" "${EMULATOR_URL}" 34 | 35 | chmod 755 "${EMULATOR_FILENAME}" 36 | 37 | ./${EMULATOR_FILENAME} --logtostderr dev --listen="127.0.0.1:3628" & 38 | -------------------------------------------------------------------------------- /Tests/Integration/Resources/fdc-kitchensink/dataconnect/default/queries.gql: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | query GetStandardScalar($id: UUID!) @auth(level: PUBLIC) { 16 | standardScalars(id: $id) { 17 | id 18 | number 19 | text 20 | decimal 21 | } 22 | } 23 | 24 | query GetScalarBoundary($id: UUID!) @auth(level: PUBLIC) { 25 | scalarBoundary(id: $id) { 26 | maxNumber 27 | minNumber 28 | maxDecimal 29 | minDecimal 30 | } 31 | } 32 | 33 | query GetLargeNum($id: UUID!) @auth(level: PUBLIC) { 34 | largeIntType(id: $id) { 35 | num 36 | maxNum 37 | minNum 38 | } 39 | } 40 | 41 | query GetLocalDateType($id: UUID!) @auth(level: PUBLIC) { 42 | localDateType(id: $id) { 43 | localDate 44 | } 45 | } 46 | 47 | query GetAnyValueType($id: UUID!) @auth(level: PUBLIC) { 48 | anyValueType(id: $id) { 49 | props 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/Integration/Resources/fdc-kitchensink/dataconnect/schema/schema.gql: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | 17 | #Types for Scalar Tests 18 | type TestId @table { 19 | id: UUID! 20 | } 21 | 22 | type TestAutoId @table { 23 | id: UUID! @default(expr: "uuidV4()") 24 | } 25 | 26 | type StandardScalars @table { 27 | id: UUID! 28 | number: Int! 29 | text: String! 30 | decimal: Float! 31 | } 32 | 33 | type ScalarBoundary @table { 34 | id: UUID! 35 | maxNumber: Int! 36 | minNumber: Int! 37 | maxDecimal: Float! 38 | minDecimal: Float! 39 | } 40 | 41 | type LargeIntType @table { 42 | id: UUID! 43 | num: Int64! 44 | maxNum: Int64! 45 | minNum: Int64! 46 | } 47 | 48 | type LocalDateType @table { 49 | id: UUID! 50 | localDate: Date 51 | } 52 | 53 | type AnyValueType @table { 54 | id: UUID! 55 | props: Any! 56 | } 57 | 58 | type Person @table { 59 | name: String! 60 | } 61 | 62 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlixSDK/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | // Copyright 2024 Google LLC 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | 18 | import PackageDescription 19 | 20 | let package = Package( 21 | name: "FriendlyFlixSDK", 22 | platforms: [ 23 | .iOS(.v17), 24 | .macOS(.v14), 25 | .watchOS(.v10), 26 | .tvOS(.v17), 27 | ], 28 | products: [ 29 | .library( 30 | name: "FriendlyFlixSDK", 31 | targets: ["FriendlyFlixSDK"] 32 | ), 33 | ], 34 | dependencies: [ 35 | .package(url: "https://github.com/firebase/data-connect-ios-sdk", from: "11.3.0-beta"), 36 | 37 | ], 38 | targets: [ 39 | .target( 40 | name: "FriendlyFlixSDK", 41 | dependencies: [ 42 | .product(name: "FirebaseDataConnect", package: "data-connect-ios-sdk"), 43 | ], 44 | path: "Sources" 45 | ), 46 | ] 47 | ) 48 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/MovieList/MovieListScreen.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftUI 16 | 17 | struct MovieListScreen: View { 18 | var namespace: Namespace.ID 19 | var movies: [Movie] 20 | 21 | var body: some View { 22 | List { 23 | ForEach(movies) { movie in 24 | MovieListRowView( 25 | title: movie.title, 26 | subtitle: movie.description, 27 | imageUrl: movie.imageUrl 28 | ) 29 | .matchedTransitionSource(id: movie.id, in: namespace) 30 | .navigationLink(value: movie, hideChevron: true) 31 | } 32 | } 33 | .listStyle(.plain) 34 | } 35 | } 36 | 37 | #Preview { 38 | @Previewable @Namespace var namespace 39 | NavigationStack { 40 | MovieListScreen(namespace: namespace, movies: Movie.mockList) 41 | .navigationTitle("Continue watching?") 42 | .navigationBarTitleDisplayMode(.inline) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/Integration/Resources/fdc-kitchensink/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | # dataconnect generated files 69 | .dataconnect 70 | -------------------------------------------------------------------------------- /Tools/TemplateProject/Resources/demo-iosproject/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | # dataconnect generated files 69 | .dataconnect 70 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AuthenticationToolbarButton.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftUI 16 | 17 | struct AuthenticationToolbarButton: View { 18 | @Environment(AuthenticationService.self) var authenticationService 19 | 20 | private func onButtonTapped() { 21 | if authenticationService.authenticationState == .unauthenticated { 22 | authenticationService.presentingAuthenticationDialog.toggle() 23 | } else { 24 | authenticationService.presentingAccountDialog.toggle() 25 | } 26 | } 27 | } 28 | 29 | extension AuthenticationToolbarButton { 30 | var body: some View { 31 | Button(action: onButtonTapped) { 32 | Image(systemName: authenticationService 33 | .authenticationState == .unauthenticated ? "person.circle" : "person.circle.fill") 34 | } 35 | } 36 | } 37 | 38 | #Preview { 39 | AuthenticationToolbarButton() 40 | .environment(AuthenticationService()) 41 | } 42 | -------------------------------------------------------------------------------- /Sources/DataConnectPathSegment.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 16 | public enum DataConnectPathSegment: Codable, Equatable, Sendable { 17 | case field(String) 18 | case listIndex(Int) 19 | } 20 | 21 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 22 | public extension DataConnectPathSegment { 23 | init(from decoder: any Decoder) throws { 24 | let container = try decoder.singleValueContainer() 25 | 26 | do { 27 | let field = try container.decode(String.self) 28 | self = .field(field) 29 | } catch { 30 | let index = try container.decode(Int.self) 31 | self = .listIndex(index) 32 | } 33 | } 34 | 35 | func encode(to encoder: any Encoder) throws { 36 | var container = encoder.singleValueContainer() 37 | switch self { 38 | case let .field(fieldVal): 39 | try container.encode(fieldVal) 40 | case let .listIndex(indexVal): 41 | try container.encode(indexVal) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/Unit/UserAgentTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import XCTest 17 | 18 | import FirebaseCore 19 | @testable import FirebaseDataConnect 20 | 21 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 22 | final class UserAgentTests: XCTestCase { 23 | static var options: FirebaseOptions = { 24 | let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", 25 | gcmSenderID: "00000000000000000-00000000000-000000000") 26 | options.projectID = "user-agent-test" 27 | options.apiKey = "testUserAgentDummyApiKey" 28 | return options 29 | }() 30 | 31 | override class func setUp() { 32 | FirebaseApp.configure(name: "user-agent", options: options) 33 | } 34 | 35 | /// Confirm that Data Connect gets added to the user agent. 36 | func testUserAgent() { 37 | let userAgent = FirebaseApp.firebaseUserAgent() 38 | let version = Version.sdkVersion 39 | XCTAssertTrue(userAgent.contains("fire-dc/\(version)")) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Model/Movie+DataConnect.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import FirebaseDataConnect 16 | import FriendlyFlixSDK 17 | 18 | extension Movie { 19 | init(from: ListMoviesQuery.Data.Movie) { 20 | id = from.id 21 | title = from.title 22 | description = from.description ?? "" 23 | releaseYear = from.releaseYear 24 | rating = from.rating 25 | imageUrl = from.imageUrl 26 | } 27 | 28 | init(from: ListMoviesByPartialTitleQuery.Data.Movie) { 29 | id = from.id 30 | title = from.title 31 | description = from.description ?? "" 32 | releaseYear = from.releaseYear 33 | rating = from.rating 34 | imageUrl = from.imageUrl 35 | } 36 | 37 | init(from: GetUserFavoriteMoviesQuery.Data.User.FavoriteMovieFavoriteMovies) { 38 | id = from.movie.id 39 | title = from.movie.title 40 | description = from.movie.description ?? "" 41 | releaseYear = from.movie.releaseYear 42 | rating = from.movie.rating 43 | imageUrl = from.movie.imageUrl 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Queries/QueryFetchPolicy.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Policies for executing a Data Connect query. This value is optionally passed to `execute()` 16 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 17 | public enum QueryFetchPolicy { 18 | /// default policy tries to fetch from cache if fetch is within the revalidationInterval. 19 | /// If fetch is outside revalidationInterval it revalidates / refreshes from the server. 20 | /// Throws if server revalidation fails 21 | /// Callers may call with `cacheOnly` policy to fetch data (if present) outside 22 | /// revalidationInterval from cache. 23 | /// revalidationInterval is specified as part of the query YAML config using 24 | /// `client-cache.revalidateAfter` key 25 | case preferCache 26 | 27 | /// Always attempts to return from cache. Does not reach out to server 28 | case cacheOnly 29 | 30 | /// Attempts to fetch from server ignoring cache. 31 | /// Cache is refreshed from server data if call succeeds. 32 | /// Throws if server call fails 33 | case serverOnly 34 | } 35 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/App/RootView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftUI 16 | 17 | struct RootView: View { 18 | @Environment(AuthenticationService.self) private var authenticationViewModel 19 | 20 | var body: some View { 21 | @Bindable var authenticationViewModel = authenticationViewModel 22 | TabView { 23 | HomeScreen() 24 | .tabItem { 25 | Label("Home", systemImage: "house") 26 | } 27 | SearchScreen() 28 | .tabItem { 29 | Label("Search", systemImage: "magnifyingglass") 30 | } 31 | LibraryScreen() 32 | .tabItem { 33 | Label("Library", systemImage: "rectangle.on.rectangle") 34 | } 35 | } 36 | .sheet(isPresented: $authenticationViewModel.presentingAuthenticationDialog) { 37 | AuthenticationScreen() 38 | } 39 | .sheet(isPresented: $authenticationViewModel.presentingAccountDialog) { 40 | AccountScreen() 41 | } 42 | } 43 | } 44 | 45 | #Preview { 46 | RootView() 47 | .environment(AuthenticationService()) 48 | } 49 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/View+Extension.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftUI 16 | 17 | extension View { 18 | @ViewBuilder 19 | func redacted(if condition: @autoclosure () -> Bool) -> some View { 20 | redacted(reason: condition() ? .placeholder : []) 21 | } 22 | } 23 | 24 | extension View { 25 | @ViewBuilder 26 | func navigationLink(value: any Hashable, hideChevron: Bool = false) -> some View { 27 | if hideChevron { 28 | // If hideChevron is true, apply the overlay trick to hide the chevron 29 | // Put the NavigationLink into an overlay, and set its opacity to zero. By using this trick, 30 | // we can hide the chevron. 31 | // Source: https://www.reddit.com/r/SwiftUI/comments/13rhg02/how_can_i_use_navigationlink_inside_list_without/jlkqbkz/ 32 | overlay { 33 | NavigationLink(value: value) { 34 | EmptyView() 35 | } 36 | .opacity(0) 37 | } 38 | } else { 39 | // If hideChevron is false, return the original view without modification 40 | self 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/Color+Hex.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftUI 16 | 17 | extension Color { 18 | init(hex: String, opacity: Double = 1.0) { 19 | var hexFormatted: String = hex.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) 20 | .uppercased() 21 | 22 | if hexFormatted.hasPrefix("#") { 23 | hexFormatted = String(hexFormatted.dropFirst()) 24 | } 25 | 26 | assert(hexFormatted.count == 6 || hexFormatted.count == 8, "Invalid hex code") 27 | 28 | var rgbValue: UInt64 = 0 29 | Scanner(string: hexFormatted).scanHexInt64(&rgbValue) 30 | 31 | var alpha: Double = opacity 32 | if hexFormatted.count == 8 { 33 | alpha = Double((rgbValue & 0xFF00_0000) >> 24) / 255.0 34 | } 35 | 36 | let red = Double((rgbValue & 0x00FF_0000) >> 16) / 255.0 37 | let green = Double((rgbValue & 0x0000_FF00) >> 8) / 255.0 38 | let blue = Double(rgbValue & 0x0000_00FF) / 255.0 39 | 40 | self 41 | .init(.sRGB, red: red, green: green, blue: blue, 42 | opacity: alpha) // Use alpha instead of opacity 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/Integration/IntegrationTestBase.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import XCTest 16 | 17 | import FirebaseCore 18 | @testable import FirebaseDataConnect 19 | import Foundation 20 | 21 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 22 | class IntegrationTestBase: XCTestCase { 23 | static var defaultApp: FirebaseApp? 24 | 25 | static var options: FirebaseOptions = { 26 | let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:testAppId", 27 | gcmSenderID: "00000000000000000-00000000000-000000000") 28 | options.projectID = "fdc-test" 29 | options.apiKey = "testDummyApiKey" 30 | return options 31 | }() 32 | 33 | var fakeConnectorConfigOne = ConnectorConfig( 34 | serviceId: "dataconnect", 35 | location: "us-central1", 36 | connector: "kitchensink" 37 | ) 38 | 39 | override class func setUp() { 40 | if defaultApp == nil { 41 | FirebaseApp.configure(options: options) 42 | defaultApp = FirebaseApp.app() 43 | FirebaseConfiguration.shared.setLoggerLevel(.debug) 44 | } 45 | DataConnect.kitchenSinkConnector.useEmulator(port: 3628) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/DataConnectSettings.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 18 | public struct DataConnectSettings: Hashable, Equatable, Sendable { 19 | public let host: String 20 | public let port: Int 21 | public let sslEnabled: Bool 22 | public let cacheSettings: CacheSettings? 23 | 24 | public init(host: String, port: Int, sslEnabled: Bool, 25 | cacheSettings: CacheSettings? = CacheSettings()) { 26 | self.host = host 27 | self.port = port 28 | self.sslEnabled = sslEnabled 29 | self.cacheSettings = cacheSettings 30 | } 31 | 32 | public init() { 33 | host = "firebasedataconnect.googleapis.com" 34 | port = 443 35 | sslEnabled = true 36 | cacheSettings = CacheSettings() 37 | } 38 | 39 | public func hash(into hasher: inout Hasher) { 40 | hasher.combine(host) 41 | hasher.combine(port) 42 | hasher.combine(sslEnabled) 43 | } 44 | 45 | public static func == (lhs: DataConnectSettings, rhs: DataConnectSettings) -> Bool { 46 | return lhs.host == rhs.host && 47 | lhs.port == rhs.port && 48 | lhs.sslEnabled == rhs.sslEnabled 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/OptionalVarWrapper.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 18 | @propertyWrapper 19 | public struct OptionalVariable where Value: Encodable { 20 | public private(set) var isSet = false 21 | 22 | public var wrappedValue: Value? { 23 | didSet { 24 | isSet = true 25 | } 26 | } 27 | 28 | // init called when var isn't initialized 29 | // it is important to define this otherwise the var gets initialized with nil value 30 | 31 | public init() { 32 | wrappedValue = nil 33 | isSet = false 34 | } 35 | 36 | // init called with explicit initialization either with nil or value 37 | public init(wrappedValue initialValue: Value?) { 38 | wrappedValue = initialValue 39 | isSet = true 40 | } 41 | 42 | public var projectedValue: Self { 43 | return self 44 | } 45 | } 46 | 47 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 48 | extension OptionalVariable: Encodable { 49 | public func encode(to encoder: Encoder) throws { 50 | if isSet { 51 | var container = encoder.singleValueContainer() 52 | try container.encode(wrappedValue) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SwiftPackageManager.md: -------------------------------------------------------------------------------- 1 | # Swift Package Manager for Firebase 2 | 3 | ## Requirements 4 | 5 | - Requires Xcode 15.2 or above 6 | - See [Package.swift](Package.swift) for supported platform versions. 7 | 8 | ## Installation 9 | 10 | ### Installing from Xcode 11 | 12 | Add a package by selecting `File` → `Add Packages…` in Xcode’s menu bar. 13 | 14 | 15 | 16 | --- 17 | 18 | Search for the Firebase Data Connect Apple SDK using the repo's URL: 19 | ```console 20 | https://github.com/firebase/data-connect-ios-sdk.git 21 | ``` 22 | 23 | Next, set the **Dependency Rule** to be `Up to Next Major Version`. 24 | 25 | Then, select **Add Package**. 26 | 27 | 28 | 29 | --- 30 | 31 | Choose the product FirebaseDataConnect to install in your app. 32 | 33 | 34 | ### Alternatively, add Firebase Data Connect to a `Package.swift` manifest 35 | 36 | To integrate via a `Package.swift` manifest instead of Xcode, you can add 37 | Firebase Data Connect to the dependencies array of your package: 38 | 39 | ```swift 40 | dependencies: [ 41 | .package( 42 | url: "https://github.com/firebase/data-connect-ios-sdk.git", 43 | .upToNextMajor(from: "11.3.0") 44 | ), 45 | 46 | // Any other dependencies you have... 47 | ], 48 | ``` 49 | 50 | Then, in any target that depends on Firebase Data Connect, add it to the `dependencies` 51 | array of that target: 52 | 53 | ```swift 54 | .target( 55 | name: "MyTargetName", 56 | dependencies: [ 57 | .product(name: "FirebaseDataConnect", package: "data-connect-ios-sdk"), 58 | ] 59 | ), 60 | ``` 61 | 62 | ## Questions and Issues 63 | 64 | Please provide any feedback via a [GitHub 65 | Issue](https://github.com/firebase/firebase-ios-sdk/issues/new?template=bug_report.md). 66 | 67 | See current open Swift Package Manager issues 68 | [here]([https://github.com/firebase/firebase-ios-sdk/labels/Swift%20Package%20Manager](https://github.com/firebase/firebase-ios-sdk/issues?q=is%3Aopen+label%3A%22Swift+Package+Manager%22+sort%3Acomments-desc)). 69 | -------------------------------------------------------------------------------- /Sources/Queries/QueryRef.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | @preconcurrency import Combine 18 | import Observation 19 | 20 | /// The type of publisher to use for the Query Ref 21 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 22 | public enum ResultsPublisherType { 23 | /// automatically determine ObservableQueryRef. 24 | /// Tries to pick the iOS 17+ Observation but falls back to ObservableObject 25 | case auto 26 | 27 | /// pre-iOS 17 ObservableObject 28 | case observableObject 29 | 30 | /// iOS 17+ Observation framework 31 | case observableMacro 32 | } 33 | 34 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 35 | public protocol QueryRef: OperationRef, Equatable, Hashable { 36 | /// This call starts query execution and publishes data 37 | /// Subscribe always returns cache data first while it attempts to fetch from the server 38 | func subscribe() async throws -> AnyPublisher, 40 | AnyDataConnectError 41 | >, Never> 42 | 43 | /// Execute override for queries to include fetch policy. Defaults to `preferCache` policy 44 | func execute(fetchPolicy: QueryFetchPolicy) async throws -> OperationResult 45 | } 46 | 47 | public extension QueryRef { 48 | // default implementation for execute() 49 | func execute() async throws -> OperationResult { 50 | try await execute(fetchPolicy: .preferCache) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Reviews/MovieReviewCard.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftUI 16 | 17 | struct MovieReviewCard: View { 18 | var title: String 19 | var rating: Double 20 | var reviewerName: String 21 | var review: String 22 | 23 | var body: some View { 24 | VStack(alignment: .leading, spacing: 10) { 25 | Text(title) 26 | .font(.headline) 27 | HStack { 28 | StarRatingView(rating: rating) 29 | Text("·") 30 | Text(reviewerName) 31 | } 32 | .font(.subheadline) 33 | Text(review) 34 | Spacer() 35 | } 36 | .padding(16) 37 | .frame(height: 200) 38 | .background(Color(UIColor.secondarySystemBackground)) 39 | .clipShape( 40 | UnevenRoundedRectangle( 41 | cornerRadii: .init( 42 | topLeading: 16, 43 | bottomLeading: 16, 44 | bottomTrailing: 16, 45 | topTrailing: 16 46 | ), 47 | style: .continuous 48 | ) 49 | ) 50 | } 51 | } 52 | 53 | #Preview { 54 | ScrollView { 55 | MovieReviewCard( 56 | title: "Really great", 57 | rating: 4.5, 58 | reviewerName: "John Doe", 59 | review: 60 | "Velit officia quis ut ut dolor velit voluptate magna Lorem. Sint do ex adipisicing laboris magna et duis aute fugiat culpa minim id culpa nulla do. Occaecat in anim ad Lorem eu aute consectetur excepteur fugiat laboris eiusmod. Et tempor Lorem quis eu magna cillum adipisicing consectetur." 61 | ) 62 | .padding() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/MutationRef.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 18 | struct MutationRequest: OperationRequest { 19 | private(set) var operationName: String 20 | private(set) var variables: Variable? 21 | 22 | init(operationName: String, variables: Variable? = nil) { 23 | self.operationName = operationName 24 | self.variables = variables 25 | } 26 | } 27 | 28 | /// Represents a predefined graphql mutation identified by name and variables. 29 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 30 | public class MutationRef< 31 | ResultData: Decodable & Sendable, 32 | Variable: OperationVariable 33 | >: OperationRef { 34 | private var request: MutationRequest 35 | 36 | private var grpcClient: GrpcClient 37 | 38 | init(request: MutationRequest, grpcClient: GrpcClient) { 39 | self.request = request 40 | self.grpcClient = grpcClient 41 | } 42 | 43 | public func execute() async throws -> OperationResult { 44 | let results = try await grpcClient.executeMutation( 45 | request: request, 46 | resultType: ResultData.self 47 | ) 48 | return results 49 | } 50 | 51 | public func hash(into hasher: inout Hasher) { 52 | hasher.combine(request) 53 | } 54 | 55 | public static func == (lhs: MutationRef, 56 | rhs: MutationRef) -> Bool { 57 | return lhs.request == rhs.request 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/Integration/ConfigSetup.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | import FirebaseCore 18 | import FirebaseDataConnect 19 | 20 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 21 | enum KitchenSinkError: Error { 22 | case configureFailed 23 | } 24 | 25 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 26 | actor ProjectConfigurator { 27 | static let shared = ProjectConfigurator() 28 | 29 | private init() {} 30 | 31 | private var setupComplete = false 32 | 33 | func configureProject(useDummyEngine: Bool = true) async throws { 34 | guard !setupComplete else { 35 | // setup already complete 36 | return 37 | } 38 | 39 | guard let resourcePath = Bundle.module.resourcePath 40 | else { throw KitchenSinkError.configureFailed } 41 | let projectDirPath = URL(fileURLWithPath: resourcePath) 42 | .appendingPathComponent("fdc-kitchensink/dataconnect").path 43 | 44 | let configureBody = """ 45 | { 46 | "service_id": "\(KitchenSinkConnector.connectorConfig.serviceId)", 47 | "config_directory": "\(projectDirPath)", 48 | "use_dummy": \(useDummyEngine) 49 | }' 50 | 51 | """ 52 | 53 | let configureUrl = URL(string: "http://127.0.0.1:3628/emulator/configure")! 54 | var configureRequest = URLRequest(url: configureUrl) 55 | configureRequest.httpMethod = "POST" 56 | 57 | let (_, response) = try await URLSession.shared.upload( 58 | for: configureRequest, 59 | from: configureBody.data(using: .utf8)! 60 | ) 61 | setupComplete = true 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/App/FriendlyFlixApp.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Firebase 16 | import FirebaseAuth 17 | import FirebaseDataConnect 18 | import FriendlyFlixSDK 19 | import os 20 | import SwiftUI 21 | 22 | @main 23 | struct FriendlyFlixApp: App { 24 | private let logger = Logger(subsystem: "FriendlyFlix", category: "configuration") 25 | 26 | var authenticationService: AuthenticationService? 27 | 28 | /// Determines whether to use the Firebase Local Emulator Suite. 29 | /// To use the local emulator, go to the active scheme, and add `-useEmulator YES` 30 | /// to the _Arguments Passed On Launch_ section. 31 | public var useEmulator: Bool { 32 | let value = UserDefaults.standard.bool(forKey: "useEmulator") 33 | logger.log("Using the emulator: \(value == true ? "YES" : "NO")") 34 | return value 35 | } 36 | 37 | init() { 38 | FirebaseApp.configure() 39 | if useEmulator { 40 | DataConnect.friendlyFlixConnector.useEmulator(port: 9399) 41 | Auth.auth().useEmulator(withHost: "localhost", port: 9099) 42 | } 43 | 44 | authenticationService = AuthenticationService() 45 | authenticationService?.onSignUp { user in 46 | let userName = String(user.email?.split(separator: "@").first ?? "(unknown)") 47 | Task { 48 | try await DataConnect.friendlyFlixConnector.upsertUserMutation.execute(username: userName) 49 | } 50 | } 51 | } 52 | 53 | var body: some Scene { 54 | WindowGroup { 55 | RootView() 56 | .environment(authenticationService) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/implicit.gql: -------------------------------------------------------------------------------- 1 | extend type FavoriteMovie { 2 | """ 3 | ✨ Implicit foreign key field based on `FavoriteMovie`.`user`. It must match the value of `User`.`id`. See `@ref` for how to customize it. 4 | """ 5 | userId: String! @fdc_generated(from: "FavoriteMovie.user", purpose: IMPLICIT_REF_FIELD) 6 | """ 7 | ✨ Implicit foreign key field based on `FavoriteMovie`.`movie`. It must match the value of `Movie`.`id`. See `@ref` for how to customize it. 8 | """ 9 | movieId: UUID! @fdc_generated(from: "FavoriteMovie.movie", purpose: IMPLICIT_REF_FIELD) 10 | } 11 | extend type MovieActor { 12 | """ 13 | ✨ Implicit foreign key field based on `MovieActor`.`movie`. It must match the value of `Movie`.`id`. See `@ref` for how to customize it. 14 | """ 15 | movieId: UUID! @fdc_generated(from: "MovieActor.movie", purpose: IMPLICIT_REF_FIELD) 16 | """ 17 | ✨ Implicit foreign key field based on `MovieActor`.`actor`. It must match the value of `Actor`.`id`. See `@ref` for how to customize it. 18 | """ 19 | actorId: UUID! @fdc_generated(from: "MovieActor.actor", purpose: IMPLICIT_REF_FIELD) 20 | } 21 | extend type MovieMetadata { 22 | """ 23 | ✨ Implicit primary key field. It's a UUID column default to a generated new value. See `@table` for how to customize it. 24 | """ 25 | id: UUID! @default(expr: "uuidV4()") @fdc_generated(from: "MovieMetadata", purpose: IMPLICIT_KEY_FIELD) 26 | """ 27 | ✨ Implicit foreign key field based on `MovieMetadata`.`movie`. It must match the value of `Movie`.`id`. See `@ref` for how to customize it. 28 | """ 29 | movieId: UUID! @fdc_generated(from: "MovieMetadata.movie", purpose: IMPLICIT_REF_FIELD) 30 | } 31 | extend type Review { 32 | """ 33 | ✨ Implicit foreign key field based on `Review`.`movie`. It must match the value of `Movie`.`id`. See `@ref` for how to customize it. 34 | """ 35 | movieId: UUID! @fdc_generated(from: "Review.movie", purpose: IMPLICIT_REF_FIELD) 36 | """ 37 | ✨ Implicit foreign key field based on `Review`.`user`. It must match the value of `User`.`id`. See `@ref` for how to customize it. 38 | """ 39 | userId: String! @fdc_generated(from: "Review.user", purpose: IMPLICIT_REF_FIELD) 40 | } 41 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AccountScreen.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftUI 16 | 17 | struct AccountScreen: View { 18 | @Environment(\.dismiss) var dismiss 19 | @Environment(AuthenticationService.self) var authenticationService 20 | 21 | private var displayName: String { 22 | authenticationService.user?.displayName ?? "(not set)" 23 | } 24 | 25 | private var email: String { 26 | authenticationService.user?.email ?? "" 27 | } 28 | 29 | private func signOut() { 30 | do { 31 | try authenticationService.signOut() 32 | dismiss() 33 | } catch {} 34 | } 35 | } 36 | 37 | extension AccountScreen { 38 | var body: some View { 39 | NavigationStack { 40 | List { 41 | Section { 42 | HStack(alignment: .center) { 43 | Image(systemName: "person.circle.fill") 44 | .resizable() 45 | .scaledToFit() 46 | .frame(height: 48) 47 | VStack(alignment: .leading) { 48 | Text(displayName) 49 | Text(email) 50 | } 51 | } 52 | } 53 | 54 | Section { 55 | Button(action: signOut) { 56 | Text("Sign out") 57 | } 58 | } 59 | } 60 | .navigationTitle("Account") 61 | .navigationBarTitleDisplayMode(.inline) 62 | .toolbar { 63 | ToolbarItem { 64 | Button(action: { dismiss() }) { 65 | Text("Done") 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | #Preview { 74 | AccountScreen() 75 | .environment(AuthenticationService()) 76 | } 77 | -------------------------------------------------------------------------------- /Tests/Unit/ErrorsTypes.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Combine 16 | import Foundation 17 | import XCTest 18 | 19 | import FirebaseCore 20 | @testable import FirebaseDataConnect 21 | 22 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 23 | final class ErrorTypesTests: XCTestCase { 24 | private var pubCancellable: AnyCancellable? 25 | private let errPublisher = PassthroughSubject, Never>() 26 | 27 | func throwInitError() throws { 28 | throw DataConnectInitError.appNotConfigured() 29 | } 30 | 31 | func testCatchAsGenericTypes() throws { 32 | do { 33 | try throwInitError() 34 | } catch let dcerr as DataConnectError { 35 | XCTAssertTrue(true) 36 | } 37 | } 38 | 39 | func testPublisherErrorType() throws { 40 | let errExpectation = XCTestExpectation(description: "Expect a Domain Erro") 41 | 42 | pubCancellable = errPublisher.sink(receiveValue: { result in 43 | switch result { 44 | case .success: 45 | XCTFail("Unexpectedly got success. We expect a failure with error") 46 | case let .failure(dcerror): 47 | if let initErr = dcerror.dataConnectError as? DataConnectInitError, 48 | initErr.code == .appNotConfigured { 49 | // got Init domain error 50 | errExpectation.fulfill() 51 | } else { 52 | XCTFail("Did not get DataConnectInitError.appNotConfigured as expected") 53 | } 54 | } 55 | }) 56 | 57 | errPublisher 58 | .send( 59 | .failure(AnyDataConnectError(dataConnectError: DataConnectInitError.appNotConfigured())) 60 | ) 61 | 62 | wait(for: [errExpectation], timeout: 1.0) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | Secrets.tar 4 | 5 | # OS X 6 | .DS_Store 7 | 8 | # Xcode 9 | build/ 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | *.xccheckout 20 | profile 21 | *.moved-aside 22 | DerivedData 23 | *.hmap 24 | *.ipa 25 | 26 | # Swift Package Manager 27 | Package.resolved 28 | **/.build 29 | 30 | # Bad sorts get generated if the package .xcscheme is not regenerated. 31 | # Anything committed to xcshareddata gets propagated to clients. (#8167) 32 | .swiftpm/xcode/xcshareddata/ 33 | .swiftpm/Xcode/package.xcworkspace/xcuserdata/ 34 | 35 | # Mint package manager 36 | Mint 37 | 38 | # IntelliJ 39 | .idea 40 | 41 | # Vim 42 | *.swo 43 | *.swp 44 | *~ 45 | 46 | # Bundler 47 | /.bundle 48 | /vendor 49 | 50 | Carthage 51 | # Cocoapods recommends against adding the Pods directory to your .gitignore. See 52 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 53 | 54 | # Since Firebase is building libraries, not apps, we should not check in Pods. 55 | # Pods are only used in the Examples and tests and doing a 'pod install' better 56 | # matches our customers' environments. 57 | # 58 | # Note: if you ignore the Pods directory, make sure to uncomment 59 | # `pod install` in .travis.yml 60 | # 61 | Pods/ 62 | Podfile.lock 63 | *.xcworkspace 64 | 65 | # CMake 66 | .downloads 67 | Debug 68 | Release 69 | Ninja 70 | 71 | # CLion 72 | /cmake-build-debug 73 | /cmake-build-release 74 | 75 | # Python 76 | *.pyc 77 | 78 | # Visual Studio 79 | /.vs 80 | 81 | # Visual Studio Code 82 | /.vscode 83 | 84 | # clangd support file 85 | compile_commands.json 86 | 87 | # CocoaPods generate 88 | gen/ 89 | 90 | # b/111916494 91 | default.profraw 92 | 93 | 94 | # generated Terraform docs 95 | .terraform/* 96 | .terraform.lock.hcl 97 | *.tfstate 98 | *.tfstate.* 99 | *.xcuserstate 100 | 101 | #firebase-ios-sdk clone 102 | firebase-ios-sdk/ 103 | 104 | #scripts link 105 | scripts 106 | 107 | #xcodebuild log 108 | xcodebuild.log 109 | 110 | Package.resolved 111 | GoogleService-Info.plist 112 | /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/GoogleService-Info.plist 113 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Home/DetailsSection.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftUI 16 | 17 | struct DetailsSection: View where Title: View, Details: View { 18 | var title: () -> Title 19 | var content: () -> Details 20 | 21 | init(@ViewBuilder _ title: @escaping () -> Title, @ViewBuilder content: @escaping () -> Details) { 22 | self.title = title 23 | self.content = content 24 | } 25 | 26 | var body: some View { 27 | VStack(alignment: .leading, spacing: 10) { 28 | HStack(alignment: .center) { 29 | title() 30 | .font(.title2) 31 | .bold() 32 | Image(systemName: "chevron.right") 33 | .font(.title3) 34 | .bold() 35 | .foregroundStyle(Color.secondary) 36 | Spacer() 37 | } 38 | .padding(.bottom, 8) 39 | 40 | content() 41 | } 42 | .padding(.bottom, 20) 43 | } 44 | } 45 | 46 | extension DetailsSection where Title == Text { 47 | init(_ title: Text, content: @escaping () -> Details) { 48 | self.title = { title } 49 | self.content = { content() } 50 | } 51 | 52 | init(_ title: any StringProtocol, content: @escaping () -> Details) { 53 | self.title = { Text(title) } 54 | self.content = { content() } 55 | } 56 | } 57 | 58 | #Preview { 59 | ScrollView { 60 | DetailsSection(Text("Title")) { 61 | Text("Details go here") 62 | } 63 | 64 | DetailsSection("Title as string") { 65 | Text("Details go here") 66 | } 67 | 68 | DetailsSection { 69 | NavigationLink(value: Movie.mock) { 70 | Text("Movie") 71 | } 72 | } content: { 73 | Text("Details go here") 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Queries/QueryRequest.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// A QueryRequest is used to get a QueryRef to a Data Connect query using the specified query name 18 | /// and input variables to the query 19 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 20 | struct QueryRequest: OperationRequest, Hashable, Equatable { 21 | private(set) var operationName: String 22 | private(set) var variables: Variable? 23 | 24 | // Computed requestId 25 | lazy var requestId: String = { 26 | var keyIdData = Data() 27 | if let nameData = operationName.data(using: .utf8) { 28 | keyIdData.append(nameData) 29 | } 30 | 31 | if let variables { 32 | let encoder = JSONEncoder() 33 | encoder.outputFormatting = .sortedKeys 34 | do { 35 | let jsonData = try encoder.encode(variables) 36 | keyIdData.append(jsonData) 37 | } catch { 38 | DataConnectLogger.logger 39 | .warning("Error encoding variables to compute request identifier: \(error)") 40 | } 41 | } 42 | 43 | return keyIdData.sha256String 44 | }() 45 | 46 | init(operationName: String, variables: Variable? = nil) { 47 | self.operationName = operationName 48 | self.variables = variables 49 | } 50 | 51 | // MARK: - Hashable and Equatable implementation 52 | 53 | func hash(into hasher: inout Hasher) { 54 | hasher.combine(operationName) 55 | if let variables { 56 | hasher.combine(variables) 57 | } 58 | } 59 | 60 | static func == (lhs: QueryRequest, rhs: QueryRequest) -> Bool { 61 | guard lhs.operationName == rhs.operationName else { 62 | return false 63 | } 64 | 65 | return lhs.variables == rhs.variables 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tools/SetupDevEnv/SetupDevEnv.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | #if os(macOS) 18 | import ShellExecutor 19 | import TemplateProject 20 | #endif 21 | 22 | // port on which FDC tools (code-server) listen 23 | let FDC_TOOLS_PORT: UInt = 9394 24 | 25 | @available(macOS 12.0, *) 26 | @main 27 | struct SetupDevEnv { 28 | static func main() { 29 | #if os(macOS) 30 | let currentDirectoryPath = FileManager.default.currentDirectoryPath 31 | print( 32 | "ℹ️ Attempting to start Firebase Data Connect Tools in Directory: \(currentDirectoryPath)" 33 | ) 34 | 35 | let executor = ShellExecutor() 36 | 37 | do { 38 | // When the `Start FDC Tools` process is stopped from Xcode, 39 | // it still leaves an orphaned code-server process since SIGKILL isn't received by the 40 | // command line utility 41 | try executor.killProcessOnPort(FDC_TOOLS_PORT) 42 | } catch { 43 | print("❌ Error killing process \(error)") 44 | } 45 | 46 | if !CommandLine.arguments.contains("--skip-template-project") { 47 | do { 48 | let templateManager = TemplateProjectManager() 49 | try templateManager 50 | .copyTemplateProject(to: URL(fileURLWithPath: FileManager.default.currentDirectoryPath)) 51 | 52 | } catch { 53 | print("❌ Error copying template project: \(error)") 54 | } 55 | } else { 56 | print("ℹ️ Skipping copying template project because --skip-template-project was provided") 57 | } 58 | 59 | do { 60 | let commandToRun = "curl -sL https://firebase.tools/dataconnect | TMPDIR=$(mktemp -d) bash" 61 | try executor.run(commandToRun) 62 | } catch { 63 | print("❌ Error running command: \(error)") 64 | } 65 | #endif // if macos 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 11.10.0 2 | - [added] Starting FDC Tools from Xcode sets up a template Firebase Data Connect project in the Xcode project folder. See the `README.md` for an updated `Getting Started` section. 3 | 4 | # 11.9.0 5 | - [added] Start Firebase Data Connect Schema and Query design tools from Xcode. See the `README.md` for more information. 6 | 7 | # 11.8.0 8 | - [changed] Firebase Core SDK dependency version range has been extended to include the 12.x version. [#15139](https://github.com/firebase/firebase-ios-sdk/issues/15139) 9 | 10 | # 11.7.2 11 | - [fixed] `DataConnectOperationError` now includes underlying server error messages in the debug description instead of a generic decoding error. [#14945](https://github.com/firebase/firebase-ios-sdk/issues/14945) 12 | 13 | # 11.7.1 14 | - [fixed] `AnyValue` type fixes to support decoding values fetched using PostgreSQL `jsonb_build_object`. `AnyValue` now internally stores data as a JSON value / dictionary instead of `Swift.Data`. 15 | 16 | # 11.7.0 17 | - [changed] Firebase Data Connect has exited beta and is now generally available for use. 18 | - [changed] **Breaking Change:** Refactored the base `DataConnectError` error type to be a protocol instead of an enum and introduced concrete error types `DataConnectInitError`, `DataConnectCodecError`, `DataConnectOperationError`. Note that if you have code using a `switch` on the previous error enum, you will need to update that code. See the [PR] (https://github.com/firebase/data-connect-ios-sdk/pull/42) for a usage example and `DataConnectError.swift` for implementation details. 19 | - [added] Support for partial errors via the above mentioned `DataConnectOperationError`. 20 | 21 | # 11.6.0-beta 22 | - [changed] Dependency on Firebase iOS SDK changed to 'minimum version required' instead of an 'exact version'. This lets apps use the latest version of the Firebase iOS SDK. 23 | 24 | # 11.5.0-beta 25 | - [added] FriendlyFlix - a comprehensive SwiftUI sample app. 26 | - [changed] Switched to using AppCheckInterop APIs to remove hard dependency on FirebaseAppCheck library. 27 | 28 | # 11.4.0-beta 29 | - [added] Support for Swift 6 strict concurrency. 30 | - [added] Logging within SDKs. 31 | - [changed] SDK will throw errors if partial GraphQL errors are detected during operation execution. 32 | 33 | # 11.3.0-beta 34 | - [added] Initial public preview (pre-announced) release of the SDK. For more information visit 35 | [Firebase Data Connect](https://firebase.google.com/products/data-connect). 36 | -------------------------------------------------------------------------------- /Sources/Internal/Version.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | import GoogleUtilities_Environment 18 | 19 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 20 | struct Version { 21 | static let sdkVersion = "11.10.0" 22 | 23 | // returns value of form gl-PLATFORM_NAME/PLATFORM_VERSION 24 | static func platformVersionHeader() -> String { 25 | let versionString = GULAppEnvironmentUtil.systemVersion() 26 | let platformName = GULAppEnvironmentUtil.applePlatform() 27 | 28 | return "gl-\(platformName)/\(versionString)" 29 | } 30 | 31 | // returns the build time major version of swift 32 | static func swiftVersion() -> String { 33 | #if swift(>=6.2) 34 | return "6.2" 35 | #elseif swift(>=6.1) 36 | return "6.1" 37 | #elseif swift(>=6) 38 | return "6" 39 | #elseif swift(>=5.10) 40 | return "5.10" 41 | #elseif swift(>=5.9) 42 | return "5.9" 43 | #elseif swift(>=5.8) 44 | return "5.8" 45 | #elseif swift(>=5.7) 46 | return "5.7" 47 | #elseif swift(>=5.6) 48 | return "5.6" 49 | #elseif swift(>=5.5) 50 | return "5.5" 51 | #elseif swift(>=5.4) 52 | return "5.4" 53 | #elseif swift(>=5.2) 54 | return "5.2" 55 | #elseif swift(>=5.1) 56 | return "5.1" 57 | #elseif swift(>=5.0) 58 | return "5.0" 59 | #elseif swift(>=4.2) 60 | return "4.2" 61 | #elseif swift(>=4.1) 62 | return "4.1" 63 | #elseif swift(>=4.0) 64 | return "4.0" 65 | #elseif swift(>=3.1) 66 | return "3.1" 67 | #elseif swift(>=3.0) 68 | return "3.0" 69 | #elseif swift(>=2.2) 70 | return "2.2" 71 | #elseif swift(>=2.1) 72 | return "2.1" 73 | #elseif swift(>=2.0) 74 | return "2.0" 75 | #elseif swift(>=1.2) 76 | return "1.2" 77 | #elseif swift(>=1.1) 78 | return "1.1" 79 | #elseif swift(>=1.0) 80 | return "1.0" 81 | #else 82 | return "" 83 | #endif 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/MovieListSection.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import SwiftUI 16 | 17 | struct SectionedMovie: Identifiable, Hashable { 18 | var id = UUID() 19 | var movie: Movie 20 | 21 | static func == (lhs: SectionedMovie, rhs: SectionedMovie) -> Bool { 22 | lhs.id == rhs.id && lhs.movie.id == rhs.movie.id // Assuming MovieRepresentable has an id 23 | } 24 | 25 | func hash(into hasher: inout Hasher) { 26 | hasher.combine(id) 27 | } 28 | } 29 | 30 | struct MovieListSection: View { 31 | // pass namespace from parent view, see https://forums.developer.apple.com/forums/thread/651996 32 | var namespace: Namespace.ID 33 | var title: String 34 | var movies: [Movie] 35 | 36 | private var sectionedMovies: [SectionedMovie] { 37 | movies.map { movie in 38 | SectionedMovie(movie: movie) 39 | } 40 | } 41 | 42 | var body: some View { 43 | DetailsSection { 44 | NavigationLink(value: movies) { 45 | Text(title) 46 | } 47 | .buttonStyle(.noHighlight) 48 | } content: { 49 | ScrollView(.horizontal) { 50 | LazyHStack { 51 | ForEach(sectionedMovies) { sectionedMovie in 52 | NavigationLink(value: sectionedMovie) { 53 | MovieTileView( 54 | title: sectionedMovie.movie.title, 55 | imageUrl: sectionedMovie.movie.imageUrl, 56 | averageRating: sectionedMovie.movie.rating ?? 0, 57 | userRating: 10 58 | ) 59 | .frame(maxWidth: 150, maxHeight: 300) 60 | .matchedTransitionSource(id: sectionedMovie.id, in: namespace) 61 | } 62 | .buttonStyle(.noHighlight) 63 | } 64 | } 65 | } 66 | .scrollIndicators(.never) 67 | } 68 | } 69 | } 70 | 71 | #Preview { 72 | @Previewable @Namespace var namespace 73 | NavigationStack { 74 | MovieListSection(namespace: namespace, title: "Top Movies", movies: Movie.topMovies) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Protos/Readme.md: -------------------------------------------------------------------------------- 1 | # Data Connect protos 2 | This folder contains the Data Connect protos defining the GRPC service. 3 | 4 | # Google Protos 5 | The google protos are obtained from https://github.com/googleapis/googleapis/tree/master 6 | 7 | # Instructions to Generate the Swift files for the protos 8 | 9 | ## Step 1: 10 | (for Googlers) Copy the .proto files from 11 | 12 | `third_party/firebase/dataconnect/emulator/server/api/connector_service.proto` 13 | 14 | `third_party/firebase/dataconnect/emulator/server/api/graphql_error.proto` 15 | 16 | 17 | ## Step 2: 18 | If needed, adjust package name after discussion with server team. 19 | 20 | Make following changes if needed - 21 | - Follow same changes as specified by the copybara lines. 22 | - Where it says strip, take that line out and follow any replace rules with a replace 23 | - If protos reference any internal packages, those are typically not needed by the SDKs or these are marked with copybara strip rules. 24 | 25 | ## Step 3: 26 | Get the standard protoc compiler and ensure that it is in your system path 27 | 28 | ## Step 4: 29 | Get the protoc Swift gen plugin from the releases section of Swift GRPC 30 | 31 | https://github.com/grpc/grpc-swift/releases 32 | 33 | Under the 'Assets' section of a release, download the protoc-grpc-swift-plugins file for macOS. 34 | Example: 35 | protoc-grpc-swift-plugins-1.23.1.zip 36 | 37 | ## Step 5: 38 | Extract this in a location of your choice. You will need the folder path of the extracted folder. 39 | 40 | ## Step 6: 41 | Edit the `build_protos.sh` script that is present in the same folder as this Readme file. 42 | 43 | Adjust the variables that configure the folder paths needed by the script 44 | 45 | Note: If you don't have the protoc compiler in your PATH, you will need specify the full path of the protoc binary in the script above 46 | 47 | ## Step 7: 48 | Open a Terminal at the folder where the script is and run the build_protos.sh script 49 | 50 | `sh build_protos.sh` 51 | 52 | 53 | ## Step 8: 54 | If this script executes successfully (i.e. no errors printed), it will place the generated files in the 55 | `data-connect-ios-sdk/Sources/ProtoGen` folder 56 | 57 | If any of the generated Swift files doesn't have the standard License, insert one. 58 | 59 | ## Step 9: 60 | If the proto package or endpoint names have changed you may get build errors. You will need to adjust the new name in two files 61 | 62 | `data-connect-ios-sdk/Sources/Internal/GrpcClient.swift` 63 | 64 | `data-connect-ios-sdk/Sources/Internal/Codec.swift` 65 | 66 | ## Step 10: 67 | Run Integration tests to confirm that the new protos are working fine. 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /Protos/google/protobuf/empty.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option go_package = "github.com/golang/protobuf/ptypes/empty"; 37 | option java_package = "com.google.protobuf"; 38 | option java_outer_classname = "EmptyProto"; 39 | option java_multiple_files = true; 40 | option objc_class_prefix = "GPB"; 41 | option cc_enable_arenas = true; 42 | 43 | // A generic empty message that you can re-use to avoid defining duplicated 44 | // empty messages in your APIs. A typical example is to use it as the request 45 | // or the response type of an API method. For instance: 46 | // 47 | // service Foo { 48 | // rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); 49 | // } 50 | // 51 | // The JSON representation for `Empty` is empty JSON object `{}`. 52 | message Empty {} 53 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AuthenticationService.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import FirebaseAuth 16 | import Foundation 17 | import Observation 18 | import os 19 | 20 | enum AuthenticationState { 21 | case unauthenticated 22 | case authenticating 23 | case authenticated 24 | } 25 | 26 | @Observable 27 | class AuthenticationService { 28 | private let logger = Logger(subsystem: "FriendlyFlix", category: "auth") 29 | 30 | var presentingAuthenticationDialog = false 31 | var presentingAccountDialog = false 32 | 33 | var authenticationState: AuthenticationState = .unauthenticated 34 | var user: User? 35 | 36 | private var authenticationListener: AuthStateDidChangeListenerHandle? 37 | 38 | init() { 39 | authenticationListener = Auth.auth().addStateDidChangeListener { auth, user in 40 | if let user { 41 | self.authenticationState = .authenticated 42 | self.user = user 43 | } else { 44 | self.authenticationState = .unauthenticated 45 | } 46 | } 47 | } 48 | 49 | private var onSignUp: ((User) -> Void)? 50 | public func onSignUp(_ action: @escaping (User) -> Void) { 51 | onSignUp = action 52 | } 53 | 54 | func signInWithEmailPassword(email: String, password: String) async throws { 55 | try await Auth.auth().signIn(withEmail: email, password: password) 56 | authenticationState = .authenticated 57 | } 58 | 59 | func signUpWithEmailPassword(email: String, password: String) async throws { 60 | try await Auth.auth().createUser(withEmail: email, password: password) 61 | 62 | if let onSignUp, let user = Auth.auth().currentUser { 63 | logger 64 | .debug( 65 | "User signed in \(user.displayName ?? "(no fullname)") with email \(user.email ?? "(no email)")" 66 | ) 67 | onSignUp(user) 68 | } 69 | 70 | authenticationState = .authenticated 71 | } 72 | 73 | func signOut() throws { 74 | try Auth.auth().signOut() 75 | authenticationState = .unauthenticated 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Home/MovieTeaserView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import NukeUI 16 | import SwiftUI 17 | 18 | struct MovieTeaserView: View { 19 | var title: String 20 | var subtitle: String 21 | var imageUrl: String 22 | 23 | var body: some View { 24 | ZStack(alignment: .bottom) { 25 | GeometryReader { geometry in 26 | if let imageUrl = URL(string: imageUrl) { 27 | LazyImage(url: imageUrl) { state in 28 | if let image = state.image { 29 | image 30 | .resizable() 31 | .scaledToFill() 32 | .frame(width: geometry.size.width) 33 | .clipped() 34 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) 35 | } else if state.error != nil { 36 | Color.red 37 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) 38 | .redacted(if: true) 39 | } else { 40 | Image(systemName: "photo.artframe") 41 | .resizable() 42 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) 43 | .redacted(reason: .placeholder) 44 | } 45 | } 46 | } 47 | } 48 | VStack { 49 | Text(title) 50 | .font(.largeTitle) 51 | .foregroundStyle(.white) 52 | Text(subtitle) 53 | .font(.body) 54 | .foregroundStyle(.white) 55 | } 56 | .padding(.horizontal) 57 | .padding(.vertical, 60) 58 | .background { 59 | LinearGradient( 60 | gradient: Gradient(colors: [.clear, .black.opacity(0.9)]), 61 | startPoint: .top, 62 | endPoint: .bottom 63 | ) 64 | } 65 | } 66 | } 67 | } 68 | 69 | #Preview { 70 | var movie = Movie.mock 71 | MovieTeaserView( 72 | title: movie.title, 73 | subtitle: movie.description, 74 | imageUrl: movie.imageUrl 75 | ) 76 | .frame(maxHeight: 400) 77 | } 78 | -------------------------------------------------------------------------------- /Tests/Unit/LocalDateTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import XCTest 16 | 17 | @testable import FirebaseDataConnect 18 | import Foundation 19 | 20 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 21 | final class LocalDateTests: XCTestCase { 22 | override func setUpWithError() throws {} 23 | 24 | override func tearDownWithError() throws {} 25 | 26 | func testEqualityWithDifferentCreationMethods() throws { 27 | let ldString = try LocalDate(localDateString: "2024-05-14") 28 | let ldComponents = try LocalDate(year: 2024, month: 5, day: 14) 29 | 30 | XCTAssertEqual(ldString, ldComponents) 31 | } 32 | 33 | func testEqualitySameDayInstances() throws { 34 | let calendar = Calendar(identifier: .gregorian) 35 | let dc = DateComponents(calendar: calendar, year: 2024, month: 6, day: 1, hour: 6, minute: 5) 36 | 37 | let date = calendar.date(from: dc)! 38 | 39 | let ld1 = LocalDate(date: date) 40 | 41 | let date2 = date.addingTimeInterval(72.0) // add 60 seconds. Should be same day 42 | let ld2 = LocalDate(date: date2) 43 | 44 | XCTAssertEqual(ld1, ld2) 45 | } 46 | 47 | func testLessThan() throws { 48 | let ldLower = try LocalDate(localDateString: "2023-12-29") 49 | let ldHigher = try LocalDate(localDateString: "2024-02-01") 50 | 51 | XCTAssertTrue(ldLower < ldHigher) 52 | } 53 | 54 | func testInvalidLessThan() throws { 55 | let ldLower = try LocalDate(localDateString: "2023-12-29") 56 | let ldHigher = try LocalDate(localDateString: "2024-02-01") 57 | 58 | XCTAssertFalse(ldLower > ldHigher) 59 | } 60 | 61 | func testInvalidDateComponents() throws { 62 | XCTAssertThrowsError(try LocalDate(year: 2024, month: 13, day: 45)) 63 | } 64 | 65 | func testEncodingDecodingJSON() throws { 66 | let ld = try LocalDate(year: 2024, month: 05, day: 14) 67 | 68 | let jsonEncoder = JSONEncoder() 69 | let jsonData = try jsonEncoder.encode(ld) 70 | 71 | let jsonDecoder = JSONDecoder() 72 | let decodedLocalDate = try jsonDecoder.decode(LocalDate.self, from: jsonData) 73 | 74 | XCTAssertEqual(ld, decodedLocalDate) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Cache/CacheSettings.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// Specifies the cache configuration for a `DataConnect` instance. 18 | /// 19 | /// You can configure the cache's storage policy and its maximum size. 20 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 21 | public struct CacheSettings: Sendable { 22 | /// Defines the storage mechanism for the cache. 23 | public enum Storage: Sendable { 24 | /// The cache will be written to disk, persisting data across application launches. 25 | case persistent 26 | /// The cache will only be stored in memory and will be cleared when the application terminates. 27 | case memory 28 | } 29 | 30 | /// The storage mechanism to be used for caching. The default is `.persistent`. 31 | public let storage: Storage 32 | /// The maximum size of the cache in bytes. 33 | /// 34 | /// This size is not strictly enforced but is used as a guideline by the cache 35 | /// to trigger cleanup procedures. The default is 100MB (100,000,000 bytes). 36 | public let maxSizeBytes: UInt64 37 | 38 | /// Max time interval before a queries cache is considered stale and refreshed from the server 39 | /// This interval does not imply that cached data is evicted and it can still be accessed using 40 | /// the `cacheOnly` fetch policy 41 | public let maxAge: TimeInterval 42 | 43 | /// Creates a new cache settings configuration. 44 | /// 45 | /// - Parameters: 46 | /// - storage: The storage mechanism to use. Defaults to `.persistent`. 47 | /// - maxSize: The maximum desired size of the cache in bytes. Defaults to 100MB. 48 | /// - maxAge: The max time interval before a queries cache is considered stale and refreshed 49 | /// from the server. Defaults to zero, implying queries results are always fetched from server and 50 | /// also cached. Results can be fetched from cache using the `cacheOnly` flag. 51 | public init(storage: Storage = .persistent, maxSize: UInt64 = 100_000_000, 52 | maxAge: TimeInterval = 0) { 53 | self.storage = storage 54 | maxSizeBytes = maxSize 55 | self.maxAge = maxAge 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/MovieListRowView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import NukeUI 16 | import SwiftUI 17 | 18 | struct MovieListRowView: View { 19 | var title: String 20 | var subtitle: String 21 | var imageUrl: String 22 | 23 | var body: some View { 24 | HStack(alignment: .top) { 25 | if let imageUrl = URL(string: imageUrl) { 26 | LazyImage(url: imageUrl) { state in 27 | if let image = state.image { 28 | image 29 | .resizable() 30 | .aspectRatio(contentMode: .fill) 31 | .frame(width: 150, height: 75) 32 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) 33 | } else if state.error != nil { 34 | Color.red 35 | .frame(width: 150, height: 75) 36 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) 37 | .redacted(if: true) 38 | } else { 39 | Image(systemName: "photo.artframe") 40 | .resizable() 41 | .aspectRatio(contentMode: .fill) 42 | .frame(width: 150, height: 75) 43 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) 44 | .redacted(reason: .placeholder) 45 | } 46 | } 47 | .frame(width: 150, height: 75) 48 | } 49 | VStack(alignment: .leading, spacing: 4) { 50 | Text(title) 51 | .lineLimit(2) 52 | .font(.headline) 53 | Text(subtitle) 54 | .lineLimit(2) 55 | .font(.subheadline) 56 | } 57 | Spacer() 58 | } 59 | .contentShape(Rectangle()) // ensure entire frame is clickable 60 | } 61 | } 62 | 63 | #Preview { 64 | let movie = Movie.mock 65 | MovieListRowView(title: movie.title, subtitle: movie.description, imageUrl: movie.imageUrl) 66 | } 67 | 68 | #Preview { 69 | NavigationStack { 70 | List(Movie.mockList) { movie in 71 | MovieListRowView(title: movie.title, subtitle: movie.description, imageUrl: movie.imageUrl) 72 | } 73 | .listStyle(.plain) 74 | .navigationTitle("Movies") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy" : { 3 | "accessLevel" : "private" 4 | }, 5 | "indentation" : { 6 | "spaces" : 2 7 | }, 8 | "indentConditionalCompilationBlocks" : true, 9 | "indentSwitchCaseLabels" : false, 10 | "lineBreakAroundMultilineExpressionChainComponents" : false, 11 | "lineBreakBeforeControlFlowKeywords" : false, 12 | "lineBreakBeforeEachArgument" : false, 13 | "lineBreakBeforeEachGenericRequirement" : false, 14 | "lineLength" : 100, 15 | "maximumBlankLines" : 1, 16 | "multiElementCollectionTrailingCommas" : true, 17 | "noAssignmentInExpressions" : { 18 | "allowedFunctions" : [ 19 | "XCTAssertNoThrow" 20 | ] 21 | }, 22 | "prioritizeKeepingFunctionOutputTogether" : false, 23 | "respectsExistingLineBreaks" : true, 24 | "rules" : { 25 | "AllPublicDeclarationsHaveDocumentation" : false, 26 | "AlwaysUseLiteralForEmptyCollectionInit" : false, 27 | "AlwaysUseLowerCamelCase" : true, 28 | "AmbiguousTrailingClosureOverload" : true, 29 | "BeginDocumentationCommentWithOneLineSummary" : false, 30 | "DoNotUseSemicolons" : true, 31 | "DontRepeatTypeInStaticProperties" : true, 32 | "FileScopedDeclarationPrivacy" : true, 33 | "FullyIndirectEnum" : true, 34 | "GroupNumericLiterals" : true, 35 | "IdentifiersMustBeASCII" : true, 36 | "NeverForceUnwrap" : false, 37 | "NeverUseForceTry" : false, 38 | "NeverUseImplicitlyUnwrappedOptionals" : false, 39 | "NoAccessLevelOnExtensionDeclaration" : true, 40 | "NoAssignmentInExpressions" : true, 41 | "NoBlockComments" : true, 42 | "NoCasesWithOnlyFallthrough" : true, 43 | "NoEmptyTrailingClosureParentheses" : true, 44 | "NoLabelsInCasePatterns" : true, 45 | "NoLeadingUnderscores" : false, 46 | "NoParensAroundConditions" : true, 47 | "NoPlaygroundLiterals" : true, 48 | "NoVoidReturnOnFunctionSignature" : true, 49 | "OmitExplicitReturns" : false, 50 | "OneCasePerLine" : true, 51 | "OneVariableDeclarationPerLine" : true, 52 | "OnlyOneTrailingClosureArgument" : true, 53 | "OrderedImports" : true, 54 | "ReplaceForEachWithForLoop" : true, 55 | "ReturnVoidInsteadOfEmptyTuple" : true, 56 | "TypeNamesShouldBeCapitalized" : true, 57 | "UseEarlyExits" : false, 58 | "UseExplicitNilCheckInConditions" : true, 59 | "UseLetInEveryBoundCaseVariable" : true, 60 | "UseShorthandTypeNames" : true, 61 | "UseSingleLinePropertyGetter" : true, 62 | "UseSynthesizedInitializer" : true, 63 | "UseTripleSlashForDocumentationComments" : true, 64 | "UseWhereClausesInForLoops" : false, 65 | "ValidateDocumentationComments" : false 66 | }, 67 | "spacesAroundRangeFormationOperators" : false, 68 | "tabWidth" : 8, 69 | "version" : 1 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Internal/CodableHelpers.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 18 | protocol CodableConverter { 19 | associatedtype E: Encodable 20 | associatedtype D: Decodable 21 | 22 | func encode(input: E) throws -> D 23 | func decode(input: D) throws -> E 24 | } 25 | 26 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 27 | class Int64CodableConverter: CodableConverter { 28 | func encode(input: Int64?) throws -> String? { 29 | guard let input else { 30 | return nil 31 | } 32 | 33 | let int64String = "\(input)" 34 | return int64String 35 | } 36 | 37 | func decode(input: String?) throws -> Int64? { 38 | guard let input else { 39 | return nil 40 | } 41 | 42 | guard let int64Value = Int64(input) else { 43 | throw DataConnectInitError.appNotConfigured() 44 | } 45 | return int64Value 46 | } 47 | } 48 | 49 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 50 | class UUIDCodableConverter: CodableConverter { 51 | func encode(input: UUID?) throws -> String? { 52 | guard let input else { 53 | return nil 54 | } 55 | 56 | let uuidNoDashString = convertToNoDashUUID(uuid: input) 57 | return uuidNoDashString 58 | } 59 | 60 | func decode(input: String?) throws -> UUID? { 61 | guard let input, 62 | let dashesAddedUUID = addDashesToUUIDString(uuidKeyString: input) 63 | else { 64 | return nil 65 | } 66 | 67 | return UUID(uuidString: dashesAddedUUID) 68 | } 69 | 70 | private func convertToNoDashUUID(uuid: UUID) -> String { 71 | return uuid.uuidString.replacingOccurrences(of: "-", with: "").lowercased() 72 | } 73 | 74 | private func addDashesToUUIDString(uuidKeyString: String) -> String? { 75 | guard uuidKeyString.count == 32 else { 76 | return nil 77 | } 78 | 79 | let sourceChars = [Character](uuidKeyString) 80 | var targetChars = [Character]() 81 | 82 | var indx = 0 83 | while indx < sourceChars.count { 84 | switch indx { 85 | case 8, 12, 16, 20: 86 | targetChars.append("-") 87 | default: 88 | break 89 | } 90 | targetChars.append(sourceChars[indx]) 91 | indx += 1 92 | } 93 | 94 | return String(targetChars) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /.github/workflows/spm.yml: -------------------------------------------------------------------------------- 1 | name: spm 2 | 3 | on: 4 | pull_request: 5 | schedule: 6 | # Run every day at 11pm (PST) - cron uses UTC times 7 | - cron: '0 7 * * *' 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | changed_today: 15 | runs-on: ubuntu-latest 16 | name: Check if the repo was updated today 17 | outputs: 18 | WAS_CHANGED: ${{ steps.check_changed.outputs.WAS_CHANGED }} 19 | steps: 20 | - uses: actions/checkout@v4 21 | - id: check_changed 22 | name: Check 23 | if: ${{ github.event_name == 'schedule' }} 24 | run: echo '::set-output name=WAS_CHANGED::'$(test -n "$(git log --format=%H --since='24 hours ago')" && echo 'true' || echo 'false') 25 | 26 | spm-package-resolved: 27 | runs-on: macos-15 28 | outputs: 29 | cache_key: ${{ steps.generate_cache_key.outputs.cache_key }} 30 | env: 31 | FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT: 1 32 | FIREBASE_MAIN: 1 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Xcode 36 | run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer 37 | - name: Generate Swift Package.resolved 38 | id: swift_package_resolve 39 | run: | 40 | swift package resolve 41 | - name: Generate cache key 42 | id: generate_cache_key 43 | run: | 44 | cache_key="${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}" 45 | echo "cache_key=${cache_key}" >> "$GITHUB_OUTPUT" 46 | - uses: actions/cache/save@v4 47 | id: cache 48 | with: 49 | path: .build 50 | key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} 51 | 52 | spm: 53 | needs: [changed_today, spm-package-resolved] 54 | if: ${{ github.event_name == 'pull_request' || needs.changed_today.outputs.WAS_CHANGED == 'true' }} 55 | 56 | strategy: 57 | matrix: 58 | os: [macos-15] 59 | # GitHub actions' runners do not include visionOS. https://github.com/actions/runner-images/issues/10559 60 | target: [iOS, macOS, tvOS, catalyst] 61 | xcode: [Xcode_16.4] 62 | runs-on: ${{ matrix.os }} 63 | env: 64 | FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT: 1 65 | FIREBASE_MAIN: 1 66 | steps: 67 | - uses: actions/checkout@v4 68 | - uses: actions/cache/restore@v4 69 | with: 70 | path: .build 71 | key: ${{needs.spm-package-resolved.outputs.cache_key}} 72 | - name: Xcode 73 | run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer 74 | - name: Setup Scripts Directory 75 | run: ./setup-scripts.sh 76 | - name: Integration Test Setup 77 | run: Tests/Integration/Emulator/start-emulator.sh 78 | - name: Unit and Integration Tests 79 | run: scripts/third_party/travis/retry.sh ./scripts/build.sh FirebaseDataConnect-Package ${{ matrix.target }} spm 80 | -------------------------------------------------------------------------------- /Tests/Integration/Resources/fdc-kitchensink/dataconnect/default/mutations.gql: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | mutation createTestId($id: UUID!) @auth(level: PUBLIC) { 16 | testId_insert(data: { 17 | id: $id 18 | }) 19 | } 20 | 21 | mutation createTestAutoId @auth(level: PUBLIC) { 22 | testAutoId_insert(data: {}) 23 | } 24 | 25 | mutation createStandardScalar($id: UUID!, $number: Int!, $text: String!, $decimal: Float!) @auth(level: PUBLIC) { 26 | standardScalars_insert(data: { 27 | id: $id, 28 | number: $number, 29 | text: $text, 30 | decimal: $decimal 31 | }) 32 | } 33 | 34 | mutation createScalarBoundary($id: UUID!, $maxNumber: Int!, $minNumber: Int!, $maxDecimal: Float!, $minDecimal: Float!) @auth(level: PUBLIC) { 35 | scalarBoundary_insert(data: { 36 | id: $id, 37 | maxNumber: $maxNumber, 38 | minNumber: $minNumber, 39 | maxDecimal: $maxDecimal, 40 | minDecimal: $minDecimal 41 | }) 42 | } 43 | 44 | mutation createLargeNum($id: UUID!, $num: Int64!, $maxNum: Int64!, $minNum: Int64!) @auth(level: PUBLIC) { 45 | largeIntType_insert(data: { 46 | id: $id, 47 | num: $num, 48 | maxNum: $maxNum, 49 | minNum: $minNum 50 | }) 51 | } 52 | 53 | mutation createLocalDate($id: UUID!, $localDate: Date!) @auth(level: PUBLIC) { 54 | localDateType_insert(data: { 55 | id: $id 56 | localDate: $localDate 57 | }) 58 | } 59 | 60 | mutation createAnyValueType($id: UUID!, $props: Any!) @auth(level: PUBLIC) { 61 | anyValueType_insert(data: { 62 | id: $id 63 | props: $props 64 | }) 65 | } 66 | 67 | # Notice how both "inserts" use the same ID; this means that one of them 68 | # will necessarily fail because you can't have two rows with the same ID. 69 | mutation InsertMultiplePeople($id: UUID!, $name1: String!, $name2: String!) @auth(level: PUBLIC) { 70 | person1: person_insert(data: { id: $id, name: $name1 }) 71 | person2: person_insert(data: { id: $id, name: $name2 }) @check(expr: "false") 72 | } 73 | 74 | # ID for second person does not exist. 75 | # so first one will succeed but second should fail 76 | mutation DeleteNonExistentPeople($id: UUID!) @auth(level: PUBLIC) { 77 | person1_insert: person_insert(data: { id: $id, name: "name1" }) 78 | person1: person_delete(id: $id) 79 | person2: person_delete(id: "11A7649B-8FF5-485B-AFBF-2EA5F28894DF") 80 | } 81 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/MovieTileView.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import NukeUI 16 | import SwiftUI 17 | 18 | struct MovieTileView: View { 19 | var title: String = "The Matrix" 20 | var imageUrl: String 21 | var averageRating: Double = 5.7 22 | var userRating: Double = 9 23 | 24 | let gradient = LinearGradient( 25 | colors: [.blue, .green], 26 | startPoint: .leading, 27 | endPoint: .trailing 28 | ) 29 | 30 | private let star = Image(systemName: "star.fill") 31 | 32 | var body: some View { 33 | VStack(alignment: .leading) { 34 | if let imageUrl = URL(string: imageUrl) { 35 | LazyImage(url: imageUrl) { state in 36 | if let image = state.image { 37 | image 38 | .resizable() 39 | .aspectRatio(contentMode: .fill) 40 | .frame(width: 150, height: 200) 41 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) 42 | } else if state.error != nil { 43 | Color.red 44 | .frame(width: 150, height: 200) 45 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) 46 | .redacted(if: true) 47 | } else { 48 | Image(systemName: "photo.artframe") 49 | .resizable() 50 | .aspectRatio(contentMode: .fill) 51 | .frame(width: 150, height: 200) 52 | .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) 53 | .redacted(reason: .placeholder) 54 | } 55 | } 56 | .frame(width: 150, height: 200) 57 | } 58 | Text(title) 59 | .lineLimit(1) 60 | .font(.headline) 61 | HStack { 62 | Text(star) 63 | .foregroundColor(.yellow) + Text(" ") + Text("\(averageRating, specifier: "%.1f")") + 64 | Text(" ") + Text(star) 65 | .foregroundColor(.blue) + Text(" ") + Text("\(userRating, specifier: "%.1f")") 66 | } 67 | } 68 | } 69 | } 70 | 71 | #Preview { 72 | ScrollView(.horizontal) { 73 | LazyHStack { 74 | ForEach(Movie.featured) { movie in 75 | MovieTileView(title: movie.title, 76 | imageUrl: movie.imageUrl, 77 | averageRating: 8, 78 | userRating: 10) 79 | .frame(width: 200, height: 300) 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Tests/Unit/AnyValueCodableTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | import XCTest 17 | 18 | @testable import FirebaseDataConnect 19 | 20 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 21 | final class AnyValueCodableTests: XCTestCase { 22 | override func setUpWithError() throws {} 23 | 24 | override func tearDownWithError() throws {} 25 | 26 | func testAnyValueStringCodable() throws { 27 | let stringVal = "Hello World \(Int.random(in: 1 ... 1000))" 28 | let anyValue = try AnyValue(codableValue: stringVal) 29 | let stringDecoded = try anyValue.decodeValue(String.self) 30 | XCTAssert(stringVal == stringDecoded) 31 | } 32 | 33 | func testAnyValueDoubleRandomCodable() throws { 34 | let doubleVal = Double 35 | .random(in: Double.leastNormalMagnitude ... Double.greatestFiniteMagnitude) 36 | let anyValue = try AnyValue(codableValue: doubleVal) 37 | let doubleDecoded = try anyValue.decodeValue(Double.self) 38 | XCTAssertEqual(doubleVal, doubleDecoded) 39 | } 40 | 41 | func testAnyValueDoubleMaxCodable() throws { 42 | let doubleValMax = Double.greatestFiniteMagnitude 43 | let anyValue = try AnyValue(codableValue: doubleValMax) 44 | let doubleDecoded = try anyValue.decodeValue(Double.self) 45 | XCTAssertEqual(doubleValMax, doubleDecoded) 46 | } 47 | 48 | func testAnyValueDoubleMinCodable() throws { 49 | let doubleValMin = Double.leastNormalMagnitude 50 | let anyValue = try AnyValue(codableValue: doubleValMin) 51 | let doubleDecoded = try anyValue.decodeValue(Double.self) 52 | XCTAssertEqual(doubleValMin, doubleDecoded) 53 | } 54 | 55 | func testAnyValueInt64RandomCodable() throws { 56 | let int64 = Int64.random(in: Int64.min ... Int64.max) 57 | let anyValue = try AnyValue(codableValue: int64) 58 | let int64Decoded = try anyValue.decodeValue(Int64.self) 59 | XCTAssertEqual(int64, int64Decoded) 60 | } 61 | 62 | func testAnyValueInt64MaxCodable() throws { 63 | let int64 = Int64.max 64 | let anyValue = try AnyValue(codableValue: int64) 65 | let int64Decoded = try anyValue.decodeValue(Int64.self) 66 | XCTAssertEqual(int64, int64Decoded) 67 | } 68 | 69 | func testAnyValueInt64MinCodable() throws { 70 | let int64 = Int64.min 71 | let anyValue = try AnyValue(codableValue: int64) 72 | let int64Decoded = try anyValue.decodeValue(Int64.self) 73 | XCTAssertEqual(int64, int64Decoded) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Protos/google/type/latlng.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.type; 18 | 19 | option go_package = "google.golang.org/genproto/googleapis/type/latlng;latlng"; 20 | option java_multiple_files = true; 21 | option java_outer_classname = "LatLngProto"; 22 | option java_package = "com.google.type"; 23 | option objc_class_prefix = "GTP"; 24 | 25 | 26 | // An object representing a latitude/longitude pair. This is expressed as a pair 27 | // of doubles representing degrees latitude and degrees longitude. Unless 28 | // specified otherwise, this must conform to the 29 | // WGS84 30 | // standard. Values must be within normalized ranges. 31 | // 32 | // Example of normalization code in Python: 33 | // 34 | // def NormalizeLongitude(longitude): 35 | // """Wraps decimal degrees longitude to [-180.0, 180.0].""" 36 | // q, r = divmod(longitude, 360.0) 37 | // if r > 180.0 or (r == 180.0 and q <= -1.0): 38 | // return r - 360.0 39 | // return r 40 | // 41 | // def NormalizeLatLng(latitude, longitude): 42 | // """Wraps decimal degrees latitude and longitude to 43 | // [-90.0, 90.0] and [-180.0, 180.0], respectively.""" 44 | // r = latitude % 360.0 45 | // if r <= 90.0: 46 | // return r, NormalizeLongitude(longitude) 47 | // elif r >= 270.0: 48 | // return r - 360, NormalizeLongitude(longitude) 49 | // else: 50 | // return 180 - r, NormalizeLongitude(longitude + 180.0) 51 | // 52 | // assert 180.0 == NormalizeLongitude(180.0) 53 | // assert -180.0 == NormalizeLongitude(-180.0) 54 | // assert -179.0 == NormalizeLongitude(181.0) 55 | // assert (0.0, 0.0) == NormalizeLatLng(360.0, 0.0) 56 | // assert (0.0, 0.0) == NormalizeLatLng(-360.0, 0.0) 57 | // assert (85.0, 180.0) == NormalizeLatLng(95.0, 0.0) 58 | // assert (-85.0, -170.0) == NormalizeLatLng(-95.0, 10.0) 59 | // assert (90.0, 10.0) == NormalizeLatLng(90.0, 10.0) 60 | // assert (-90.0, -10.0) == NormalizeLatLng(-90.0, -10.0) 61 | // assert (0.0, -170.0) == NormalizeLatLng(-180.0, 10.0) 62 | // assert (0.0, -170.0) == NormalizeLatLng(180.0, 10.0) 63 | // assert (-90.0, 10.0) == NormalizeLatLng(270.0, 10.0) 64 | // assert (90.0, 10.0) == NormalizeLatLng(-270.0, 10.0) 65 | message LatLng { 66 | // The latitude in degrees. It must be in the range [-90.0, +90.0]. 67 | double latitude = 1; 68 | 69 | // The longitude in degrees. It must be in the range [-180.0, +180.0]. 70 | double longitude = 2; 71 | } 72 | -------------------------------------------------------------------------------- /Tests/Integration/PartialErrorsTest.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import XCTest 16 | 17 | import FirebaseCore 18 | @testable import FirebaseDataConnect 19 | 20 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 21 | final class PartialErrorTests: IntegrationTestBase { 22 | override func setUp(completion: @escaping ((any Error)?) -> Void) { 23 | Task { 24 | do { 25 | try await ProjectConfigurator.shared.configureProject() 26 | completion(nil) 27 | } catch { 28 | completion(error) 29 | } 30 | } 31 | } 32 | 33 | // Tests for insertion of duplicate primary keys. 34 | // Second insert should fail 35 | // Decode should fail so there isn't any partially encoded data 36 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 37 | func testDuplicatePrimaryKeys() async throws { 38 | let id = UUID() 39 | 40 | do { 41 | let results = try await DataConnect.kitchenSinkConnector.insertMultiplePeopleMutation.execute( 42 | id: id, name1: "name1", name2: "name2" 43 | ).data 44 | print("results \(results)") 45 | } catch let dcError as DataConnectOperationError { 46 | guard let response = dcError.response else { 47 | XCTAssertFalse(false, "No response received from partial error") 48 | throw dcError 49 | } 50 | 51 | let foo1Path = [DataConnectPathSegment.field("person2")] 52 | let error = response.errors.first(where: { $0.path == foo1Path }) 53 | XCTAssertNotNil(error) 54 | 55 | } catch { 56 | XCTFail("Did not throw OperationError \(error)") 57 | } 58 | } 59 | 60 | // Test for partially decoded data 61 | // Second delete fails but first succeeds and returns data 62 | // We test for existence and equality of first key. 63 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 64 | func testPartiallyDecodedData() async throws { 65 | let id = UUID() 66 | do { 67 | _ = try await DataConnect.kitchenSinkConnector.deleteNonExistentPeopleMutation.execute(id: id) 68 | } catch let dcError as DataConnectOperationError { 69 | guard let response = dcError.response else { 70 | XCTAssertFalse(false, "No response received from partial error") 71 | throw dcError 72 | } 73 | 74 | if let data = response.data( 75 | asType: DeleteNonExistentPeopleMutation.Data.self 76 | ) { 77 | XCTAssertNil(data.person2) 78 | XCTAssertTrue(data.person1?.id == id) 79 | } else { 80 | XCTFail("Partial Data is nil. We should have got back partially decoded data") 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/dataconnect/movie-connector/mutations.gql: -------------------------------------------------------------------------------- 1 | mutation UpsertUser($username: String!) @auth(level: USER) { 2 | user_upsert( 3 | data: { 4 | id_expr: "auth.uid" 5 | username: $username 6 | } 7 | ) 8 | } 9 | 10 | # Add a movie to the user's favorites list 11 | mutation AddFavoritedMovie($movieId: UUID!) @auth(level: USER) { 12 | favorite_movie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId }) 13 | } 14 | 15 | # Remove a movie from the user's favorites list 16 | mutation DeleteFavoritedMovie($movieId: UUID!) @auth(level: USER) { 17 | favorite_movie_delete(key: { userId_expr: "auth.uid", movieId: $movieId }) 18 | } 19 | 20 | # Add a review for a movie 21 | mutation AddReview($movieId: UUID!, $rating: Int!, $reviewText: String!) 22 | @auth(level: USER) { 23 | review_insert( 24 | data: { 25 | userId_expr: "auth.uid" 26 | movieId: $movieId 27 | rating: $rating 28 | reviewText: $reviewText 29 | reviewDate_date: { today: true } 30 | } 31 | ) 32 | } 33 | 34 | # Update a user's review for a movie 35 | mutation UpdateReview($movieId: UUID!, $rating: Int!, $reviewText: String!) 36 | @auth(level: USER) { 37 | review_update( 38 | key: { userId_expr: "auth.uid", movieId: $movieId } 39 | data: { 40 | userId_expr: "auth.uid" 41 | movieId: $movieId 42 | rating: $rating 43 | reviewText: $reviewText 44 | reviewDate_date: { today: true } 45 | } 46 | ) 47 | } 48 | 49 | # Delete a user's review for a movie 50 | mutation DeleteReview($movieId: UUID!) @auth(level: USER) { 51 | review_delete(key: { userId_expr: "auth.uid", movieId: $movieId }) 52 | } 53 | 54 | # The mutations below are unused by the application, but are useful examples for more complex cases 55 | 56 | # Create a movie based on user input 57 | # mutation CreateMovie( 58 | # $title: String! 59 | # $releaseYear: Int! 60 | # $genre: String! 61 | # $rating: Float 62 | # $description: String 63 | # $imageUrl: String! 64 | # $tags: [String!] = [] 65 | # ) @auth(expr: "auth.token.isAdmin == true") { 66 | 67 | # } 68 | # Update movie information based on the provided ID 69 | # mutation UpdateMovie( 70 | # $id: UUID! 71 | # $title: String 72 | # $releaseYear: Int 73 | # $genre: String 74 | # $rating: Float 75 | # $description: String 76 | # $imageUrl: String 77 | # $tags: [String!] = [] 78 | # ) @auth(level: USER_EMAIL_VERIFIED) { 79 | # movie_update( 80 | # id: $id 81 | # data: { 82 | # title: $title 83 | # releaseYear: $releaseYear 84 | # genre: $genre 85 | # rating: $rating 86 | # description: $description 87 | # imageUrl: $imageUrl 88 | # tags: $tags 89 | # } 90 | # ) 91 | # } 92 | 93 | # Delete a movie by its ID 94 | # mutation DeleteMovie($id: UUID!) @auth(level: USER_EMAIL_VERIFIED) { 95 | # movie_delete(id: $id) 96 | # } 97 | 98 | # Delete movies with a rating lower than the specified minimum rating 99 | # mutation DeleteUnpopularMovies($minRating: Float!) @auth(level: USER_EMAIL_VERIFIED) { 100 | # movie_deleteMany(where: { rating: { le: $minRating } }) 101 | # } 102 | # End of example mutations -------------------------------------------------------------------------------- /Examples/FriendlyFlix/dataconnect/schema/schema.gql: -------------------------------------------------------------------------------- 1 | # Movies 2 | # TODO: Fill out Movie table 3 | type Movie 4 | # The below parameter values are generated by default with @table, and can be edited manually. 5 | @table { 6 | # implicitly calls @col to generates a column name. ex: @col(name: "movie_id") 7 | id: UUID! @default(expr: "uuidV4()") 8 | title: String! 9 | imageUrl: String! 10 | releaseYear: Int 11 | genre: String 12 | rating: Float 13 | description: String 14 | tags: [String] 15 | # descriptionEmbedding: Vector @col(size:768) # Enables vector search 16 | } 17 | 18 | # Movie Metadata 19 | # Movie - MovieMetadata is a one-to-one relationship 20 | # TODO: Fill out MovieMetadata table 21 | type MovieMetadata 22 | @table { 23 | # @ref creates a field in the current table (MovieMetadata) 24 | # It is a reference that holds the primary key of the referenced type 25 | # In this case, @ref(fields: "movieId", references: "id") is implied 26 | movie: Movie! @ref 27 | # movieId: UUID <- this is created by the above @ref 28 | director: String 29 | } 30 | 31 | # Actors 32 | # Suppose an actor can participate in multiple movies and movies can have multiple actors 33 | # Movie - Actors (or vice versa) is a many to many relationship 34 | # TODO: Fill out Actor table 35 | type Actor @table { 36 | id: UUID! 37 | imageUrl: String! 38 | name: String! @col(name: "name", dataType: "varchar(30)") 39 | } 40 | 41 | # Users 42 | # Suppose a user can leave reviews for movies 43 | # user-reviews is a one to many relationship, movie-reviews is a one to many relationship, movie:user is a many to many relationship 44 | # TODO: Fill out User table 45 | type User 46 | @table { 47 | id: String! @col(name: "user_auth") 48 | username: String! @col(name: "username", dataType: "varchar(50)") 49 | # The following are generated from the @ref in the Review table 50 | # reviews_on_user 51 | # movies_via_Review 52 | } 53 | 54 | # Reviews 55 | # TODO: Fill out Review table 56 | type Review @table(name: "Reviews", key: ["movie", "user"]) { 57 | id: UUID! @default(expr: "uuidV4()") 58 | user: User! 59 | movie: Movie! 60 | rating: Int 61 | reviewText: String 62 | reviewDate: Date! @default(expr: "request.time") 63 | } 64 | 65 | # Join table for many-to-many relationship for movies and actors 66 | # The 'key' param signifies the primary key(s) of this table 67 | # In this case, the keys are [movieId, actorId], the generated fields of the reference types [movie, actor] 68 | # TODO: Fill out MovieActor table 69 | type MovieActor @table(key: ["movie", "actor"]) { 70 | # @ref creates a field in the current table (MovieActor) that holds the primary key of the referenced type 71 | # In this case, @ref(fields: "id") is implied 72 | movie: Movie! 73 | # movieId: UUID! <- this is created by the implied @ref, see: implicit.gql 74 | 75 | actor: Actor! 76 | # actorId: UUID! <- this is created by the implied @ref, see: implicit.gql 77 | 78 | role: String! # "main" or "supporting" 79 | } 80 | 81 | # Join table for many-to-many relationship for users and favorite movies 82 | # TODO: Fill out FavoriteMovie table 83 | type FavoriteMovie 84 | @table(name: "FavoriteMovies", singular: "favorite_movie", plural: "favorite_movies", key: ["user", "movie"]) { 85 | # @ref is implicit 86 | user: User! 87 | movie: Movie! 88 | } -------------------------------------------------------------------------------- /Tests/ShellExecutor/TestExecutor.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import XCTest 16 | 17 | #if os(macOS) 18 | @testable import ShellExecutor 19 | 20 | @available(macOS 12.0, *) 21 | final class ShellExecutorTests: XCTestCase { 22 | var executor: ShellExecutor! 23 | 24 | override func setUp() { 25 | super.setUp() 26 | // This is run before each test 27 | executor = ShellExecutor() 28 | } 29 | 30 | /// Tests that a simple, successful command completes without throwing a Swift error. 31 | func testSuccessfulCommand() { 32 | // The `true` command does nothing and exits with status 0. 33 | XCTAssertNoThrow( 34 | try executor.run("true"), 35 | "A successful command should not throw an error." 36 | ) 37 | } 38 | 39 | /// Tests that a failing command completes without throwing a Swift error, 40 | /// as our function handles the non-zero exit code internally. 41 | func testFailingCommand() { 42 | // The `false` command does nothing and exits with status 1. 43 | XCTAssertNoThrow( 44 | try executor.run("false"), 45 | "A failing command should be handled gracefully, not throw." 46 | ) 47 | } 48 | 49 | /// Tests if the standard output from the command is correctly captured and printed. 50 | func testStandardOutputCapture() throws { 51 | let testString = "Hello from XCTest!" 52 | let command = "echo '\(testString)'" 53 | 54 | // We need to capture the process's stdout to verify it. 55 | // This is a common pattern for testing command-line output. 56 | let outputPipe = Pipe() 57 | 58 | // Save the original standard output file handle 59 | let originalStdout = dup(STDOUT_FILENO) 60 | 61 | // Redirect stdout to our pipe's write end 62 | dup2(outputPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO) 63 | 64 | // Run the command. Its output will go into the pipe. 65 | try executor.run(command) 66 | 67 | // Close the write end of the pipe to signal end-of-file 68 | try outputPipe.fileHandleForWriting.close() 69 | 70 | // Restore the original standard output 71 | dup2(originalStdout, STDOUT_FILENO) 72 | close(originalStdout) 73 | 74 | // Read all data from the pipe 75 | let capturedData = outputPipe.fileHandleForReading.readDataToEndOfFile() 76 | let capturedOutput = String(data: capturedData, encoding: .utf8) ?? "" 77 | 78 | XCTAssertTrue( 79 | capturedOutput.contains(testString), 80 | "The captured output should contain the test string '\(testString)'" 81 | ) 82 | 83 | XCTAssertTrue( 84 | capturedOutput.contains("✅ ShellExecutor: Command finished successfully."), 85 | "The output should contain the success message." 86 | ) 87 | } 88 | } 89 | #endif 90 | -------------------------------------------------------------------------------- /Sources/Internal/CodableTimestamp.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import FirebaseCore 16 | import SwiftProtobuf 17 | 18 | /** 19 | * A protocol describing the encodable properties of a Timestamp. 20 | * 21 | * Note: this protocol exists as a workaround for the Swift compiler: if the Timestamp class 22 | * was extended directly to conform to Codable, the methods implementing the protocol would be need 23 | * to be marked required but that can't be done in an extension. Declaring the extension on the 24 | * protocol sidesteps this issue. 25 | */ 26 | private protocol CodableTimestamp: Codable { 27 | var seconds: Int64 { get } 28 | var nanoseconds: Int32 { get } 29 | 30 | init(seconds: Int64, nanoseconds: Int32) 31 | } 32 | 33 | /** The keys in a Timestamp. Must match the properties of CodableTimestamp. */ 34 | private enum TimestampKeys: String, CodingKey { 35 | case seconds 36 | case nanoseconds 37 | } 38 | 39 | /** 40 | * An extension of Timestamp that implements the behavior of the Codable protocol. 41 | * 42 | * Note: this is implemented manually here because the Swift compiler can't synthesize these methods 43 | * when declaring an extension to conform to Codable. 44 | */ 45 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 46 | extension CodableTimestamp { 47 | public init(from decoder: any Swift.Decoder) throws { 48 | let container = try decoder.singleValueContainer() 49 | let timestampString = try container.decode(String.self).uppercased() 50 | 51 | guard CodableTimestampHelper.regex 52 | .firstMatch(in: timestampString, range: NSRange(location: 0, 53 | length: timestampString.count)) != 54 | nil else { 55 | DataConnectLogger.error( 56 | "Timestamp string format \(timestampString) is not supported." 57 | ) 58 | throw DataConnectCodecError.invalidTimestampFormat() 59 | } 60 | 61 | let buf: Google_Protobuf_Timestamp = 62 | try! Google_Protobuf_Timestamp(jsonString: "\"\(timestampString)\"") 63 | self.init(seconds: buf.seconds, nanoseconds: buf.nanos) 64 | } 65 | 66 | public func encode(to encoder: any Swift.Encoder) throws { 67 | // timestamp to string 68 | var container = encoder.singleValueContainer() 69 | let bufString = try! Google_Protobuf_Timestamp(seconds: seconds, nanos: nanoseconds) 70 | .jsonString() 71 | let timestampString = bufString.trimmingCharacters(in: CharacterSet(charactersIn: "\"")) 72 | try container.encode(timestampString) 73 | } 74 | } 75 | 76 | /** Extends Timestamp to conform to Codable. */ 77 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 78 | extension Timestamp: CodableTimestamp {} 79 | 80 | class CodableTimestampHelper { 81 | static let regex = 82 | try! NSRegularExpression( 83 | pattern: #"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{0,9})?(Z|[+-]\d{2}:\d{2})$"# 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix.xcodeproj/xcshareddata/xcschemes/FriendlyFlix.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 66 | 68 | 74 | 75 | 76 | 77 | 79 | 80 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /Tests/Unit/HeaderTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import FirebaseCore 16 | @testable import FirebaseDataConnect 17 | import Foundation 18 | import GRPC 19 | 20 | import XCTest 21 | 22 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 23 | final class HeaderTests: XCTestCase { 24 | static var defaultApp: FirebaseApp? 25 | 26 | static var options: FirebaseOptions = { 27 | let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:testAppId", 28 | gcmSenderID: "00000000000000000-00000000000-000000000") 29 | options.projectID = "fdc-test" 30 | options.apiKey = "testDummyApiKey" 31 | return options 32 | }() 33 | 34 | var fakeConnectorConfigOne = ConnectorConfig( 35 | serviceId: "dataconnect", 36 | location: "us-central1", 37 | connector: "kitchensink" 38 | ) 39 | 40 | var fakeConnectorConfigTwo = ConnectorConfig( 41 | serviceId: "dataconnect", 42 | location: "us-east1", 43 | connector: "kitchensinkgen" 44 | ) 45 | 46 | override class func setUp() { 47 | FirebaseApp.configure(options: options) 48 | defaultApp = FirebaseApp.app() 49 | } 50 | 51 | func testGmpAppIdHeader() async throws { 52 | let dcOne = DataConnect.dataConnect(connectorConfig: fakeConnectorConfigOne) 53 | let callOptions = await dcOne.grpcClient.createCallOptions() 54 | let values = callOptions.customMetadata.values( 55 | forHeader: GrpcClient.RequestHeaders.firebaseAppId, canonicalForm: false 56 | ) 57 | let contains = values.contains { $0 == HeaderTests.defaultApp!.options.googleAppID } 58 | XCTAssertTrue(contains) 59 | } 60 | 61 | func testGoogApiClientHeaderBaseSdk() async throws { 62 | let dcOne = DataConnect.dataConnect(connectorConfig: fakeConnectorConfigOne) 63 | let callOptions = await dcOne.grpcClient.createCallOptions() 64 | let values = callOptions.customMetadata.values( 65 | forHeader: GrpcClient.RequestHeaders.googApiClient, canonicalForm: false 66 | ) 67 | let contains = values 68 | .contains { 69 | $0 == 70 | "gl-swift/\(Version.swiftVersion()) fire/\(Version.sdkVersion) \(Version.platformVersionHeader()) grpc-swift/" 71 | } 72 | XCTAssertTrue(contains) 73 | } 74 | 75 | func testGoogleApiClientHeaderGenSdk() async throws { 76 | let dcOne = DataConnect.dataConnect( 77 | connectorConfig: fakeConnectorConfigTwo, 78 | callerSDKType: .generated 79 | ) 80 | let callOptions = await dcOne.grpcClient.createCallOptions() 81 | let values = callOptions.customMetadata.values( 82 | forHeader: GrpcClient.RequestHeaders.googApiClient, canonicalForm: false 83 | ) 84 | let contains = values 85 | .contains { 86 | $0 == 87 | "gl-swift/\(Version.swiftVersion()) fire/\(Version.sdkVersion) \(Version.platformVersionHeader()) grpc-swift/ swift/gen" 88 | } 89 | XCTAssertTrue(contains) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Protos/graphql_error.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Adapted from third_party/firebase/dataconnect/emulator/server/api/graphql_error.proto 16 | 17 | syntax = "proto3"; 18 | 19 | package google.firebase.dataconnect.v1; 20 | 21 | import "google/protobuf/struct.proto"; 22 | 23 | option java_package = "com.google.firebase.dataconnect.v1"; 24 | option java_multiple_files = true; 25 | option java_outer_classname = "GraphqlErrorProto"; 26 | 27 | // GraphqlError conforms to the GraphQL error spec. 28 | // https://spec.graphql.org/draft/#sec-Errors 29 | // 30 | // Firebase Data Connect API surfaces `GraphqlError` in various APIs: 31 | // - Upon compile error, `UpdateSchema` and `UpdateConnector` return 32 | // Code.Invalid_Argument with a list of `GraphqlError` in error details. 33 | // - Upon query compile error, `ExecuteGraphql` and `ExecuteGraphqlRead` return 34 | // Code.OK with a list of `GraphqlError` in response body. 35 | // - Upon query execution error, `ExecuteGraphql`, `ExecuteGraphqlRead`, 36 | // `ExecuteMutation` and `ExecuteQuery` all return Code.OK with a list of 37 | // `GraphqlError` in response body. 38 | message GraphqlError { 39 | // The detailed error message. 40 | // The message should help developer understand the underlying problem without 41 | // leaking internal data. 42 | string message = 1; 43 | 44 | // The source locations where the error occurred. 45 | // Locations should help developers and toolings identify the source of error 46 | // quickly. 47 | // 48 | // Included in admin endpoints (`ExecuteGraphql`, `ExecuteGraphqlRead`, 49 | // `UpdateSchema` and `UpdateConnector`) to reference the provided GraphQL 50 | // GQL document. 51 | // 52 | // Omitted in `ExecuteMutation` and `ExecuteQuery` since the caller shouldn't 53 | // have access access the underlying GQL source. 54 | repeated SourceLocation locations = 2; 55 | 56 | // The result field which could not be populated due to error. 57 | // 58 | // Clients can use path to identify whether a null result is intentional or 59 | // caused by a runtime error. 60 | // It should be a list of string or index from the root of GraphQL query 61 | // document. 62 | google.protobuf.ListValue path = 3; 63 | 64 | // Additional error information. 65 | GraphqlErrorExtensions extensions = 4; 66 | } 67 | 68 | // SourceLocation references a location in a GraphQL source. 69 | message SourceLocation { 70 | // Line number starting at 1. 71 | int32 line = 1; 72 | // Column number starting at 1. 73 | int32 column = 2; 74 | } 75 | 76 | // GraphqlErrorExtensions contains additional information of `GraphqlError`. 77 | // (-- TODO(b/305311379): include more detailed error fields: 78 | // go/firemat:api:gql-errors. --) 79 | message GraphqlErrorExtensions { 80 | // The source file name where the error occurred. 81 | // Included only for `UpdateSchema` and `UpdateConnector`, it corresponds 82 | // to `File.path` of the provided `Source`. 83 | string file = 1; 84 | } 85 | -------------------------------------------------------------------------------- /Sources/Internal/Logger/DataConnectLogger.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import FirebaseCore 16 | import os 17 | 18 | let privateLogDisabledArgument = "-FIRPrivateLogDisabled" 19 | 20 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 21 | class DataConnectLogger { 22 | static let logger = Logger( 23 | subsystem: "com.google.firebase", 24 | category: "[FirebaseDataConnect]" 25 | ) 26 | 27 | static let privateLoggingEnabled: Bool = { 28 | let arguments = ProcessInfo.processInfo.arguments 29 | if arguments.contains(privateLogDisabledArgument) { 30 | DataConnectLogger.debug("DataConnect private logging disabled.") 31 | return false 32 | } else { 33 | DataConnectLogger.debug("DataConnect private logging enabled.") 34 | return true 35 | } 36 | }() 37 | 38 | private static let logPrefix = "\(Version.sdkVersion) - [FirebaseDataConnect]" 39 | 40 | static var logLevel: FirebaseLoggerLevel { 41 | return FirebaseConfiguration.shared.loggerLevel() 42 | } 43 | 44 | static func error(_ message: String, code: MessageCode = .placeHolder) { 45 | if logLevel.rawValue >= FirebaseLoggerLevel.error.rawValue { 46 | let messageCode = String(format: "I-FDC%06d", code.rawValue) 47 | logger.error("\(logPrefix)[\(messageCode)] \(message)") 48 | } 49 | } 50 | 51 | static func warning(_ message: String, code: MessageCode = .placeHolder) { 52 | if logLevel.rawValue >= FirebaseLoggerLevel.warning.rawValue { 53 | let messageCode = String(format: "I-FDC%06d", code.rawValue) 54 | logger.warning("\(logPrefix)[\(messageCode)] \(message)") 55 | } 56 | } 57 | 58 | static func notice(_ message: String, code: MessageCode = .placeHolder) { 59 | if logLevel.rawValue >= FirebaseLoggerLevel.notice.rawValue { 60 | let messageCode = String(format: "I-FDC%06d", code.rawValue) 61 | logger.notice("\(logPrefix)[\(messageCode)] \(message)") 62 | } 63 | } 64 | 65 | static func info(_ message: String, code: MessageCode = .placeHolder) { 66 | if logLevel.rawValue >= FirebaseLoggerLevel.info.rawValue { 67 | let messageCode = String(format: "I-FDC%06d", code.rawValue) 68 | logger.info("\(logPrefix)[\(messageCode)] \(message)") 69 | } 70 | } 71 | 72 | static func debug(_ message: String, code: MessageCode = .placeHolder) { 73 | if logLevel.rawValue >= FirebaseLoggerLevel.debug.rawValue { 74 | let messageCode = String(format: "I-FDC%06d", code.rawValue) 75 | logger.debug("\(logPrefix)[\(messageCode)] \(message)") 76 | } 77 | } 78 | } 79 | 80 | enum LogPrivacy { 81 | case `public` 82 | case `private` 83 | } 84 | 85 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 86 | extension DefaultStringInterpolation { 87 | mutating func appendInterpolation(_ value: String, privacy: LogPrivacy = .public) { 88 | if privacy == .private, DataConnectLogger.privateLoggingEnabled { 89 | appendLiteral("") 90 | } else { 91 | appendLiteral(value) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Library/LibraryScreen.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import FirebaseDataConnect 16 | import FriendlyFlixSDK 17 | import SwiftUI 18 | 19 | struct LibraryScreen: View { 20 | @Namespace var namespace 21 | @Environment(AuthenticationService.self) var authenticationService 22 | 23 | private var connector = DataConnect.friendlyFlixConnector 24 | 25 | private var isSignedIn: Bool { 26 | authenticationService.user != nil 27 | } 28 | 29 | init() { 30 | watchListRef = connector.getUserFavoriteMoviesQuery.ref() 31 | } 32 | 33 | private let watchListRef: QueryRefObservation< 34 | GetUserFavoriteMoviesQuery.Data, 35 | GetUserFavoriteMoviesQuery.Variables 36 | > 37 | private var watchList: [Movie] { 38 | watchListRef.data?.user?.favoriteMovies.map(Movie.init) ?? [] 39 | } 40 | 41 | private func presentSignInDialog() { 42 | authenticationService.presentingAuthenticationDialog.toggle() 43 | } 44 | } 45 | 46 | extension LibraryScreen { 47 | var body: some View { 48 | NavigationStack { 49 | ScrollView { 50 | if isSignedIn { 51 | Group { 52 | MovieListSection(namespace: namespace, title: "Watch List", movies: watchList) 53 | .onAppear { 54 | Task { 55 | try await watchListRef.execute() 56 | } 57 | } 58 | // TODO: insert section with list of all movies the user has rated 59 | } 60 | .padding() 61 | } 62 | } 63 | .navigationTitle("Library") 64 | .toolbar { 65 | ToolbarItem(placement: .topBarTrailing) { 66 | AuthenticationToolbarButton() 67 | } 68 | } 69 | .navigationDestination(for: Movie.self) { movie in 70 | MovieCardView(showDetails: true, movie: movie) 71 | .navigationTransition(.zoom(sourceID: movie.id, in: namespace)) 72 | } 73 | .navigationDestination(for: [Movie].self) { movies in 74 | MovieListScreen(namespace: namespace, movies: movies) 75 | } 76 | .navigationDestination(for: SectionedMovie.self) { sectionedMovie in 77 | MovieCardView(showDetails: true, movie: sectionedMovie.movie) 78 | .navigationTransition(.zoom(sourceID: sectionedMovie.id, in: namespace)) 79 | } 80 | } 81 | .overlay { 82 | if !isSignedIn { 83 | ContentUnavailableView { 84 | Label("Your library is empty", systemImage: "rectangle.on.rectangle.slash") 85 | } description: { 86 | VStack { 87 | Text("Your watch list and favourites will appear here once you sign in.") 88 | Button(action: presentSignInDialog) { 89 | Text("Sign in") 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | 98 | #Preview { 99 | LibraryScreen() 100 | .environment(AuthenticationService()) 101 | } 102 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlixSDK/Sources/FriendlyFlixClient.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | import FirebaseCore 18 | import FirebaseDataConnect 19 | 20 | public extension DataConnect { 21 | static let friendlyFlixConnector: FriendlyFlixConnector = { 22 | let dc = DataConnect.dataConnect( 23 | connectorConfig: FriendlyFlixConnector.connectorConfig, 24 | callerSDKType: .generated 25 | ) 26 | return FriendlyFlixConnector(dataConnect: dc) 27 | }() 28 | } 29 | 30 | public class FriendlyFlixConnector { 31 | let dataConnect: DataConnect 32 | 33 | public static let connectorConfig = ConnectorConfig( 34 | serviceId: "dataconnect", 35 | location: "us-central1", 36 | connector: "friendly-flix" 37 | ) 38 | 39 | init(dataConnect: DataConnect) { 40 | self.dataConnect = dataConnect 41 | 42 | // init operations 43 | upsertUserMutation = UpsertUserMutation(dataConnect: dataConnect) 44 | addFavoritedMovieMutation = AddFavoritedMovieMutation(dataConnect: dataConnect) 45 | deleteFavoritedMovieMutation = DeleteFavoritedMovieMutation(dataConnect: dataConnect) 46 | addReviewMutation = AddReviewMutation(dataConnect: dataConnect) 47 | updateReviewMutation = UpdateReviewMutation(dataConnect: dataConnect) 48 | deleteReviewMutation = DeleteReviewMutation(dataConnect: dataConnect) 49 | listMoviesQuery = ListMoviesQuery(dataConnect: dataConnect) 50 | getMovieByIdQuery = GetMovieByIdQuery(dataConnect: dataConnect) 51 | getActorByIdQuery = GetActorByIdQuery(dataConnect: dataConnect) 52 | getCurrentUserQuery = GetCurrentUserQuery(dataConnect: dataConnect) 53 | getIfFavoritedMovieQuery = GetIfFavoritedMovieQuery(dataConnect: dataConnect) 54 | searchAllQuery = SearchAllQuery(dataConnect: dataConnect) 55 | listMoviesByPartialTitleQuery = ListMoviesByPartialTitleQuery(dataConnect: dataConnect) 56 | getUserFavoriteMoviesQuery = GetUserFavoriteMoviesQuery(dataConnect: dataConnect) 57 | } 58 | 59 | public func useEmulator(host: String = DataConnect.EmulatorDefaults.host, 60 | port: Int = DataConnect.EmulatorDefaults.port) { 61 | dataConnect.useEmulator(host: host, port: port) 62 | } 63 | 64 | // MARK: Operations 65 | 66 | public let upsertUserMutation: UpsertUserMutation 67 | public let addFavoritedMovieMutation: AddFavoritedMovieMutation 68 | public let deleteFavoritedMovieMutation: DeleteFavoritedMovieMutation 69 | public let addReviewMutation: AddReviewMutation 70 | public let updateReviewMutation: UpdateReviewMutation 71 | public let deleteReviewMutation: DeleteReviewMutation 72 | public let listMoviesQuery: ListMoviesQuery 73 | public let getMovieByIdQuery: GetMovieByIdQuery 74 | public let getActorByIdQuery: GetActorByIdQuery 75 | public let getCurrentUserQuery: GetCurrentUserQuery 76 | public let getIfFavoritedMovieQuery: GetIfFavoritedMovieQuery 77 | public let searchAllQuery: SearchAllQuery 78 | public let listMoviesByPartialTitleQuery: ListMoviesByPartialTitleQuery 79 | public let getUserFavoriteMoviesQuery: GetUserFavoriteMoviesQuery 80 | } 81 | -------------------------------------------------------------------------------- /Tests/Integration/Gen/KitchenSink/Sources/KitchenSinkClient.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | import FirebaseCore 18 | import FirebaseDataConnect 19 | 20 | public extension DataConnect { 21 | static let kitchenSinkConnector: KitchenSinkConnector = { 22 | let dc = DataConnect.dataConnect( 23 | connectorConfig: KitchenSinkConnector.connectorConfig, 24 | callerSDKType: .generated 25 | ) 26 | return KitchenSinkConnector(dataConnect: dc) 27 | }() 28 | } 29 | 30 | public class KitchenSinkConnector { 31 | let dataConnect: DataConnect 32 | 33 | public static let connectorConfig = ConnectorConfig( 34 | serviceId: "fdc-kitchensink", 35 | location: "us-central1", 36 | connector: "kitchen-sink" 37 | ) 38 | 39 | init(dataConnect: DataConnect) { 40 | self.dataConnect = dataConnect 41 | 42 | // init operations 43 | createTestIdMutation = CreateTestIdMutation(dataConnect: dataConnect) 44 | createTestAutoIdMutation = CreateTestAutoIdMutation(dataConnect: dataConnect) 45 | createStandardScalarMutation = CreateStandardScalarMutation(dataConnect: dataConnect) 46 | createScalarBoundaryMutation = CreateScalarBoundaryMutation(dataConnect: dataConnect) 47 | createLargeNumMutation = CreateLargeNumMutation(dataConnect: dataConnect) 48 | createLocalDateMutation = CreateLocalDateMutation(dataConnect: dataConnect) 49 | createAnyValueTypeMutation = CreateAnyValueTypeMutation(dataConnect: dataConnect) 50 | insertMultiplePeopleMutation = InsertMultiplePeopleMutation(dataConnect: dataConnect) 51 | deleteNonExistentPeopleMutation = DeleteNonExistentPeopleMutation(dataConnect: dataConnect) 52 | getStandardScalarQuery = GetStandardScalarQuery(dataConnect: dataConnect) 53 | getScalarBoundaryQuery = GetScalarBoundaryQuery(dataConnect: dataConnect) 54 | getLargeNumQuery = GetLargeNumQuery(dataConnect: dataConnect) 55 | getLocalDateTypeQuery = GetLocalDateTypeQuery(dataConnect: dataConnect) 56 | getAnyValueTypeQuery = GetAnyValueTypeQuery(dataConnect: dataConnect) 57 | } 58 | 59 | public func useEmulator(host: String = DataConnect.EmulatorDefaults.host, 60 | port: Int = DataConnect.EmulatorDefaults.port) { 61 | dataConnect.useEmulator(host: host, port: port) 62 | } 63 | 64 | // MARK: Operations 65 | 66 | public let createTestIdMutation: CreateTestIdMutation 67 | public let createTestAutoIdMutation: CreateTestAutoIdMutation 68 | public let createStandardScalarMutation: CreateStandardScalarMutation 69 | public let createScalarBoundaryMutation: CreateScalarBoundaryMutation 70 | public let createLargeNumMutation: CreateLargeNumMutation 71 | public let createLocalDateMutation: CreateLocalDateMutation 72 | public let createAnyValueTypeMutation: CreateAnyValueTypeMutation 73 | public let insertMultiplePeopleMutation: InsertMultiplePeopleMutation 74 | public let deleteNonExistentPeopleMutation: DeleteNonExistentPeopleMutation 75 | public let getStandardScalarQuery: GetStandardScalarQuery 76 | public let getScalarBoundaryQuery: GetScalarBoundaryQuery 77 | public let getLargeNumQuery: GetLargeNumQuery 78 | public let getLocalDateTypeQuery: GetLocalDateTypeQuery 79 | public let getAnyValueTypeQuery: GetAnyValueTypeQuery 80 | } 81 | -------------------------------------------------------------------------------- /Protos/connector_service.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | //adopted from third_party/firebase/dataconnect/emulator/server/api/connector_service.proto 17 | syntax = "proto3"; 18 | 19 | package google.firebase.dataconnect.v1; 20 | 21 | import "google/api/annotations.proto"; 22 | import "google/api/field_behavior.proto"; 23 | import "google/protobuf/struct.proto"; 24 | import "graphql_error.proto"; 25 | 26 | option java_package = "com.google.firebase.dataconnect.v1"; 27 | option java_multiple_files = true; 28 | option java_outer_classname = "ConnectorServiceProto"; 29 | 30 | service ConnectorService { 31 | // Execute a predefined query in a Connector. 32 | rpc ExecuteQuery(ExecuteQueryRequest) returns (ExecuteQueryResponse) {} 33 | 34 | // Execute a predefined mutation in a Connector. 35 | rpc ExecuteMutation(ExecuteMutationRequest) 36 | returns (ExecuteMutationResponse) {} 37 | } 38 | 39 | // The ExecuteQuery request to Firebase Data Connect. 40 | message ExecuteQueryRequest { 41 | // The resource name of the connector to find the predefined query, in 42 | // the format: 43 | // ``` 44 | // projects/{project}/locations/{location}/services/{service}/connectors/{connector} 45 | // ``` 46 | string name = 1 [(google.api.field_behavior) = REQUIRED]; 47 | 48 | // The name of the GraphQL operation name. 49 | // Required because all Connector operations must be named. 50 | // See https://graphql.org/learn/queries/#operation-name. 51 | // (-- api-linter: core::0122::name-suffix=disabled 52 | // aip.dev/not-precedent: Must conform to GraphQL HTTP spec standard. --) 53 | string operation_name = 2 [(google.api.field_behavior) = REQUIRED]; 54 | 55 | // Values for GraphQL variables provided in this request. 56 | google.protobuf.Struct variables = 3 [(google.api.field_behavior) = OPTIONAL]; 57 | } 58 | 59 | // The ExecuteMutation request to Firebase Data Connect. 60 | message ExecuteMutationRequest { 61 | // The resource name of the connector to find the predefined mutation, in 62 | // the format: 63 | // ``` 64 | // projects/{project}/locations/{location}/services/{service}/connectors/{connector} 65 | // ``` 66 | string name = 1 [(google.api.field_behavior) = REQUIRED]; 67 | 68 | // The name of the GraphQL operation name. 69 | // Required because all Connector operations must be named. 70 | // See https://graphql.org/learn/queries/#operation-name. 71 | // (-- api-linter: core::0122::name-suffix=disabled 72 | // aip.dev/not-precedent: Must conform to GraphQL HTTP spec standard. --) 73 | string operation_name = 2 [(google.api.field_behavior) = REQUIRED]; 74 | 75 | // Values for GraphQL variables provided in this request. 76 | google.protobuf.Struct variables = 3 [(google.api.field_behavior) = OPTIONAL]; 77 | } 78 | 79 | // The ExecuteQuery response from Firebase Data Connect. 80 | message ExecuteQueryResponse { 81 | // The result of executing the requested operation. 82 | google.protobuf.Struct data = 1; 83 | // Errors of this response. 84 | repeated GraphqlError errors = 2; 85 | } 86 | 87 | // The ExecuteMutation response from Firebase Data Connect. 88 | message ExecuteMutationResponse { 89 | // The result of executing the requested operation. 90 | google.protobuf.Struct data = 1; 91 | // Errors of this response. 92 | repeated GraphqlError errors = 2; 93 | } 94 | -------------------------------------------------------------------------------- /Sources/Scalars/AnyValue.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// AnyValue represents the Any graphql scalar, which represents Codable data - scalar data (Int, 18 | /// Double, String, Bool,...) or a JSON object 19 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 20 | public struct AnyValue { 21 | public var value: Data { 22 | do { 23 | let jsonEncoder = JSONEncoder() 24 | let data = try jsonEncoder.encode(anyCodableValue) 25 | return data 26 | } catch { 27 | DataConnectLogger.logger.warning("Error encoding anyCodableValue \(error)") 28 | return Data() 29 | } 30 | } 31 | 32 | private var anyCodableValue: AnyCodableValue 33 | 34 | public init(codableValue: Codable) throws { 35 | do { 36 | if let int64Val = codableValue as? Int64 { 37 | anyCodableValue = .int64(int64Val) 38 | } else { 39 | // to recontruct JSON dictionary, one has to decode it from json data 40 | let jsonEncoder = JSONEncoder() 41 | let jsonData = try jsonEncoder.encode(codableValue) 42 | let jsonDecoder = JSONDecoder() 43 | anyCodableValue = try jsonDecoder.decode(AnyCodableValue.self, from: jsonData) 44 | } 45 | } 46 | } 47 | 48 | public func decodeValue(_ type: T.Type) throws -> T? { 49 | do { 50 | switch anyCodableValue { 51 | case let .int64(int64): 52 | if type == Int64.self { 53 | return int64 as? T 54 | } else { 55 | throw DataConnectCodecError.decodingFailed() 56 | } 57 | default: 58 | let jsonDecoder = JSONDecoder() 59 | let decodedResult = try jsonDecoder.decode(type, from: value) 60 | return decodedResult 61 | } 62 | } 63 | } 64 | } 65 | 66 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 67 | extension AnyValue: Codable { 68 | public init(from decoder: any Swift.Decoder) throws { 69 | var container = try decoder.singleValueContainer() 70 | do { 71 | if let b64Data = try? container.decode(Data.self) { 72 | // backwards compatibility 73 | let jsonDecoder = JSONDecoder() 74 | anyCodableValue = try jsonDecoder.decode(AnyCodableValue.self, from: b64Data) 75 | } else { 76 | let codecHelper = SingleValueCodecHelper() 77 | anyCodableValue = try codecHelper.decodeSingle(AnyCodableValue.self, container: &container) 78 | } 79 | } 80 | } 81 | 82 | public func encode(to encoder: any Encoder) throws { 83 | var container = encoder.singleValueContainer() 84 | let codecHelper = SingleValueCodecHelper() 85 | try codecHelper.encodeSingle(anyCodableValue, container: &container) 86 | } 87 | } 88 | 89 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 90 | extension AnyValue: Equatable { 91 | public static func == (lhs: Self, rhs: Self) -> Bool { 92 | return lhs.value == rhs.value 93 | } 94 | } 95 | 96 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 97 | extension AnyValue: Hashable { 98 | public func hash(into hasher: inout Hasher) { 99 | hasher.combine(value) 100 | } 101 | } 102 | 103 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 104 | extension AnyValue: Sendable {} 105 | -------------------------------------------------------------------------------- /Tools/TemplateProject/TemplateProject.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | /// This struct is for internal Firebase Data Connect use only 18 | public struct TemplateProjectManager { 19 | public init() {} 20 | 21 | /// Copies a folder named "Templates" from the package's resource bundle 22 | /// to the current working directory. 23 | public func copyTemplateProject(to directoryURL: URL? = nil) throws { 24 | #if os(macOS) 25 | let fileManager = FileManager.default 26 | 27 | guard let workingDirectoryURL = directoryURL else { 28 | print("❌ Invalid target directory URL. Cannot copy template project.") 29 | return 30 | } 31 | 32 | guard !containsDataConnectProject(folderURL: workingDirectoryURL) else { 33 | print( 34 | "ℹ️ Working directory already contains a dataconnect project. Skipping copy of template project." 35 | ) 36 | return 37 | } 38 | 39 | let resourceFolderName = "dataconnect" 40 | let destinationURL = workingDirectoryURL.appendingPathComponent(resourceFolderName) 41 | 42 | // 3. Find the URL for the resource folder within the compiled tool's bundle 43 | guard let sourceURL = Bundle.module.url( 44 | forResource: resourceFolderName, 45 | withExtension: nil, 46 | subdirectory: "Resources/demo-iosproject" 47 | ) else { 48 | throw NSError(domain: "StartFDCTools", code: 1, userInfo: [ 49 | NSLocalizedDescriptionKey: "Could not find '\(resourceFolderName)' in the resource bundle.", 50 | ]) 51 | } 52 | 53 | // 4. Perform the copy operation 54 | try fileManager.copyItem(at: sourceURL, to: destinationURL) 55 | 56 | // Copy the firebase.json 57 | if let sourceJsonUrl = Bundle.module.url( 58 | forResource: "firebase", 59 | withExtension: "json", 60 | subdirectory: "Resources/demo-iosproject" 61 | ) { 62 | let destinationJson = workingDirectoryURL.appendingPathComponent("firebase.json") 63 | try fileManager.copyItem(at: sourceJsonUrl, to: destinationJson) 64 | } 65 | 66 | // Copy the GoogleServices-Info.plist 67 | if let sourcePlistUrl = Bundle.module.url( 68 | forResource: "GoogleService-Info-Template", 69 | withExtension: "plist", 70 | subdirectory: "Resources/demo-iosproject" 71 | ) { 72 | let destinationPlist = workingDirectoryURL 73 | .appendingPathComponent("GoogleService-Info.plist") 74 | try fileManager.copyItem(at: sourcePlistUrl, to: destinationPlist) 75 | } 76 | #endif 77 | } 78 | 79 | // Looks for dataconnect.yaml file within the specified folder recursively 80 | func containsDataConnectProject(folderURL: URL) -> Bool { 81 | #if os(macOS) 82 | let fileManager = FileManager.default 83 | if let enumerator = fileManager.enumerator( 84 | at: folderURL, 85 | includingPropertiesForKeys: nil, 86 | options: [.skipsHiddenFiles, .skipsPackageDescendants] 87 | ) { 88 | for case let itemURL as URL in enumerator { 89 | if itemURL.lastPathComponent.contains("dataconnect.yaml") { 90 | print("Found existing dataconnect.yaml \(itemURL)") 91 | return true 92 | } 93 | } 94 | } 95 | #endif 96 | return false 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/Internal/OperationsManager.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 18 | class OperationsManager { 19 | private var grpcClient: GrpcClient 20 | 21 | private var cache: Cache? 22 | 23 | private let queryRefAccessQueue = DispatchQueue( 24 | label: "firebase.dataconnect.queryRef.AccessQ", 25 | autoreleaseFrequency: .workItem 26 | ) 27 | private var queryRefs = [String: any ObservableQueryRef]() 28 | 29 | private let mutationRefAccessQueue = DispatchQueue( 30 | label: "firebase.dataconnect.mutRef.AccessQ", 31 | autoreleaseFrequency: .workItem 32 | ) 33 | private var mutationRefs = [AnyHashable: any OperationRef]() 34 | 35 | init(grpcClient: GrpcClient, cache: Cache? = nil) { 36 | self.grpcClient = grpcClient 37 | self.cache = cache 38 | } 39 | 40 | func queryRef(for request: QueryRequest, 42 | with resultType: ResultDataType 43 | .Type, 44 | publisher: ResultsPublisherType = .auto) 45 | -> any ObservableQueryRef { 46 | queryRefAccessQueue.sync { 47 | var req = request // requestId is a mutating call. 48 | let requestId = req.requestId 49 | 50 | if let ref = queryRefs[requestId] { 51 | return ref 52 | } 53 | 54 | if publisher == .auto || publisher == .observableMacro { 55 | if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { 56 | let obsRef = QueryRefObservation( 57 | request: request, 58 | dataType: resultType, 59 | grpcClient: self.grpcClient, 60 | cache: self.cache 61 | ) as (any ObservableQueryRef) 62 | queryRefs[requestId] = obsRef 63 | return obsRef 64 | } 65 | } 66 | 67 | let refObsObject = QueryRefObservableObject( 68 | request: request, 69 | dataType: resultType, 70 | grpcClient: grpcClient, 71 | cache: self.cache 72 | ) as (any ObservableQueryRef) 73 | queryRefs[requestId] = refObsObject 74 | return refObsObject 75 | } // accessQueue.sync 76 | } 77 | 78 | func queryRef(for operationId: String) -> (any ObservableQueryRef)? { 79 | queryRefAccessQueue.sync { 80 | queryRefs[operationId] 81 | } 82 | } 83 | 84 | func mutationRef(for request: MutationRequest, 86 | with resultType: ResultDataType 87 | .Type) -> MutationRef { 88 | mutationRefAccessQueue.sync { 89 | if let ref = mutationRefs[ 90 | AnyHashable( 91 | request 92 | ) 93 | ] as? MutationRef { 94 | return ref 95 | } 96 | 97 | let ref = MutationRef( 98 | request: request, 99 | grpcClient: grpcClient 100 | ) 101 | mutationRefs[AnyHashable(request)] = ref 102 | return ref 103 | } 104 | } // accessQueue.sync 105 | } 106 | -------------------------------------------------------------------------------- /Sources/Internal/ProtoCodec.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | import SwiftProtobuf 18 | 19 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 20 | typealias FirebaseDataConnectExecuteMutationRequest = 21 | Google_Firebase_Dataconnect_V1_ExecuteMutationRequest 22 | typealias FirebaseDataConnectExecuteQueryRequest = 23 | Google_Firebase_Dataconnect_V1_ExecuteQueryRequest 24 | 25 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 26 | class ProtoCodec { 27 | // Encode Codable to Protos 28 | func encode(args: any Encodable) throws -> Google_Protobuf_Struct { 29 | do { 30 | let jsonEncoder = JSONEncoder() 31 | let jsonData = try jsonEncoder.encode(args) 32 | let argsStruct = try Google_Protobuf_Struct(jsonUTF8Data: jsonData) 33 | return argsStruct 34 | } 35 | } 36 | 37 | // Decode Protos to Codable 38 | func decode(result: Google_Protobuf_Struct, asType: T.Type) throws -> T { 39 | do { 40 | let jsonData = try result.jsonUTF8Data() 41 | let jsonDecoder = JSONDecoder() 42 | 43 | let resultAsType = try jsonDecoder.decode(asType, from: jsonData) 44 | 45 | return resultAsType 46 | } 47 | } 48 | 49 | func createQueryRequestProto(connectorName: String, 50 | request: QueryRequest< 51 | VariableType 52 | >) throws 53 | -> FirebaseDataConnectExecuteQueryRequest { 54 | do { 55 | var varStruct: Google_Protobuf_Struct? = nil 56 | if let variables = request.variables { 57 | varStruct = try encode(args: variables) 58 | } 59 | 60 | let internalRequest = FirebaseDataConnectExecuteQueryRequest.with { ireq in 61 | ireq.operationName = request.operationName 62 | 63 | if let varStruct { 64 | ireq.variables = varStruct 65 | } else { 66 | ireq.variables = Google_Protobuf_Struct() 67 | } 68 | 69 | ireq.name = connectorName 70 | } 71 | 72 | return internalRequest 73 | } 74 | } 75 | 76 | func createMutationRequestProto(connectorName: String, 77 | request: MutationRequest< 78 | VariableType 79 | >) throws 80 | -> FirebaseDataConnectExecuteMutationRequest { 81 | do { 82 | var varStruct: Google_Protobuf_Struct? = nil 83 | if let variables = request.variables { 84 | varStruct = try encode(args: variables) 85 | } 86 | 87 | let internalRequest = FirebaseDataConnectExecuteMutationRequest 88 | .with { ireq in 89 | ireq.operationName = request.operationName 90 | 91 | if let varStruct { 92 | ireq.variables = varStruct 93 | } else { 94 | // always provide an empty struct otherwise request fails. 95 | ireq.variables = Google_Protobuf_Struct() 96 | } 97 | 98 | ireq.name = connectorName 99 | } 100 | 101 | return internalRequest 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Search/SearchScreen.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import FirebaseDataConnect 16 | import FriendlyFlixSDK 17 | import SwiftUI 18 | 19 | struct SearchedView: View { 20 | @Environment(\.isSearching) private var isSearching 21 | var namespace: Namespace.ID 22 | var filteredMovies = [Movie]() 23 | var connector = DataConnect.friendlyFlixConnector 24 | 25 | private var topMovies: [Movie] { 26 | connector.listMoviesQuery 27 | .ref { optionalVars in 28 | optionalVars.limit = 5 29 | optionalVars.orderByRating = .DESC 30 | } 31 | .data?.movies.map(Movie.init) ?? [] 32 | } 33 | 34 | var body: some View { 35 | if !isSearching { 36 | MovieListSection(namespace: namespace, title: "Top Movies", movies: topMovies) 37 | } else { 38 | ForEach(filteredMovies) { movie in 39 | NavigationLink(value: movie) { 40 | MovieListRowView( 41 | title: movie.title, 42 | subtitle: movie.description, 43 | imageUrl: movie.imageUrl 44 | ) 45 | .matchedTransitionSource(id: movie.id, in: namespace) 46 | } 47 | .buttonStyle(.noHighlight) 48 | } 49 | } 50 | } 51 | } 52 | 53 | struct SearchScreen: View { 54 | @State private var searchText: String = "" 55 | @State private var isStatusBarHidden = false 56 | @Namespace private var namespace 57 | 58 | var connector = DataConnect.friendlyFlixConnector 59 | 60 | private var filteredMovies: [Movie] { 61 | connector.listMoviesByPartialTitleQuery 62 | .ref(searchTerm: searchText.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)) 63 | .data?.movies.map(Movie.init) ?? [] 64 | } 65 | } 66 | 67 | extension SearchScreen { 68 | var body: some View { 69 | NavigationStack { 70 | ScrollView { 71 | SearchedView(namespace: namespace, filteredMovies: filteredMovies) 72 | .searchable(text: $searchText) 73 | .textInputAutocapitalization(.never) 74 | } 75 | .padding() 76 | .navigationTitle("Search") 77 | .navigationDestination(for: Movie.self) { movie in 78 | MovieCardView(showDetails: true, movie: movie) 79 | .navigationTransition(.zoom(sourceID: movie.id, in: namespace)) 80 | .task { 81 | // NavigationStack requires `.statusBarHidden` to be applied to the navigationstack 82 | // itself, not on any children. 83 | // See https://danielsaidi.com/blog/2023/03/14/handling-status-bar-color-scheme-and-visibility-in-swiftui 84 | isStatusBarHidden = true 85 | } 86 | } 87 | .navigationDestination(for: [Movie].self) { movies in 88 | MovieListScreen(namespace: namespace, movies: movies) 89 | } 90 | .navigationDestination(for: SectionedMovie.self) { sectionedMovie in 91 | MovieCardView(showDetails: true, movie: sectionedMovie.movie) 92 | .navigationTransition(.zoom(sourceID: sectionedMovie.id, in: namespace)) 93 | } 94 | .toolbar { 95 | ToolbarItem(placement: .topBarTrailing) { 96 | AuthenticationToolbarButton() 97 | } 98 | } 99 | } 100 | .statusBarHidden(isStatusBarHidden) 101 | } 102 | } 103 | 104 | #Preview { 105 | SearchScreen() 106 | .environment(AuthenticationService()) 107 | } 108 | -------------------------------------------------------------------------------- /Sources/Cache/EntityDataObject.swift: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import Foundation 16 | 17 | // Represents a normalized entity shared amongst queries. 18 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 19 | class EntityDataObject: Codable { 20 | let guid: String // globally unique id received from server 21 | 22 | private let accessQueue = DispatchQueue( 23 | label: "com.google.firebase.dataconnect.entityData", 24 | autoreleaseFrequency: .workItem 25 | ) 26 | 27 | required init(guid: String) { 28 | self.guid = guid 29 | } 30 | 31 | private var serverValues = [String: AnyCodableValue]() 32 | 33 | enum CodingKeys: String, CodingKey { 34 | case guid = "_id" 35 | case serverValues = "sval" 36 | } 37 | 38 | // Updates the value received from server and returns a list of QueryRef operation ids 39 | // referenced from this EntityDataObject 40 | @discardableResult func updateServerValue(_ key: String, 41 | _ newValue: AnyCodableValue, 42 | _ requestor: (any QueryRefInternal)? = nil) 43 | -> [String] { 44 | accessQueue.sync { 45 | self.serverValues[key] = newValue 46 | DataConnectLogger.debug("EDO updateServerValue: \(key) \(newValue) for \(guid)") 47 | 48 | if let requestor { 49 | referencedFrom.insert(requestor.operationId) 50 | DataConnectLogger 51 | .debug("Inserted referencedFrom \(requestor). New count \(referencedFrom.count)") 52 | } 53 | let refs = [String](referencedFrom) 54 | return refs 55 | } 56 | } 57 | 58 | func value(forKey key: String) -> AnyCodableValue? { 59 | accessQueue.sync { 60 | self.serverValues[key] 61 | } 62 | } 63 | 64 | // MARK: Track referenced QueryRefs 65 | 66 | // Set of QueryRefs that reference this EDO 67 | private var referencedFrom = Set() 68 | 69 | func updateReferencedFrom(_ refs: Set) { 70 | accessQueue.sync { 71 | self.referencedFrom = refs 72 | } 73 | } 74 | 75 | func referencedFromRefs() -> Set { 76 | accessQueue.sync { 77 | self.referencedFrom 78 | } 79 | } 80 | 81 | var isReferencedFromAnyQueryRef: Bool { 82 | accessQueue.sync { 83 | !referencedFrom.isEmpty 84 | } 85 | } 86 | 87 | // inline encodable data 88 | // used when trying to create a hydrated tree 89 | func encodableData() throws -> [String: AnyCodableValue] { 90 | accessQueue.sync { 91 | var encodingValues = [String: AnyCodableValue]() 92 | encodingValues[GlobalIDKey] = .string(guid) 93 | encodingValues.merge(serverValues) { _, new in new } 94 | return encodingValues 95 | } 96 | } 97 | } 98 | 99 | extension EntityDataObject: CustomStringConvertible { 100 | var description: String { 101 | return """ 102 | EntityDataObject: 103 | globalID: \(guid) 104 | serverValues: 105 | \(serverValues) 106 | """ 107 | } 108 | } 109 | 110 | extension EntityDataObject: Equatable { 111 | static func == (lhs: EntityDataObject, rhs: EntityDataObject) -> Bool { 112 | return lhs.guid == rhs.guid && lhs.serverValues == rhs.serverValues 113 | } 114 | } 115 | 116 | extension EntityDataObject: CustomDebugStringConvertible { 117 | var debugDescription: String { 118 | return description 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Protos/google/protobuf/struct.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 36 | option cc_enable_arenas = true; 37 | option go_package = "github.com/golang/protobuf/ptypes/struct;structpb"; 38 | option java_package = "com.google.protobuf"; 39 | option java_outer_classname = "StructProto"; 40 | option java_multiple_files = true; 41 | option objc_class_prefix = "GPB"; 42 | 43 | 44 | // `Struct` represents a structured data value, consisting of fields 45 | // which map to dynamically typed values. In some languages, `Struct` 46 | // might be supported by a native representation. For example, in 47 | // scripting languages like JS a struct is represented as an 48 | // object. The details of that representation are described together 49 | // with the proto support for the language. 50 | // 51 | // The JSON representation for `Struct` is JSON object. 52 | message Struct { 53 | // Unordered map of dynamically typed values. 54 | map fields = 1; 55 | } 56 | 57 | // `Value` represents a dynamically typed value which can be either 58 | // null, a number, a string, a boolean, a recursive struct value, or a 59 | // list of values. A producer of value is expected to set one of that 60 | // variants, absence of any variant indicates an error. 61 | // 62 | // The JSON representation for `Value` is JSON value. 63 | message Value { 64 | // The kind of value. 65 | oneof kind { 66 | // Represents a null value. 67 | NullValue null_value = 1; 68 | // Represents a double value. 69 | double number_value = 2; 70 | // Represents a string value. 71 | string string_value = 3; 72 | // Represents a boolean value. 73 | bool bool_value = 4; 74 | // Represents a structured value. 75 | Struct struct_value = 5; 76 | // Represents a repeated `Value`. 77 | ListValue list_value = 6; 78 | } 79 | } 80 | 81 | // `NullValue` is a singleton enumeration to represent the null value for the 82 | // `Value` type union. 83 | // 84 | // The JSON representation for `NullValue` is JSON `null`. 85 | enum NullValue { 86 | // Null value. 87 | NULL_VALUE = 0; 88 | } 89 | 90 | // `ListValue` is a wrapper around a repeated field of values. 91 | // 92 | // The JSON representation for `ListValue` is JSON array. 93 | message ListValue { 94 | // Repeated field of dynamically typed values. 95 | repeated Value values = 1; 96 | } 97 | --------------------------------------------------------------------------------