├── 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 |
--------------------------------------------------------------------------------