From ab62489623d893f9e466ac9c53ff52042d29cbce Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Wed, 11 Sep 2024 01:29:37 -0700 Subject: [PATCH 1/2] feat(fdc): Data Connect Dart SDK init (#13063) * First pass at dataconnect * Got mutations to work * Finished WIP * Used generated SDK * Removed unrelated chagnes * Cleanup * Updated deps * Addressed comments * Removed prints * Removed unnecessary protos * Removed user class * Removed unnecessary class * Removed unnecessary deps * Added back config files * Fixed formatting * added index.html * Added license files * Fixed formatting * Fixed linting issues * Fixed formatting * Added protobuf as dependency * Added license * Ignored example dir for secret checking * Added changelog and readme * Updated package version * Implemented app check * Removed key * Revert "Removed key" This reverts commit 6a643f12ba1cf86f197904ff8803c8f98f89a612. * Removed key again * Added error catching implementation (#13163) * feat(dataconnect): Added error handling (#13175) * Added missing file * Removed unnecessary changes * Miscellaneous cleanup * Removed macros experiment * Removed .then statements * Removed debug prints * Addressed first pass of comments * Added movie_insert gql file * Added comments * Renamed package * Another package rename * Removed common package * Added missing files * Updated readme * Revert "Removed common package" This reverts commit 422bd80af0d603c9e756df40a52a5a43d174ce76. * Various improvements * Made changes per api proposal * Fixed initialization issues with generated SDK * Made changes according to api council review * Addressed Comments. Removed references to personal firebase projects Moved mapped host logic to new firebase_common package * Added missing license header to files * /s/query/operation * API Council Changes * Included list movies changes * API council updates * Addressed comments * Removed print * Added license header * Removed files that should be gitignored * Removed requirement on firebase_options * Added missing line * Removed windows and linux folders, updated changelog and gitignore * Updated version of firebase_data_connect --- .../example/android/app/google-services.json | 2 +- .../firebase_auth/lib/src/firebase_auth.dart | 11 +- .../example/android/app/google-services.json | 2 +- .../firebase_core/lib/firebase_core.dart | 1 + .../firebase_core/lib/src/port_mapping.dart | 18 + .../analysis_options.yaml | 91 ++ .../firebase_data_connect/.gitignore | 29 + .../firebase_data_connect/CHANGELOG.md | 2 + .../firebase_data_connect/LICENSE | 27 + .../firebase_data_connect/README.md | 25 + .../example/.firebase/.graphqlrc | 1 + .../firebase_data_connect/example/.gitignore | 50 + .../firebase_data_connect/example/.metadata | 45 + .../firebase_data_connect/example/README.md | 26 + .../example/analysis_options.yaml | 31 + .../example/android/.gitignore | 13 + .../example/android/app/build.gradle | 61 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 45 + .../dataconnect/example/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + .../example/android/build.gradle | 18 + .../example/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../example/android/settings.gradle | 28 + .../example/dataconnect.yaml | 11 + .../.dataconnect/schema/main/implicit.gql | 10 + .../.dataconnect/schema/main/input.gql | 88 ++ .../.dataconnect/schema/main/mutation.gql | 74 ++ .../.dataconnect/schema/main/query.gql | 26 + .../.dataconnect/schema/main/relation.gql | 20 + .../.dataconnect/schema/prelude.gql | 1031 +++++++++++++++++ .../dataconnect/connector/connector.yaml | 6 + .../dataconnect/connector/movie_insert.gql | 210 ++++ .../dataconnect/connector/mutations.gql | 121 ++ .../example/dataconnect/connector/queries.gql | 574 +++++++++ .../example/dataconnect/dataconnect.yaml | 11 + .../example/dataconnect/schema/schema.gql | 114 ++ .../example/firebase.json | 5 + .../example/flutter_01.png | Bin 0 -> 78324 bytes .../example/ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../example/ios/Flutter/Debug.xcconfig | 2 + .../example/ios/Flutter/Release.xcconfig | 2 + .../firebase_data_connect/example/ios/Podfile | 44 + .../ios/Runner.xcodeproj/project.pbxproj | 749 ++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 98 ++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 ++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../example/ios/Runner/Info.plist | 62 + .../ios/Runner/Runner-Bridging-Header.h | 1 + .../example/ios/RunnerTests/RunnerTests.swift | 14 + .../lib/generated/add_director_to_movie.dart | 136 +++ .../example/lib/generated/add_person.dart | 93 ++ .../example/lib/generated/create_movie.dart | 129 +++ .../example/lib/generated/list_movies.dart | 102 ++ .../example/lib/generated/movies.dart | 49 + .../example/lib/login.dart | 85 ++ .../example/lib/main.dart | 260 +++++ .../example/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../example/macos/Podfile | 43 + .../macos/Runner.xcodeproj/project.pbxproj | 709 ++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 98 ++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/macos/Runner/AppDelegate.swift | 9 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 ++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + .../macos/Runner/GoogleService-Info.plist | 38 + .../example/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../example/macos/Runner/Release.entitlements | 8 + .../macos/RunnerTests/RunnerTests.swift | 14 + .../example/pubspec.yaml | 100 ++ .../example/schema/schema.gql | 23 + .../example/test/widget_test.dart | 34 + .../example/web/favicon.png | Bin 0 -> 917 bytes .../example/web/icons/Icon-192.png | Bin 0 -> 5292 bytes .../example/web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../example/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../example/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes .../example/web/index.html | 38 + .../example/web/manifest.json | 35 + .../firebase_data_connect/generate_proto.sh | 4 + .../lib/firebase_data_connect.dart | 25 + .../lib/src/common/common_library.dart | 60 + .../lib/src/common/dataconnect_error.dart | 21 + .../lib/src/common/dataconnect_options.dart | 40 + .../lib/src/core/empty_serializer.dart | 10 + .../lib/src/core/ref.dart | 146 +++ .../lib/src/dataconnect_version.dart | 16 + .../lib/src/firebase_data_connect.dart | 120 ++ .../src/generated/connector_service.pb.dart | 408 +++++++ .../generated/connector_service.pbenum.dart | 14 + .../generated/connector_service.pbgrpc.dart | 99 ++ .../generated/connector_service.pbjson.dart | 138 +++ .../generated/google/protobuf/struct.pb.dart | 345 ++++++ .../google/protobuf/struct.pbenum.dart | 39 + .../google/protobuf/struct.pbjson.dart | 138 +++ .../lib/src/generated/graphql_error.pb.dart | 319 +++++ .../src/generated/graphql_error.pbenum.dart | 14 + .../src/generated/graphql_error.pbjson.dart | 85 ++ .../lib/src/network/grpc_library.dart | 19 + .../lib/src/network/grpc_transport.dart | 139 +++ .../lib/src/network/rest_library.dart | 17 + .../lib/src/network/rest_transport.dart | 145 +++ .../lib/src/network/transport_library.dart | 11 + .../lib/src/network/transport_stub.dart | 56 + .../lib/src/optional.dart | 100 ++ .../lib/src/timestamp.dart | 64 + .../protos/connector_service.proto | 105 ++ .../protos/firebase/graphql_error.proto | 85 ++ .../protos/google/api/field_behavior.proto | 104 ++ .../protos/google/struct.proto | 95 ++ .../firebase_data_connect/pubspec.yaml | 29 + 167 files changed, 9757 insertions(+), 12 deletions(-) create mode 100644 packages/firebase_core/firebase_core/lib/src/port_mapping.dart create mode 100644 packages/firebase_data_connect/analysis_options.yaml create mode 100644 packages/firebase_data_connect/firebase_data_connect/.gitignore create mode 100644 packages/firebase_data_connect/firebase_data_connect/CHANGELOG.md create mode 100644 packages/firebase_data_connect/firebase_data_connect/LICENSE create mode 100644 packages/firebase_data_connect/firebase_data_connect/README.md create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/.firebase/.graphqlrc create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/.gitignore create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/.metadata create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/README.md create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/analysis_options.yaml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/.gitignore create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/build.gradle create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/AndroidManifest.xml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/kotlin/io/flutter/plugins/firebase/dataconnect/example/MainActivity.kt create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/values-night/styles.xml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/values/styles.xml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/app/src/profile/AndroidManifest.xml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/build.gradle create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/gradle.properties create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/android/settings.gradle create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/dataconnect.yaml create mode 100755 packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/implicit.gql create mode 100755 packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/input.gql create mode 100755 packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/mutation.gql create mode 100755 packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/query.gql create mode 100755 packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/relation.gql create mode 100755 packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/prelude.gql create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/connector.yaml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/movie_insert.gql create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/dataconnect/dataconnect.yaml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/dataconnect/schema/schema.gql create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/firebase.json create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/flutter_01.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/.gitignore create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Flutter/Debug.xcconfig create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Flutter/Release.xcconfig create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Podfile create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/AppDelegate.swift create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Info.plist create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Runner-Bridging-Header.h create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/ios/RunnerTests/RunnerTests.swift create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_director_to_movie.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_person.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/lib/generated/create_movie.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/lib/login.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/lib/main.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/.gitignore create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Podfile create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/AppDelegate.swift create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/Debug.xcconfig create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/Release.xcconfig create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/DebugProfile.entitlements create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/GoogleService-Info.plist create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Info.plist create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/MainFlutterWindow.swift create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Release.entitlements create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/macos/RunnerTests/RunnerTests.swift create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/schema/schema.gql create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/test/widget_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/web/favicon.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/web/icons/Icon-192.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/web/icons/Icon-512.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/web/icons/Icon-maskable-192.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/web/icons/Icon-maskable-512.png create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/web/index.html create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/web/manifest.json create mode 100755 packages/firebase_data_connect/firebase_data_connect/generate_proto.sh create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_options.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/core/empty_serializer.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/dataconnect_version.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pb.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbenum.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbgrpc.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbjson.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pb.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbenum.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbjson.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pb.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbenum.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbjson.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_library.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_library.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_stub.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/optional.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/timestamp.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/protos/connector_service.proto create mode 100644 packages/firebase_data_connect/firebase_data_connect/protos/firebase/graphql_error.proto create mode 100644 packages/firebase_data_connect/firebase_data_connect/protos/google/api/field_behavior.proto create mode 100644 packages/firebase_data_connect/firebase_data_connect/protos/google/struct.proto create mode 100644 packages/firebase_data_connect/firebase_data_connect/pubspec.yaml diff --git a/packages/firebase_auth/firebase_auth/example/android/app/google-services.json b/packages/firebase_auth/firebase_auth/example/android/app/google-services.json index 6b7e04085d8b..348716f0e250 100644 --- a/packages/firebase_auth/firebase_auth/example/android/app/google-services.json +++ b/packages/firebase_auth/firebase_auth/example/android/app/google-services.json @@ -142,7 +142,7 @@ "client_info": { "mobilesdk_app_id": "1:406099696497:android:3ef965ff044efc0b3574d0", "android_client_info": { - "package_name": "io.flutter.plugins.firebase.database.example" + "package_name": "io.flutter.plugins.firebase.dataconnect.example" } }, "oauth_client": [ diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index bc7906113a71..69d955247851 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -83,16 +83,7 @@ class FirebaseAuth extends FirebasePluginPlatform { /// Do not use with production credentials as emulator traffic is not encrypted. Future useAuthEmulator(String host, int port, {bool automaticHostMapping = true}) async { - String mappedHost = host; - - if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { - if ((mappedHost == 'localhost' || mappedHost == '127.0.0.1') && - automaticHostMapping) { - // ignore: avoid_print - print('Mapping Auth Emulator host "$mappedHost" to "10.0.2.2".'); - mappedHost = '10.0.2.2'; - } - } + String mappedHost = automaticHostMapping ? getMappedHost(host) : host; await _delegate.useAuthEmulator(mappedHost, port); } diff --git a/packages/firebase_core/firebase_core/example/android/app/google-services.json b/packages/firebase_core/firebase_core/example/android/app/google-services.json index 6b7e04085d8b..348716f0e250 100644 --- a/packages/firebase_core/firebase_core/example/android/app/google-services.json +++ b/packages/firebase_core/firebase_core/example/android/app/google-services.json @@ -142,7 +142,7 @@ "client_info": { "mobilesdk_app_id": "1:406099696497:android:3ef965ff044efc0b3574d0", "android_client_info": { - "package_name": "io.flutter.plugins.firebase.database.example" + "package_name": "io.flutter.plugins.firebase.dataconnect.example" } }, "oauth_client": [ diff --git a/packages/firebase_core/firebase_core/lib/firebase_core.dart b/packages/firebase_core/firebase_core/lib/firebase_core.dart index f393fa9472ec..20830d712bb6 100644 --- a/packages/firebase_core/firebase_core/lib/firebase_core.dart +++ b/packages/firebase_core/firebase_core/lib/firebase_core.dart @@ -14,3 +14,4 @@ export 'package:firebase_core_platform_interface/firebase_core_platform_interfac part 'src/firebase.dart'; part 'src/firebase_app.dart'; +part 'src/port_mapping.dart'; diff --git a/packages/firebase_core/firebase_core/lib/src/port_mapping.dart b/packages/firebase_core/firebase_core/lib/src/port_mapping.dart new file mode 100644 index 000000000000..3400b94c9bde --- /dev/null +++ b/packages/firebase_core/firebase_core/lib/src/port_mapping.dart @@ -0,0 +1,18 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_core; + +String getMappedHost(String originalHost) { + String mappedHost = originalHost; + + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + if (mappedHost == 'localhost' || mappedHost == '127.0.0.1') { + // ignore: avoid_print + print('Mapping Auth Emulator host "$mappedHost" to "10.0.2.2".'); + mappedHost = '10.0.2.2'; + } + } + return mappedHost; +} diff --git a/packages/firebase_data_connect/analysis_options.yaml b/packages/firebase_data_connect/analysis_options.yaml new file mode 100644 index 000000000000..e2467db9cb61 --- /dev/null +++ b/packages/firebase_data_connect/analysis_options.yaml @@ -0,0 +1,91 @@ +# Copyright 2021 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# in the LICENSE file. + +include: all_lint_rules.yaml +analyzer: + exclude: + - firebase_data_connect/lib/src/generated/** + # TODO(rrousselGit): disable implicit-cast/implicit-dynamic + errors: + # Otherwise cause the import of all_lint_rules to warn because of some rules conflicts. + # We explicitly enabled even conflicting rules and are fixing the conflict + # in this file + included_file_warning: ignore + +linter: + rules: + ## Disabled rules because the repository doesn't respect them (yet) + + always_put_control_body_on_new_line: false + comment_references: false + prefer_constructors_over_static_methods: false + prefer_final_fields: false + prefer_final_locals: false + omit_local_variable_types: false + avoid_equals_and_hash_code_on_mutable_classes: false + public_member_api_docs: false + + ############# + + # Personal preference. I don't find it more readable + cascade_invocations: false + + # Conflicts with `prefer_single_quotes` + # Single quotes are easier to type and don't compromise on readability. + prefer_double_quotes: false + + # Conflicts with `omit_local_variable_types` and other rules. + # As per Dart guidelines, we want to avoid unnecessary types to make the code + # more readable. + # See https://dart.dev/guides/language/effective-dart/design#avoid-type-annotating-initialized-local-variables + always_specify_types: false + + # Incompatible with `prefer_final_locals` + # Having immutable local variables makes larger functions more predictible + # so we will use `prefer_final_locals` instead. + unnecessary_final: false + + # Not quite suitable for Flutter, which may have a `build` method with a single + # return, but that return is still complex enough that a "body" is worth it. + prefer_expression_function_bodies: false + + # Conflicts with the convention used by flutter, which puts `Key key` + # and `@required Widget child` last. + always_put_required_named_parameters_first: false + + # This project doesn't use Flutter-style todos + flutter_style_todos: false + + # There are situations where we voluntarily want to catch everything, + # especially as a library. + avoid_catches_without_on_clauses: false + + # Boring as it sometimes force a line of 81 characters to be split in two. + # As long as we try to respect that 80 characters limit, going slightly + # above is fine. + lines_longer_than_80_chars: false + + # Conflicts with disabling `implicit-dynamic` + avoid_annotating_with_dynamic: false + + # conflicts with `prefer_relative_imports` + always_use_package_imports: false + + # Disabled for now until we have NNBD as it otherwise conflicts with `missing_return` + no_default_cases: false + + # False positive, null checks don't need a message + prefer_asserts_with_message: false + + # Cumbersome with `context.select` + avoid_types_on_closure_parameters: false + + # Too many false positive (builders) + diagnostic_describe_all_properties: false + + # false positives (setter-like functions) + avoid_positional_boolean_parameters: false + + # Does not apply to providers + prefer_const_constructors_in_immutables: false diff --git a/packages/firebase_data_connect/firebase_data_connect/.gitignore b/packages/firebase_data_connect/firebase_data_connect/.gitignore new file mode 100644 index 000000000000..ac5aa9893e48 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/packages/firebase_data_connect/firebase_data_connect/CHANGELOG.md b/packages/firebase_data_connect/firebase_data_connect/CHANGELOG.md new file mode 100644 index 000000000000..e84569abf410 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/CHANGELOG.md @@ -0,0 +1,2 @@ +## 0.1.0 +* Initial release \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/LICENSE b/packages/firebase_data_connect/firebase_data_connect/LICENSE new file mode 100644 index 000000000000..000b4618d2bd --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/README.md b/packages/firebase_data_connect/firebase_data_connect/README.md new file mode 100644 index 000000000000..40ad177623c7 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/README.md @@ -0,0 +1,25 @@ +# Firebase Data Connect for Flutter + +A Flutter plugin to use the [Firebase Data Connect API](https://firebase.google.com/docs/data-connect/). + +To learn more about Firebase Data Connect, please visit the [Firebase website](https://firebase.google.com/products/data-connect) + +[![pub package](https://img.shields.io/pub/v/firebase_data_connect.svg)](https://pub.dev/packages/firebase_data_connect) + +## Getting Started + +To get started with Data Connect for Flutter, please [see the documentation](https://firebase.google.com/docs/data-connect/quickstart). + +## Usage + +To use this plugin, please visit the [Data Connect Usage documentation](https://firebase.google.com/docs/data-connect/gp/schemas-queries-mutations) + +## Issues and feedback + +Please file FlutterFire specific issues, bugs, or feature requests in our [issue tracker](https://github.com/firebase/flutterfire/issues/new). + +Plugin issues that are not specific to FlutterFire can be filed in the [Flutter issue tracker](https://github.com/flutter/flutter/issues/new). + +To contribute a change to this plugin, +please review our [contribution guide](https://github.com/firebase/flutterfire/blob/master/CONTRIBUTING.md) +and open a [pull request](https://github.com/firebase/flutterfire/pulls). diff --git a/packages/firebase_data_connect/firebase_data_connect/example/.firebase/.graphqlrc b/packages/firebase_data_connect/firebase_data_connect/example/.firebase/.graphqlrc new file mode 100644 index 000000000000..51bf7fd77723 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/.firebase/.graphqlrc @@ -0,0 +1 @@ +{"schema":["../dataconnect/schema/**/*.gql","../dataconnect/.dataconnect/**/*.gql"],"document":["../dataconnect/connector/**/*.gql"]} \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/example/.gitignore b/packages/firebase_data_connect/firebase_data_connect/example/.gitignore new file mode 100644 index 000000000000..acb2d26652f5 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/.gitignore @@ -0,0 +1,50 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release +/android/app/google-services.json +/ios/firebase_app_id_file.json +/lib/firebase_options.dart +/macos/firebase_app_id_file.json + +ios/Runner/GoogleService-Info.plist +.firebaserc diff --git a/packages/firebase_data_connect/firebase_data_connect/example/.metadata b/packages/firebase_data_connect/firebase_data_connect/example/.metadata new file mode 100644 index 000000000000..8ca14df4337f --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "5dcb86f68f239346676ceb1ed1ea385bd215fba1" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: android + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: ios + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: linux + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: macos + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: web + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: windows + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/firebase_data_connect/firebase_data_connect/example/README.md b/packages/firebase_data_connect/firebase_data_connect/example/README.md new file mode 100644 index 000000000000..308fd7e0d30d --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/README.md @@ -0,0 +1,26 @@ +# Firebase Data Connect Example + +This example showcases Firebase Auth and Data Connect. + +## Getting Started + +1. Sign up for early access [here](https://firebase.google.com/products/data-connect) and receive an invitation. + + Note: This is not required for public preview. +2. Upgrade your Firebase project billing to the Blaze plan, you will not be charged for the duration of gated preview. +3. Initialize DataConnect in the [Firebase Console](https://console.firebase.google.com/u/0/). +4. Install postgres using the documentation provided [here](https://firebase.google.com/docs/data-connect/quickstart#optional_install_postgresql_locally). +5. Update `firebase-tools` with `npm install -g firebase-tools`. +6. Initialize your Firebase project in the `dataconnect` folder with `firebase init` and select DataConnect. Do not overwrite the dataconnect files when prompted. +7. Install the VSCode extension from [here](https://firebasestorage.googleapis.com/v0/b/firemat-preview-drop/o/vsix%2Ffirebase-vscode-latest.vsix?alt=media). +8. Run the mutation in `dataconnect/connector/movie_insert.gql` +9. Run `flutter run` + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/firebase_data_connect/firebase_data_connect/example/analysis_options.yaml b/packages/firebase_data_connect/firebase_data_connect/example/analysis_options.yaml new file mode 100644 index 000000000000..5636857817cf --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/analysis_options.yaml @@ -0,0 +1,31 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - lib/generated/** +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/.gitignore b/packages/firebase_data_connect/firebase_data_connect/example/android/.gitignore new file mode 100644 index 000000000000..6f568019d3c6 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/build.gradle b/packages/firebase_data_connect/firebase_data_connect/example/android/app/build.gradle new file mode 100644 index 000000000000..a66f5c8d954f --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/app/build.gradle @@ -0,0 +1,61 @@ +plugins { + id "com.android.application" + // START: FlutterFire Configuration + id 'com.google.gms.google-services' + // END: FlutterFire Configuration + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file("local.properties") +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader("UTF-8") { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") +if (flutterVersionCode == null) { + flutterVersionCode = "1" +} + +def flutterVersionName = localProperties.getProperty("flutter.versionName") +if (flutterVersionName == null) { + flutterVersionName = "1.0" +} + +android { + namespace = "io.flutter.plugins.firebase.dataconnect.example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "io.flutter.plugins.firebase.dataconnect.example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdk = 23 + targetSdk = flutter.targetSdkVersion + versionCode = flutterVersionCode.toInteger() + versionName = flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.debug + } + } +} + +flutter { + source = "../.." +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/debug/AndroidManifest.xml b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..399f6981d5d3 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..74a78b939e5e --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/kotlin/io/flutter/plugins/firebase/dataconnect/example/MainActivity.kt b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/kotlin/io/flutter/plugins/firebase/dataconnect/example/MainActivity.kt new file mode 100644 index 000000000000..edca5bed7fed --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/kotlin/io/flutter/plugins/firebase/dataconnect/example/MainActivity.kt @@ -0,0 +1,5 @@ +package io.flutter.plugins.firebase.dataconnect.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/drawable/launch_background.xml b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/values-night/styles.xml b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..06952be745f9 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/values/styles.xml b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..cb1ef88056ed --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/profile/AndroidManifest.xml b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..399f6981d5d3 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/build.gradle b/packages/firebase_data_connect/firebase_data_connect/example/android/build.gradle new file mode 100644 index 000000000000..d2ffbffa4cd2 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/gradle.properties b/packages/firebase_data_connect/firebase_data_connect/example/android/gradle.properties new file mode 100644 index 000000000000..3b5b324f6e3f --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_data_connect/firebase_data_connect/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..e1ca574ef017 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/settings.gradle b/packages/firebase_data_connect/firebase_data_connect/example/android/settings.gradle new file mode 100644 index 000000000000..7fb86d70412c --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/settings.gradle @@ -0,0 +1,28 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + // START: FlutterFire Configuration + id "com.google.gms.google-services" version "4.3.15" apply false + // END: FlutterFire Configuration + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} + +include ":app" diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect.yaml b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect.yaml new file mode 100644 index 000000000000..5a8746c0fbbd --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect.yaml @@ -0,0 +1,11 @@ +location: "us-west2" +specVersion: "v1alpha" +serviceId: "dataconnect" +schema: + source: "./schema" + datasource: + postgresql: + database: "dataconnect-test" + cloudSql: + instanceId: "local" +connectorDirs: ["./connector"] diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/implicit.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/implicit.gql new file mode 100755 index 000000000000..6023bf3bb134 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/implicit.gql @@ -0,0 +1,10 @@ +extend type DirectedBy { + movieId: UUID! @fdc_generated(from: "DirectedBy.movie", purpose: IMPLICIT_REF_FIELD) + directedbyId: UUID! @fdc_generated(from: "DirectedBy.directedby", purpose: IMPLICIT_REF_FIELD) +} +extend type Movie { + id: UUID! @default(expr: "uuidV4()") @fdc_generated(from: "Movie", purpose: IMPLICIT_KEY_FIELD) +} +extend type Person { + id: UUID! @default(expr: "uuidV4()") @fdc_generated(from: "Person", purpose: IMPLICIT_KEY_FIELD) +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/input.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/input.gql new file mode 100755 index 000000000000..e9da28933899 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/input.gql @@ -0,0 +1,88 @@ +scalar DirectedBy_Key +scalar Movie_Key +scalar Person_Key +input DirectedBy_Data { + movieId: UUID + movieId_expr: UUID_Expr + directedbyId: UUID + directedbyId_expr: UUID_Expr + directedby: Person_Key + movie: Movie_Key +} +input DirectedBy_Filter { + _and: [DirectedBy_Filter!] + _not: DirectedBy_Filter + _or: [DirectedBy_Filter!] + movieId: UUID_Filter + directedbyId: UUID_Filter + directedby: Person_Filter + movie: Movie_Filter +} +input DirectedBy_ListFilter { + count: Int_Filter = {gt:0} + exist: DirectedBy_Filter +} +input DirectedBy_Order { + movieId: OrderDirection + directedbyId: OrderDirection +} +input Movie_Data { + id: UUID + id_expr: UUID_Expr + description: String + description_expr: String_Expr + genre: String + genre_expr: String_Expr + rating: Float + releaseYear: Int + title: String + title_expr: String_Expr +} +input Movie_Filter { + _and: [Movie_Filter!] + _not: Movie_Filter + _or: [Movie_Filter!] + id: UUID_Filter + description: String_Filter + genre: String_Filter + rating: Float_Filter + releaseYear: Int_Filter + title: String_Filter + directedBies_on_movie: DirectedBy_ListFilter + people_via_DirectedBy: Person_ListFilter +} +input Movie_ListFilter { + count: Int_Filter = {gt:0} + exist: Movie_Filter +} +input Movie_Order { + id: OrderDirection + description: OrderDirection + genre: OrderDirection + rating: OrderDirection + releaseYear: OrderDirection + title: OrderDirection +} +input Person_Data { + id: UUID + id_expr: UUID_Expr + name: String + name_expr: String_Expr +} +input Person_Filter { + _and: [Person_Filter!] + _not: Person_Filter + _or: [Person_Filter!] + id: UUID_Filter + name: String_Filter + directedBies_on_directedby: DirectedBy_ListFilter + movies_via_DirectedBy: Movie_ListFilter +} +input Person_ListFilter { + count: Int_Filter = {gt:0} + exist: Person_Filter +} +input Person_Order { + id: OrderDirection + name: OrderDirection +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/mutation.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/mutation.gql new file mode 100755 index 000000000000..0feaae08b761 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/mutation.gql @@ -0,0 +1,74 @@ +extend type Mutation { + """ + Insert a single DirectedBy into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + directedBy_insert(data: DirectedBy_Data!): DirectedBy_Key! @fdc_generated(from: "DirectedBy", purpose: INSERT_SINGLE) + """ + Insert a single Movie into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + movie_insert(data: Movie_Data!): Movie_Key! @fdc_generated(from: "Movie", purpose: INSERT_SINGLE) + """ + Insert a single Person into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + person_insert(data: Person_Data!): Person_Key! @fdc_generated(from: "Person", purpose: INSERT_SINGLE) + """ + Insert or update a single DirectedBy into the table, based on the primary key. Returns the key of the newly inserted DirectedBy. + """ + directedBy_upsert(data: DirectedBy_Data!): DirectedBy_Key! @fdc_generated(from: "DirectedBy", purpose: UPSERT_SINGLE) + """ + Insert or update a single Movie into the table, based on the primary key. Returns the key of the newly inserted Movie. + """ + movie_upsert(data: Movie_Data!): Movie_Key! @fdc_generated(from: "Movie", purpose: UPSERT_SINGLE) + """ + Insert or update a single Person into the table, based on the primary key. Returns the key of the newly inserted Person. + """ + person_upsert(data: Person_Data!): Person_Key! @fdc_generated(from: "Person", purpose: UPSERT_SINGLE) + """ + Update a single DirectedBy based on `id` or `key`, setting columns specified in `data`. Returns `null` if not found. + """ + directedBy_update(key: DirectedBy_Key, data: DirectedBy_Data!): DirectedBy_Key @fdc_generated(from: "DirectedBy", purpose: UPDATE_SINGLE) + """ + Update a single Movie based on `id` or `key`, setting columns specified in `data`. Returns `null` if not found. + """ + movie_update(id: UUID, key: Movie_Key, data: Movie_Data!): Movie_Key @fdc_generated(from: "Movie", purpose: UPDATE_SINGLE) + """ + Update a single Person based on `id` or `key`, setting columns specified in `data`. Returns `null` if not found. + """ + person_update(id: UUID, key: Person_Key, data: Person_Data!): Person_Key @fdc_generated(from: "Person", purpose: UPDATE_SINGLE) + """ + Update DirectedBy entries matching `where` conditions (or `all`, if true) according to `data`. Returns the number of rows updated. + """ + directedBy_updateMany(where: DirectedBy_Filter, all: Boolean = false, data: DirectedBy_Data!): Int! @fdc_generated(from: "DirectedBy", purpose: UPDATE_MULTIPLE) + """ + Update Movie entries matching `where` conditions (or `all`, if true) according to `data`. Returns the number of rows updated. + """ + movie_updateMany(where: Movie_Filter, all: Boolean = false, data: Movie_Data!): Int! @fdc_generated(from: "Movie", purpose: UPDATE_MULTIPLE) + """ + Update Person entries matching `where` conditions (or `all`, if true) according to `data`. Returns the number of rows updated. + """ + person_updateMany(where: Person_Filter, all: Boolean = false, data: Person_Data!): Int! @fdc_generated(from: "Person", purpose: UPDATE_MULTIPLE) + """ + Delete a single DirectedBy based on `id` or `key` and return its key (or `null` if not found). + """ + directedBy_delete(key: DirectedBy_Key): DirectedBy_Key @fdc_generated(from: "DirectedBy", purpose: DELETE_SINGLE) + """ + Delete a single Movie based on `id` or `key` and return its key (or `null` if not found). + """ + movie_delete(id: UUID, key: Movie_Key): Movie_Key @fdc_generated(from: "Movie", purpose: DELETE_SINGLE) + """ + Delete a single Person based on `id` or `key` and return its key (or `null` if not found). + """ + person_delete(id: UUID, key: Person_Key): Person_Key @fdc_generated(from: "Person", purpose: DELETE_SINGLE) + """ + Delete DirectedBy entries matching `where` conditions (or `all`, if true). Returns the number of rows deleted. + """ + directedBy_deleteMany(where: DirectedBy_Filter, all: Boolean = false): Int! @fdc_generated(from: "DirectedBy", purpose: DELETE_MULTIPLE) + """ + Delete Movie entries matching `where` conditions (or `all`, if true). Returns the number of rows deleted. + """ + movie_deleteMany(where: Movie_Filter, all: Boolean = false): Int! @fdc_generated(from: "Movie", purpose: DELETE_MULTIPLE) + """ + Delete Person entries matching `where` conditions (or `all`, if true). Returns the number of rows deleted. + """ + person_deleteMany(where: Person_Filter, all: Boolean = false): Int! @fdc_generated(from: "Person", purpose: DELETE_MULTIPLE) +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/query.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/query.gql new file mode 100755 index 000000000000..a1d2505e4111 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/query.gql @@ -0,0 +1,26 @@ +extend type Query { + """ + Look up a single DirectedBy based on `id` or `key` and return selected fields (or `null` if not found). + """ + directedBy(key: DirectedBy_Key): DirectedBy @fdc_generated(from: "DirectedBy", purpose: QUERY_SINGLE) + """ + Look up a single Movie based on `id` or `key` and return selected fields (or `null` if not found). + """ + movie(id: UUID, key: Movie_Key): Movie @fdc_generated(from: "Movie", purpose: QUERY_SINGLE) + """ + Look up a single Person based on `id` or `key` and return selected fields (or `null` if not found). + """ + person(id: UUID, key: Person_Key): Person @fdc_generated(from: "Person", purpose: QUERY_SINGLE) + """ + List DirectedBy entries in the table, optionally filtered by `where` conditions. + """ + directedBies(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE) + """ + List Movie entries in the table, optionally filtered by `where` conditions. + """ + movies(where: Movie_Filter, orderBy: [Movie_Order!], limit: Int = 100): [Movie!]! @fdc_generated(from: "Movie", purpose: QUERY_MULTIPLE) + """ + List Person entries in the table, optionally filtered by `where` conditions. + """ + people(where: Person_Filter, orderBy: [Person_Order!], limit: Int = 100): [Person!]! @fdc_generated(from: "Person", purpose: QUERY_MULTIPLE) +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/relation.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/relation.gql new file mode 100755 index 000000000000..0691f598203e --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/relation.gql @@ -0,0 +1,20 @@ +extend type Movie { + """ + ✨ List DirectedBy entries in a one-to-many relationship with this object (i.e. where `DirectedBy.movie` equals this object). + """ + directedBies_on_movie(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy.movie", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + """ + ✨ List related Person entries using DirectedBy as a join table (i.e. where an entry of DirectedBy exists whose `movie` == this and `directedby` == that). + """ + people_via_DirectedBy(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [Person!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE_MANY_TO_MANY) +} +extend type Person { + """ + ✨ List DirectedBy entries in a one-to-many relationship with this object (i.e. where `DirectedBy.directedby` equals this object). + """ + directedBies_on_directedby(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy.directedby", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + """ + ✨ List related Movie entries using DirectedBy as a join table (i.e. where an entry of DirectedBy exists whose `directedby` == this and `movie` == that). + """ + movies_via_DirectedBy(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [Movie!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE_MANY_TO_MANY) +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/prelude.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/prelude.gql new file mode 100755 index 000000000000..a313edf0cca7 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/prelude.gql @@ -0,0 +1,1031 @@ +"AccessLevel specifies coarse access policies for common situations." +enum AccessLevel { + """ + This operation can be executed by anyone with or without authentication. + Equivalent to @auth(expr: "true") + """ + PUBLIC + + """ + This operation can only be executed with a valid Firebase Auth ID token. + Note: it allows anonymous auth and unverified accounts, so may be subjected to abuses. + It’s equivalent to @auth(expr: "auth.uid != nil") + """ + USER_ANON + + """ + This operation can only be executed by a non-anonymous Firebase Auth account. + It’s equivalent to @auth(expr: "auth.uid != nil && auth.token.firebase.sign_in_provider != 'anonymous'")" + """ + USER + + """ + This operation can only be executed by a verified Firebase Auth account. + It’s equivalent to @auth(expr: "auth.uid != nil && auth.token.email_verified")" + """ + USER_EMAIL_VERIFIED + + """ + This operation can not be executed with no IAM credentials. + It’s equivalent to @auth(expr: "false") + """ + NO_ACCESS +} + +""" +Defines the auth policy for a query or mutation. This directive must be added to +any operation you wish to be accessible from a client application. If left +unspecified, defaults to `@auth(level: NO_ACCESS)`. +""" +directive @auth( + "The minimal level of access required to perform this operation." + level: AccessLevel @fdc_oneOf(required: true) + """ + A CEL expression that allows access to this operation if the expression + evaluates to `true`. + """ + expr: Boolean_Expr @fdc_oneOf(required: true) +) on QUERY | MUTATION +"Conditions on a string value" +input String_Filter { + isNull: Boolean + eq: String @fdc_oneOf(group: "eq") + eq_expr: String_Expr @fdc_oneOf(group: "eq") + ne: String @fdc_oneOf(group: "ne") + ne_expr: String_Expr @fdc_oneOf(group: "ne") + in: [String!] + nin: [String!] + gt: String + ge: String + lt: String + le: String + contains: String + startsWith: String + endsWith: String + pattern: String_Pattern +} + +""" +The pattern match condition on a string. Specify either like or regex. +https://www.postgresql.org/docs/current/functions-matching.html +""" +input String_Pattern { + "the LIKE expression to use" + like: String + "the POSIX regular expression" + regex: String + "when true, it's case-insensitive. In Postgres: ILIKE, ~*" + ignoreCase: Boolean + "when true, invert the condition. In Postgres: NOT LIKE, !~" + invert: Boolean +} + +"Conditions on a string list" +input String_ListFilter { + includes: String + excludes: String + includesAll: [String!] + excludesAll: [String!] +} + +"Conditions on a UUID value" +input UUID_Filter { + isNull: Boolean + eq: UUID + ne: UUID + in: [UUID!] + nin: [UUID!] +} + +"Conditions on a UUID list" +input UUID_ListFilter { + includes: UUID + excludes: UUID + includesAll: [UUID!] + excludesAll: [UUID!] +} + +"Conditions on an Int value" +input Int_Filter { + isNull: Boolean + eq: Int + ne: Int + in: [Int!] + nin: [Int!] + gt: Int + ge: Int + lt: Int + le: Int +} + +"Conditions on an Int list" +input Int_ListFilter { + includes: Int + excludes: Int + includesAll: [Int!] + excludesAll: [Int!] +} + +"Conditions on an Int64 value" +input Int64_Filter { + isNull: Boolean + eq: Int64 + ne: Int64 + in: [Int64!] + nin: [Int64!] + gt: Int64 + ge: Int64 + lt: Int64 + le: Int64 +} + +"Conditions on an Int64 list" +input Int64_ListFilter { + includes: Int64 + excludes: Int64 + includesAll: [Int64!] + excludesAll: [Int64!] +} + +"Conditions on a Float value" +input Float_Filter { + isNull: Boolean + eq: Float + ne: Float + in: [Float!] + nin: [Float!] + gt: Float + ge: Float + lt: Float + le: Float +} + +"Conditions on a Float list" +input Float_ListFilter { + includes: Float + excludes: Float + includesAll: [Float!] + excludesAll: [Float!] +} + +"Conditions on a Boolean value" +input Boolean_Filter { + isNull: Boolean + eq: Boolean + ne: Boolean + in: [Boolean!] + nin: [Boolean!] +} + +"Conditions on a Boolean list" +input Boolean_ListFilter { + includes: Boolean + excludes: Boolean + includesAll: [Boolean!] + excludesAll: [Boolean!] +} + +"Conditions on an Any value" +input Any_Filter { + isNull: Boolean + eq: Any + ne: Any + in: [Any!] + nin: [Any!] +} + +"Conditions on a Any list" +input Any_ListFilter { + includes: Any + excludes: Any + includesAll: [Any!] + excludesAll: [Any!] +} +""" +(Internal) A string that uniquely identifies a type, field, and so on. + +The most common usage in FDC is `SomeType` or `SomeType.someField`. See the +linked page in the @specifiedBy directive for the GraphQL RFC with more details. +""" +scalar SchemaCoordinate + @specifiedBy(url: "https://github.com/graphql/graphql-wg/blob/6d02705dea034fb65ebc6799632adb7bd550d0aa/rfcs/SchemaCoordinates.md") + @fdc_forbiddenAsFieldType + @fdc_forbiddenAsVariableType + +"(Internal) The purpose of a generated type or field." +enum GeneratedPurpose { + # Implicit fields added to the table types as columns. + IMPLICIT_KEY_FIELD + IMPLICIT_REF_FIELD + + # Relational non-column fields extended to table types. + QUERY_MULTIPLE_ONE_TO_MANY + QUERY_MULTIPLE_MANY_TO_MANY + + # Top-level Query fields. + QUERY_SINGLE + QUERY_MULTIPLE + QUERY_MULTIPLE_BY_SIMILARITY + + # Top-level Mutation fields. + INSERT_SINGLE + INSERT_MULTIPLE + UPSERT_SINGLE + UPSERT_MULTIPLE + UPDATE_SINGLE + UPDATE_MULTIPLE + DELETE_SINGLE + DELETE_MULTIPLE +} + +"(Internal) Added to definitions generated by FDC." +directive @fdc_generated( + "The source type or field that causes this definition to be generated." + from: SchemaCoordinate! + "The reason why this definition is generated, such as the intended use case." + purpose: GeneratedPurpose! +) on + | SCALAR + | OBJECT + | FIELD_DEFINITION + | ARGUMENT_DEFINITION + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION +""" +Defines a database index to optimize query performance. + +Given `type TableName @table @index(fields: [“fieldName”, “secondFieldName”])`, +`table_name_field_name_second_field_name_aa_idx` is the SQL index id. +`table_name_field_name_second_field_name_ad_idx` if `order: [ASC DESC]`. +`table_name_field_name_second_field_name_dd_idx` if `order: [DESC DESC]`. + +Given `type TableName @table { fieldName: Int @index } ` +`table_name_field_name_idx` is the SQL index id. +`order` matters less for single field indexes because they can be scanned in both ways. + +Override with `@index(name)` in case of index name conflicts. +""" +directive @index( + "The SQL database index id. Defaults to __idx." + name: String + """ + Only allowed and required when used on OBJECT. + The fields to create an index on. + """ + fields: [String!] + """ + Only allowed when used on OBJECT. + Index order of each column. Default to all ASC. + """ + order: [IndexFieldOrder!] +) repeatable on FIELD_DEFINITION | OBJECT + +enum IndexFieldOrder { ASC DESC } +type Query { + _service: _Service! +} + +type Mutation { + # This is just a dummy field so that Mutation is always non-empty. + _firebase: Void @fdc_deprecated(reason: "dummy field -- does nothing useful") +} + +type _Service { + sdl: String! +} + +"(Internal) Added to things that may be removed from FDC and will soon be no longer usable in schema or operations." +directive @fdc_deprecated(reason: String = "No longer supported") on + | SCHEMA + | SCALAR + | OBJECT + | FIELD_DEFINITION + | ARGUMENT_DEFINITION + | INTERFACE + | UNION + | ENUM + | ENUM_VALUE + | INPUT_OBJECT + | INPUT_FIELD_DEFINITION + +"(Internal) Added to scalars representing quoted CEL expressions." +directive @fdc_celExpression( + "The expected CEL type that the expression should evaluate to." + returnType: String +) on SCALAR + +"(Internal) Added to scalars representing quoted SQL expressions." +directive @fdc_sqlExpression( + "The expected SQL type that the expression should evaluate to." + dataType: String +) on SCALAR + +"(Internal) Added to types that may not be used as variables." +directive @fdc_forbiddenAsVariableType on SCALAR | OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT + +"(Internal) Added to types that may not be used as fields in schema." +directive @fdc_forbiddenAsFieldType on SCALAR | OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT + +"Provides a frequently used example for this type / field / argument." +directive @fdc_example( + "A GraphQL literal value (verbatim) whose type matches the target." + value: Any + "A human-readable text description of what `value` means in this context." + description: String +) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +"(Internal) Marks this field / argument as conflicting with others in the same group." +directive @fdc_oneOf( + "The group name where fields / arguments conflict with each other." + group: String! = "" + "If true, exactly one field / argument in the group must be specified." + required: Boolean! = false +) repeatable on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION +""" +UUID is a string with hex digits representing an RFC4122 value. + +Outputs will always be 32 lower-case hex digits with no delimiters or curly +braces. Inputs in the following formats are also accepted (case insensitive): + + xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} +""" +scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") +scalar Int64 + +scalar Any +scalar Void +""" +The `True` scalar type only accepts the boolean `true`. + +An optional field / argument typed `True` may either be set to `true` or omitted +(not provided at all). `false` or `null` is not accepted. +""" +scalar True + @fdc_forbiddenAsFieldType + @fdc_forbiddenAsVariableType + @fdc_example(value: true, description: "The only allowed value.") +"Define the intervals used in timestamps and dates (subset)" +enum TimestampInterval @fdc_deprecated { + second + minute + hour + day + week + month + year +} +""" +A Common Expression Language (CEL) expression that returns a boolean at runtime. + +The expression can reference the `auth` variable, which is null if Firebase Auth +is not used. Otherwise, it contains the following fields: + + - `auth.uid`: The current user ID. + - `auth.token`: A map of all token fields (i.e. "claims"). +""" +scalar Boolean_Expr + @specifiedBy(url: "https://github.com/google/cel-spec") + @fdc_celExpression(returnType: "bool") + @fdc_forbiddenAsVariableType + @fdc_forbiddenAsFieldType + @fdc_example(value: "auth != null", description: "Allow only if a Firebase Auth user is present.") + +""" +A Common Expression Language (CEL) expression that returns a string at runtime. + +Limitation: Right now, only a few expressions are supported. Those are listed +using the @fdc_example directive on this scalar. +""" +scalar String_Expr + @specifiedBy(url: "https://github.com/google/cel-spec") + @fdc_celExpression(returnType: "string") + @fdc_forbiddenAsVariableType + @fdc_forbiddenAsFieldType + @fdc_example(value: "auth.uid", description: "The ID of the currently logged in user in Firebase Auth. (Errors if not logged in.)") + @fdc_example(value: "uuidV4()", description: "Generates a new random UUID (version 4) string, formatted as 32 lower-case hex digits without delimiters.") + +""" +A Common Expression Language (CEL) expression that returns a UUID string at runtime. + +Limitation: Right now, only a few expressions are supported. Those are listed +using the @fdc_example directive on this scalar. +""" +scalar UUID_Expr + @specifiedBy(url: "https://github.com/google/cel-spec") + @fdc_celExpression(returnType: "string") + @fdc_forbiddenAsVariableType + @fdc_forbiddenAsFieldType + @fdc_example(value: "uuidV4()", description: "Generates a new random UUID (version 4) every time.") + +""" +A Common Expression Language (CEL) expression whose return type is unspecified. + +Limitation: Only a limited set of expressions are supported for now for each +type. For type XXX, see the @fdc_example directives on XXX_Expr for a full list. +""" +scalar Any_Expr + @specifiedBy(url: "https://github.com/google/cel-spec") + @fdc_celExpression + @fdc_forbiddenAsVariableType + @fdc_forbiddenAsFieldType + @fdc_example(value: "auth.uid", description: "The ID of the currently logged in user in Firebase Auth. (Errors if not logged in.)") + @fdc_example(value: "uuidV4()", description: "Generates a new random UUID version 4 (formatted as 32 lower-case hex digits without delimiters if result type is String).") + @fdc_example(value: "request.time", description: "The timestamp when the request is received (with microseconds precision).") + +""" +A PostgreSQL value expression whose return type is unspecified. +""" +scalar Any_SQL + @specifiedBy(url: "https://www.postgresql.org/docs/current/sql-expressions.html") + @fdc_sqlExpression + @fdc_forbiddenAsVariableType + @fdc_forbiddenAsFieldType +""" +Defines a relational database table. + +Given `type TableName @table`, + + - `TableName` is the GQL type name. + - `tableName` is the singular name. Override with `@table(singular)`. + - `tableNames` is the plural name. Override with `@table(plural)`. + - `table_name` is the SQL table id. Override with `@table(name)`. + +Only a table type can be configured further with: + + - Customized data types. See `@col`. + - Index. See `@index` + - Unique constraint. See `@unqiue` + - Relation. See `@ref` + - Embedded Json. See `@embed` + +A scalar field map to a SQL database column. +An object field (like `type TableName @table { field: AnotherType }`) are either + + - a relation reference field if `AnotherType` is annotated with `@table`. + - an embedded json field if `field` is annotated with `@embed`. + +""" +directive @table( + "Override the SQL database table name. Defaults to ." + name: String + "Override the singular name. Default is the camel case of the type name." + singular: String + "Override the plural name. Default is generated based on English patterns." + plural: String + "The primary key of the table. Defaults to a single field `id: UUID!`. Generate if missing." + key: [String!] +) on OBJECT + +""" +Defines a relational database view. + +Given `type ViewName @view`, + - `ViewName` is the GQL type name. + - `viewName` is the singular name. Override with `@view(singular)`. + - `viewNames` is the plural name. Override with `@view(plural)`. + - `view_name` is the SQL view id. Override with `@view(name)`. + When `@view(sql)` is defined, it uses the given raw SQL as the view instead. + +A view type can be used just as a table type with queries. +A view type may have a nullable `@ref` field to another table, but cannot be +referenced in a `@ref`. + +WARNING: Firebase Data Connect does not validate the SQL of the view or +evaluate whether it matches the defined fields. + +If the SQL view is invalid or undefined, related requests may fail. +If the SQL view return incompatible types. Firebase Data Connect will surface +an error in the response. +""" +directive @view( + """ + The SQL view name. If no `name` or `sql` are provided, defaults to + snake_case of the singular type name. + """ + name: String @fdc_oneOf + """ + SQL SELECT statement to use as the basis for this type. Note that all SQL + identifiers should be snake_case and all GraphQL identifiers should be + camelCase. + """ + sql: String @fdc_oneOf + "Override the singular name. Default is the camel case of the type name." + singular: String + "Override the plural name. Default is generated based on English patterns." + plural: String +) on OBJECT + +""" +Specify additional column options. + +Given `type TableName @table { fieldName: Int } ` + + - `field_name` is the SQL column name. Override with `@col(name)`. + +""" +directive @col( + "The SQL database column name. Defaults to ." + name: String + """ + Override SQL columns data type. + Each GraphQL type could map to many SQL data types. + Refer to Postgres supported data types and mappings to GQL. + """ + dataType: String + """ + Defines a fixed column size for certain scalar types. + + - For Vector, size is required. It establishes the length of the vector. + - For String, size converts `text` type to `varchar(size)`. + """ + size: Int +) on FIELD_DEFINITION + + +""" +Define a reference field to another table. + +Given `type TableName @table { refField: AnotherTableName }`, it defines a foreign-key constraint + + - with id `table_name_ref_field_fkey` (override with `@ref(constraintName)`) + - from `table_name.ref_field` (override with `@ref(fields)`) + - to `another_table_name.id` (override with `@ref(references)`) + +Does not support `[AnotherTableName]` because array fields cannot have foreign-key constraints. +Nullability determines whether the reference is required. + + - `refField: AnotherTableName`: optional reference, SET_NULL on delete. + - `refField: AnotherTableName!`: required reference, CASCADE on delete. + +Consider all types of SQL relations: + + - many-to-one relations involve a reference field on the many-side. + - many-to-maybe-one if `refField: AnotherTableName`. + - many-to-exactly-one if `refField: AnotherTableName!`. + - one-to-one relations involve a unique reference field on one side. + - maybe-one-to-maybe-one if `refField: AnotherTableName @unique`. + - maybe-one-to-exact-one if `refField: AnotherTableName! @unique`. + - exact-one-to-exact-one shall be represented as a single table instead. + - many-to-many relations involve a join table. + - Its primary keys must be two non-null reference fields to tables bridged together to guarantee at most one relation per pair. + +type TableNameToAnotherTableName @table(key: ["refField", "anotherRefField"]) { + refField: TableName! + anotherRefField: AnotherTableName! +} + +""" +directive @ref( + "The SQL database foreign key constraint name. Default to __fkey." + constraintName: String + """ + Foreign key fields. Default to . + """ + fields: [String!] + "The fields that the foreign key references in the other table. Default to the primary key." + references: [String!] +) on FIELD_DEFINITION + +"Define the direction of an orderby query" +enum OrderDirection { + ASC + DESC +} + +enum ColDefault @fdc_deprecated { + """ + Generates a random UUID (v4) as the default column value. + Compatible with String or UUID typed fields. + """ + UUID + """ + Generates an auto-incrementing sequence as the default column value. + Compatible with Int and Int64 typed fields. + """ + SEQUENCE + """ + Populates the default column value with the current date or timestamp. + Compatible with Date and Timestamp typed fields. + """ + NOW +} + +""" +Specify the default column value. + +The supported arguments vary based on the field type. +""" +directive @default( + "A constant value. Validated against the field GraphQL type at compile-time." + value: Any @fdc_oneOf(required: true) + "(Deprecated) Built-in common ways to generate initial value." + generate: ColDefault @fdc_oneOf(required: true) @deprecated + "A CEL expression, whose return value must match the field data type." + expr: Any_Expr @fdc_oneOf(required: true) + """ + A raw SQL expression, whose SQL data type must match the underlying column. + + The value is any variable-free expression (in particular, cross-references to + other columns in the current table are not allowed). Subqueries are not allowed either. + https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-DEFAULT + """ + sql: Any_SQL @fdc_oneOf(required: true) +) on FIELD_DEFINITION +""" +Date is a string in the YYYY-MM-DD format representing a local-only date. + +See the description for Timestamp for range and limitations. + +As a FDC-specific extension, inputs that includes time portions (as specified by +the Timestamp scalar) are accepted but only the date portion is used. In other +words, only the part before "T" is used and the rest discarded. This effectively +truncates it to the local date in the specified time-zone. + +Outputs will always be in the canonical YYYY-MM-DD format. +""" +scalar Date @specifiedBy(url: "https://scalars.graphql.org/andimarek/local-date.html") + +""" +Timestamp is a RFC 3339 string that represents an exact point in time. + +The serialization format follows https://scalars.graphql.org/andimarek/date-time +except the "Non-optional exact milliseconds" Section. As a FDC-specific +extension, inputs and outputs may contain 0, 3, 6, or 9 fractional digits. + +Specifically, output precision varies by server-side factors such as data source +support and clients must not rely on an exact number of digits. Clients may +truncate extra digits as fit, with the caveat that there may be information loss +if the truncated value is subsequently sent back to the server. + +FDC only supports year 1583 to 9999 (inclusive) and uses the ISO-8601 calendar +system for all date-time calculations. Notably, the expanded year representation +(+/-YYYYY) is rejected and Year 1582 and before may either be rejected or cause +undefined behavior. +""" +scalar Timestamp @specifiedBy(url: "https://scalars.graphql.org/andimarek/date-time") + +""" +A Common Expression Language (CEL) expression that returns a Timestamp at runtime. + +Limitation: Right now, only a few expressions are supported. Those are listed +using the @fdc_example directive on this scalar. +""" +scalar Timestamp_Expr + @specifiedBy(url: "https://github.com/google/cel-spec") + @fdc_celExpression(returnType: "google.protobuf.Timestamp") + @fdc_forbiddenAsVariableType + @fdc_forbiddenAsFieldType + @fdc_example(value: "request.time", description: "The timestamp when the request is received (with microseconds precision).") + +""" +A Common Expression Language (CEL) expression that returns a Timestamp at runtime, +which is then truncated to UTC date only. The time-of-day parts are discarded. + +Limitation: Right now, only a few expressions are supported. Those are listed +using the @fdc_example directive on this scalar. +""" +scalar Date_Expr + @specifiedBy(url: "https://github.com/google/cel-spec") + @fdc_celExpression(returnType: "google.protobuf.Timestamp") + @fdc_forbiddenAsVariableType + @fdc_forbiddenAsFieldType + @fdc_example(value: "request.time", description: "The UTC date on which the request is received.") + +"Conditions on a Date value" +input Date_Filter { + isNull: Boolean + eq: Date @fdc_oneOf(group: "eq") + eq_expr: Date_Expr @fdc_oneOf(group: "eq") + eq_date: Date_Relative @fdc_oneOf(group: "eq") + ne: Date @fdc_oneOf(group: "ne") + ne_expr: Date_Expr @fdc_oneOf(group: "ne") + ne_date: Date_Relative @fdc_oneOf(group: "ne") + in: [Date!] + nin: [Date!] + gt: Date @fdc_oneOf(group: "gt") + gt_expr: Date_Expr @fdc_oneOf(group: "gt") + gt_date: Date_Relative @fdc_oneOf(group: "gt") + ge: Date @fdc_oneOf(group: "ge") + ge_expr: Date_Expr @fdc_oneOf(group: "ge") + ge_date: Date_Relative @fdc_oneOf(group: "ge") + lt: Date @fdc_oneOf(group: "lt") + lt_expr: Date_Expr @fdc_oneOf(group: "lt") + lt_date: Date_Relative @fdc_oneOf(group: "lt") + le: Date @fdc_oneOf(group: "le") + le_expr: Date_Expr @fdc_oneOf(group: "le") + le_date: Date_Relative @fdc_oneOf(group: "le") +} + +"Conditions on a Date list" +input Date_ListFilter { + includes: Date @fdc_oneOf(group: "includes") + includes_expr: Date_Expr @fdc_oneOf(group: "includes") + includes_date: Date_Relative @fdc_oneOf(group: "includes") + excludes: Date @fdc_oneOf(group: "excludes") + excludes_expr: Date_Expr @fdc_oneOf(group: "excludes") + excludes_date: Date_Relative @fdc_oneOf(group: "excludes") + includesAll: [Date!] + excludesAll: [Date!] +} + +"Conditions on an Timestamp value" +input Timestamp_Filter { + isNull: Boolean + eq: Timestamp @fdc_oneOf(group: "eq") + eq_expr: Timestamp_Expr @fdc_oneOf(group: "eq") + eq_time: Timestamp_Relative @fdc_oneOf(group: "eq") + ne: Timestamp @fdc_oneOf(group: "ne") + ne_expr: Timestamp_Expr @fdc_oneOf(group: "ne") + ne_time: Timestamp_Relative @fdc_oneOf(group: "ne") + in: [Timestamp!] + nin: [Timestamp!] + gt: Timestamp @fdc_oneOf(group: "gt") + gt_expr: Timestamp_Expr @fdc_oneOf(group: "gt") + gt_time: Timestamp_Relative @fdc_oneOf(group: "gt") + ge: Timestamp @fdc_oneOf(group: "ge") + ge_expr: Timestamp_Expr @fdc_oneOf(group: "ge") + ge_time: Timestamp_Relative @fdc_oneOf(group: "ge") + lt: Timestamp @fdc_oneOf(group: "lt") + lt_expr: Timestamp_Expr @fdc_oneOf(group: "lt") + lt_time: Timestamp_Relative @fdc_oneOf(group: "lt") + le: Timestamp @fdc_oneOf(group: "le") + le_expr: Timestamp_Expr @fdc_oneOf(group: "le") + le_time: Timestamp_Relative @fdc_oneOf(group: "le") +} + +"Conditions on a Timestamp list" +input Timestamp_ListFilter { + includes: Timestamp @fdc_oneOf(group: "includes") + includes_expr: Timestamp_Expr @fdc_oneOf(group: "includes") + includes_time: Timestamp_Relative @fdc_oneOf(group: "includes") + excludes: Timestamp @fdc_oneOf(group: "excludes") + excludes_expr: Timestamp_Expr @fdc_oneOf(group: "excludes") + excludes_time: Timestamp_Relative @fdc_oneOf(group: "excludes") + includesAll: [Timestamp!] + excludesAll: [Timestamp!] +} + +"Update input of a Date value" +input Date_Update { + set: Date @fdc_oneOf(group: "set") + set_expr: Date_Expr @fdc_oneOf(group: "set") + set_date: Date_Relative @fdc_oneOf(group: "set") +} + +"Update input of a Date list value" +input Date_ListUpdate { + set: [Date!] + append: [Date!] + prepend: [Date!] + delete: Int + i: Int + update: Date +} + +"Update input of a Timestamp value" +input Timestamp_Update { + set: Timestamp @fdc_oneOf(group: "set") + set_expr: Timestamp_Expr @fdc_oneOf(group: "set") + set_time: Timestamp_Relative @fdc_oneOf(group: "set") +} + +"Update input of a Timestamp list value" +input Timestamp_ListUpdate { + set: [Timestamp!] + append: [Timestamp!] + prepend: [Timestamp!] + delete: Int + i: Int + update: Timestamp +} + +"A runtime-calculated Timestamp value relative to `now` or `at`." +input Timestamp_Relative @fdc_forbiddenAsVariableType @fdc_forbiddenAsFieldType { + now: True @fdc_oneOf(group: "from", required: true) + at: Timestamp @fdc_oneOf(group: "from", required: true) + + add: Timestamp_Duration + sub: Timestamp_Duration + + truncateTo: Timestamp_Interval +} + +input Timestamp_Duration @fdc_forbiddenAsVariableType @fdc_forbiddenAsFieldType { + milliseconds: Int! = 0 + seconds: Int! = 0 + minutes: Int! = 0 + hours: Int! = 0 + days: Int! = 0 + weeks: Int! = 0 + months: Int! = 0 + years: Int! = 0 +} + +enum Timestamp_Interval @fdc_forbiddenAsFieldType { + SECOND + MINUTE + HOUR + DAY + WEEK + MONTH + YEAR +} + +"A runtime-calculated Date value relative to `today` or `on`." +input Date_Relative @fdc_forbiddenAsVariableType @fdc_forbiddenAsFieldType { + today: True @fdc_oneOf(group: "from", required: true) + on: Date @fdc_oneOf(group: "from", required: true) + + add: Date_Duration + sub: Date_Duration + + truncateTo: Date_Interval +} + +input Date_Duration @fdc_forbiddenAsVariableType @fdc_forbiddenAsFieldType { + days: Int! = 0 + weeks: Int! = 0 + months: Int! = 0 + years: Int! = 0 +} + +enum Date_Interval @fdc_forbiddenAsFieldType { + WEEK + MONTH + YEAR +} +"Update input of a String value" +input String_Update { + set: String @fdc_oneOf(group: "set") + set_expr: String_Expr @fdc_oneOf(group: "set") +} + +"Update input of a String list value" +input String_ListUpdate { + set: [String!] + append: [String!] + prepend: [String!] +} + +"Update input of a UUID value" +input UUID_Update { + set: UUID @fdc_oneOf(group: "set") + set_expr: UUID_Expr @fdc_oneOf(group: "set") +} + +"Update input of an ID list value" +input UUID_ListUpdate { + set: [UUID!] + append: [UUID!] + prepend: [UUID!] +} + +"Update input of an Int value" +input Int_Update { + set: Int + inc: Int + dec: Int +} + +"Update input of an Int list value" +input Int_ListUpdate { + set: [Int!] + append: [Int!] + prepend: [Int!] +} + +"Update input of an Int64 value" +input Int64_Update { + set: Int64 + inc: Int64 + dec: Int64 +} + +"Update input of an Int64 list value" +input Int64_ListUpdate { + set: [Int64!] + append: [Int64!] + prepend: [Int64!] +} + +"Update input of a Float value" +input Float_Update { + set: Float + inc: Float + dec: Float +} + +"Update input of a Float list value" +input Float_ListUpdate { + set: [Float!] + append: [Float!] + prepend: [Float!] +} + +"Update input of a Boolean value" +input Boolean_Update { + set: Boolean +} + +"Update input of a Boolean list value" +input Boolean_ListUpdate { + set: [Boolean!] + append: [Boolean!] + prepend: [Boolean!] +} + +"Update input of an Any value" +input Any_Update { + set: Any +} + +"Update input of an Any list value" +input Any_ListUpdate { + set: [Any!] + append: [Any!] + prepend: [Any!] +} +""" +Vector is an array of single-precision floating-point numbers, serialized +as a JSON array. All elements must be finite (no NaN, Infinity or -Infinity). + +Example: [1.1, 2, 3.3] +""" +scalar Vector + +""" +Defines what siliarlity function to use for fetching vectors. +Details here: https://github.com/pgvector/pgvector?tab=readme-ov-file#vector-functions +""" +enum VectorSimilarityMethod { + L2 + COSINE + INNER_PRODUCT +} + +"Conditions on an Vector value" +input Vector_Filter { + eq: Vector + ne: Vector + in: [Vector!] + nin: [Vector!] + isNull: Boolean +} + +input Vector_ListFilter { + "When true, will match if the list includes the supplied vector." + includes: Vector + excludes: Vector + includesAll: [Vector!] + excludesAll: [Vector!] +} + +"Update input of an Vector value" +input Vector_Update { + set: Vector @fdc_oneOf(group: "set") + set_embed: Vector_Embed @fdc_oneOf(group: "set") +} + +"Update input of a Vector list value" +input Vector_ListUpdate { + set: [Vector] + append: [Vector] + prepend: [Vector] + delete: Int + i: Int + update: Vector +} + +""" +Create a vector embedding of text using the given model on Vertex AI. + +Example: {text: "Hi there", model: "textembedding-gecko@003"} +See: https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-text-embeddings + +Limitation: In the emulator, if Vertex AI is not configured, a pseudorandom +vector is generated from text as a fake implementation for testing only. +""" +input Vector_Embed @fdc_forbiddenAsVariableType { + model: Vector_Embed_Model! + text: String! +} + +""" +A string that specifies a Vertex AI model and version. + +It is strongly recommended to specify a stable model version (for example, +textembedding-gecko@003). The latest version of a model is in Preview and is not +General Availability (GA). Because the latest version is in Preview, it isn't +guaranteed to be production ready. +""" +scalar Vector_Embed_Model + @specifiedBy(url: "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/model-versioning") + @fdc_forbiddenAsVariableType + @fdc_forbiddenAsFieldType + @fdc_example(value: "textembedding-gecko@003", description: "A stable version of the textembedding-gecko model") diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/connector.yaml b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/connector.yaml new file mode 100644 index 000000000000..4e69ba66469e --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/connector.yaml @@ -0,0 +1,6 @@ +connectorId: movies +authMode: PUBLIC +generate: + dartSdk: + outputDir: ../../lib/generated + package: movies diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/movie_insert.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/movie_insert.gql new file mode 100644 index 000000000000..d78a25b80411 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/movie_insert.gql @@ -0,0 +1,210 @@ +# mutation { +# movie1: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440000", +# title: "Inception", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Finception.jpg?alt=media&token=07b09781-b302-4623-a5c3-1956d0143168", +# releaseYear: 2010, +# genre: "sci-fi", +# rating: 8.8, +# description: "Dom Cobb (Leonardo DiCaprio) is a thief with the rare ability to enter people's dreams and steal their secrets from their subconscious. His skill has made him a valuable player in the world of corporate espionage but has also cost him everything he loves. Cobb gets a chance at redemption when he is offered a seemingly impossible task: plant an idea in someone's mind. If he succeeds, it will be the perfect crime, but a dangerous enemy anticipates Cobb's every move.", +# tags: ["thriller", "action"], +# }) + +# movie2: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440001", +# title: "The Matrix", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_matrix.jpg?alt=media&token=4975645d-fef8-409e-84a5-bcc1046e2059", +# releaseYear: 1999, +# genre: "action", +# rating: 8.7, +# description: "Thomas Anderson, a computer programmer, discovers that the world is actually a simulation controlled by malevolent machines in a dystopian future. Known as Neo, he joins a group of underground rebels led by Morpheus to fight the machines and free humanity. Along the way, Neo learns to manipulate the simulated reality, uncovering his true destiny.", +# tags: ["sci-fi", "adventure"], +# }) + +# movie3: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440002", +# title: "John Wick 4", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fjohn_wick_4.jpg?alt=media&token=463ed467-9daa-4281-965d-44e7cc4172d5", +# releaseYear: 2023, +# genre: "action", +# rating: 8.1, +# description: "John Wick (Keanu Reeves) uncovers a path to defeating The High Table, but before he can earn his freedom, he must face off against a new enemy with powerful alliances across the globe. The film follows Wick as he battles through various international locations, facing relentless adversaries and forming new alliances.", +# tags: ["action", "thriller"], +# }) + +# movie4: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440003", +# title: "The Dark Knight", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_dark_knight.jpg?alt=media&token=a9803c59-40d5-4758-a6f4-9a7c274a1218", +# releaseYear: 2008, +# genre: "action", +# rating: 9.0, +# description: "When the menace known as the Joker (Heath Ledger) emerges from his mysterious past, he wreaks havoc and chaos on the people of Gotham. The Dark Knight (Christian Bale) must accept one of the greatest psychological and physical tests of his ability to fight injustice.", +# tags: ["action", "drama"], +# }) + +# movie5: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440004", +# title: "Fight Club", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Ffight_club.jpg?alt=media&token=a4bc1933-2607-42cd-a860-e44c4587fd9c", +# releaseYear: 1999, +# genre: "drama", +# rating: 8.8, +# description: "An insomniac office worker (Edward Norton) and a devil-may-care soapmaker (Brad Pitt) form an underground fight club that evolves into something much more. The story explores themes of consumerism, masculinity, and the search for identity.", +# tags: ["drama", "thriller"], +# }) + +# movie6: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440005", +# title: "Pulp Fiction", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fpulp_fiction.jpg?alt=media&token=0df86e18-5cb1-45b3-a6d9-3f41563c3465", +# releaseYear: 1994, +# genre: "crime", +# rating: 8.9, +# description: "The lives of two mob hitmen, a boxer, a gangster and his wife, and a pair of diner bandits intertwine in four tales of violence and redemption. The film is known for its eclectic dialogue, ironic mix of humor and violence, and a host of cinematic allusions and pop culture references.", +# tags: ["crime", "drama"], +# }) + +# movie7: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440006", +# title: "The Lord of the Rings: The Fellowship of the Ring", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Flotr_fellowship.jpg?alt=media&token=92641d2d-6c52-4172-bd66-95fb86b4b96b", +# releaseYear: 2001, +# genre: "fantasy", +# rating: 8.8, +# description: "A meek Hobbit from the Shire, Frodo Baggins, and eight companions set out on a journey to destroy the powerful One Ring and save Middle-earth from the Dark Lord Sauron. The epic adventure begins the quest that will test their courage and bond.", +# tags: ["fantasy", "adventure"], +# }) + +# movie8: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440007", +# title: "The Shawshank Redemption", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_shawshanks_redemption.jpg?alt=media&token=f67b5ab2-a435-48b2-8251-5bf866b183e9", +# releaseYear: 1994, +# genre: "drama", +# rating: 9.3, +# description: "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency. The film follows Andy Dufresne (Tim Robbins), a banker sentenced to life in Shawshank State Penitentiary, and his friendship with Red (Morgan Freeman).", +# tags: ["drama", "crime"], +# }) + +# movie10: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440009", +# title: "The Godfather", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_godfather.jpg?alt=media&token=5297fd94-ae87-4995-9de5-3755232bad52", +# releaseYear: 1972, +# genre: "crime", +# rating: 9.2, +# description: "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son. The story follows the powerful Corleone family as they navigate power, loyalty, and betrayal.", +# tags: ["crime", "drama"], +# }) + +# movie11: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440010", +# title: "The Silence of the Lambs", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_silence_of_the_lambs.jpg?alt=media&token=7ca6abeb-b15c-4f5e-9280-5a590e89fe54", +# releaseYear: 1991, +# genre: "thriller", +# rating: 8.6, +# description: "A young F.B.I. cadet must receive the help of an incarcerated and manipulative cannibal killer to help catch another serial killer. Clarice Starling (Jodie Foster) seeks the assistance of Hannibal Lecter (Anthony Hopkins) to understand the mind of a killer.", +# tags: ["thriller", "crime"], +# }) + +# movie12: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440011", +# title: "Saving Private Ryan", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fsaving_private_ryan.jpg?alt=media&token=58ed877e-7ae0-4e30-9aee-d45c2deb7a00", +# releaseYear: 1998, +# genre: "war", +# rating: 8.6, +# description: "Following the Normandy Landings, a group of U.S. soldiers go behind enemy lines to retrieve a paratrooper whose brothers have been killed in action. The harrowing journey of Captain John H. Miller (Tom Hanks) and his men highlights the brutal reality of war.", +# tags: ["war", "drama"], +# }) + +# movie13: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440012", +# title: "The Avengers", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_avengers.jpg?alt=media&token=3d68ccad-2fa1-48da-a83e-7941e246c9f9", +# releaseYear: 2012, +# genre: "action", +# rating: 8.0, +# description: "Earth's mightiest heroes, including Iron Man, Captain America, Thor, Hulk, Black Widow, and Hawkeye, must come together to stop Loki and his alien army from enslaving humanity. Directed by Joss Whedon, the film is known for its witty dialogue, intense action sequences, and the chemistry among its ensemble cast.", +# tags: ["action", "sci-fi"], +# }) + +# movie14: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440013", +# title: "Gladiator", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fgladiator.jpg?alt=media&token=61d75825-b79f-4add-afdb-7da5eed53407", +# releaseYear: 2000, +# genre: "action", +# rating: 8.5, +# description: "A former Roman General, Maximus Decimus Meridius, seeks vengeance against the corrupt emperor Commodus who murdered his family and sent him into slavery. Directed by Ridley Scott, the film is known for its epic scale, intense battle scenes, and Russell Crowe's powerful performance.", +# tags: ["action", "drama"], +# }) + +# movie15: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440014", +# title: "Titanic", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Ftitanic.png?alt=media&token=dd03dc83-486e-4b03-9b03-2f9ed83fd9d0", +# releaseYear: 1997, +# genre: "romance", +# rating: 7.8, +# description: "A romantic drama recounting the ill-fated voyage of the R.M.S. Titanic through the eyes of Jack Dawson, a poor artist, and Rose DeWitt Bukater, a wealthy aristocrat. Their forbidden romance unfolds aboard the luxurious ship, which tragically sinks after striking an iceberg. Directed by James Cameron, the film is known for its epic scale, emotional depth, and stunning visuals.", +# tags: ["romance", "drama"], +# }) + +# movie16: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440015", +# title: "Avatar", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Favatar.jpg?alt=media&token=1c75b09d-7c7a-44bf-b7ad-e7da4d0b7193", +# releaseYear: 2009, +# genre: "sci-fi", +# rating: 7.8, +# description: "A paraplegic Marine named Jake Sully is sent on a unique mission to Pandora, an alien world, to bridge relations with the native Na'vi people. Torn between following his orders and protecting the world he feels is his home, Jake's journey becomes a battle for survival. Directed by James Cameron, 'Avatar' is renowned for its groundbreaking special effects and immersive 3D experience.", +# tags: ["sci-fi", "adventure"], +# }) + +# movie17: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440016", +# title: "Jurassic Park", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fjurassic_park.jpg?alt=media&token=1731ce71-3384-4435-8a5b-821d4fd286d3", +# releaseYear: 1993, +# genre: "adventure", +# rating: 8.1, +# description: "During a preview tour, a theme park suffers a major power breakdown that allows its cloned dinosaur exhibits to run amok. Directed by Steven Spielberg, 'Jurassic Park' is known for its groundbreaking special effects, thrilling storyline, and the suspenseful chaos unleashed by its prehistoric creatures.", +# tags: ["adventure", "sci-fi"], +# }) + +# movie18: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440017", +# title: "The Lion King", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fthe_lion_king.jpg?alt=media&token=3e4e4265-6ae7-47d6-a5ba-584de126ef00", +# releaseYear: 1994, +# genre: "animation", +# rating: 8.5, +# description: "A young lion prince, Simba, must overcome betrayal and tragedy to reclaim his rightful place as king. 'The Lion King' is a beloved animated musical known for its memorable songs, stunning animation, and a timeless tale of courage, loyalty, and redemption.", +# tags: ["animation", "drama"], +# }) + +# movie19: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440018", +# title: "Star Wars: Episode IV - A New Hope", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fstar_wars_4.jpg?alt=media&token=b4ea7e0c-707f-43dd-8633-9d962e77b5a4", +# releaseYear: 1977, +# genre: "sci-fi", +# rating: 8.6, +# description: "Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a Wookiee, and two droids to save the galaxy from the Empire's world-destroying battle station, the Death Star. Directed by George Lucas, 'A New Hope' revolutionized the sci-fi genre with its groundbreaking special effects and unforgettable characters.", +# tags: ["sci-fi", "adventure"], +# }) + +# movie20: movie_insert(data: { +# id: "550e8400-e29b-41d4-a716-446655440019", +# title: "Blade Runner", +# imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-quickstart-web.appspot.com/o/movies%2Fblade_runner.jpg?alt=media&token=d8e94bdd-1477-49f3-b244-dd7a9c059fc1", +# releaseYear: 1982, +# genre: "sci-fi", +# rating: 8.1, +# description: "In a dystopian future, synthetic humans known as replicants are created by powerful corporations. A blade runner named Rick Deckard is tasked with hunting down and 'retiring' four replicants who have escaped to Earth. Directed by Ridley Scott, 'Blade Runner' is a seminal sci-fi thriller that explores themes of humanity and identity.", +# tags: ["sci-fi", "thriller"], +# }) +# diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql new file mode 100644 index 000000000000..a1d3e11e2137 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql @@ -0,0 +1,121 @@ +# Create a movie based on user input +mutation addPerson($name: String) @auth(level: PUBLIC) { + person_insert(data: { + name: $name + }) +} +mutation addDirectorToMovie($personId: Person_Key, $movieId: UUID) { + directedBy_insert(data: { + directedby: $personId, + movieId: $movieId + }) +} +mutation createMovie( + $title: String! + $releaseYear: Int! + $genre: String! + $rating: Float + $description: String +) @auth(level: PUBLIC) { + movie_insert( + data: { + title: $title + releaseYear: $releaseYear + genre: $genre + rating: $rating + description: $description + + } + ) +} + +# # Update movie information based on the provided ID +# mutation updateMovie( +# $id: UUID! +# $title: String +# $releaseYear: Int +# $genre: String +# $rating: Float +# $description: String +# $imageUrl: String +# $tags: [String!] = [] +# ) { +# movie_update( +# id: $id +# data: { +# title: $title +# releaseYear: $releaseYear +# genre: $genre +# rating: $rating +# description: $description +# imageUrl: $imageUrl +# tags: $tags +# } +# ) +# } + +# # Delete a movie by its ID +# mutation deleteMovie($id: UUID!) { +# movie_delete(id: $id) +# } + +# # Delete movies with a rating lower than the specified minimum rating +# mutation deleteUnpopularMovies($minRating: Float!) { +# movie_deleteMany(where: { rating: { le: $minRating } }) +# } + +# # Add a movie to the user's watched list +# mutation addWatchedMovie($movieId: UUID!) @auth(level: USER) { +# watchedMovie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId }) +# } + +# # Remove a movie from the user's watched list +# mutation deleteWatchedMovie($userId: String!, $movieId: UUID!) @auth(level: USER) { +# watchedMovie_delete(key: { userId: $userId, movieId: $movieId }) +# } + +# # Add a movie to the user's favorites list +# mutation addFavoritedMovie($movieId: UUID!) @auth(level: USER) { +# favoriteMovie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId }) +# } + +# # Remove a movie from the user's favorites list +# mutation deleteFavoritedMovie($userId: String!, $movieId: UUID!) @auth(level: USER) { +# favoriteMovie_delete(key: { userId: $userId, movieId: $movieId }) +# } + +# # Add an actor to the user's favorites list +# mutation addFavoritedActor($actorId: UUID!) @auth(level: USER) { +# favoriteActor_upsert(data: { userId_expr: "auth.uid", actorId: $actorId }) +# } + +# # Remove an actor from the user's favorites list +# mutation deleteFavoriteActor($userId: String!, $actorId: UUID!) @auth(level: USER) { +# favoriteActor_delete(key: { userId: $userId, actorId: $actorId }) +# } + +# # Add a review for a movie +# mutation addReview($movieId: UUID!, $rating: Int!, $reviewText: String!) @auth(level: USER) { +# review_upsert( +# data: { +# userId_expr: "auth.uid" +# movieId: $movieId +# rating: $rating +# reviewText: $reviewText +# reviewDate_date: { today: true } +# } +# ) +# } + +# # Delete a user's review for a movie +# mutation deleteReview($movieId: UUID!, $userId: String!) @auth(level: USER) { +# review_delete(key: { userId: $userId, movieId: $movieId }) +# } + +# # Upsert (update or insert) a user based on their username +# mutation upsertUser($username: String!) @auth(level: USER) { +# user_upsert(data: { +# id_expr: "auth.uid", +# username: $username +# }) +# } \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql new file mode 100644 index 000000000000..d3f265b1a2d9 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql @@ -0,0 +1,574 @@ + +# List subset of fields for movies +query ListMovies @auth(level: USER) { + movies { + id + title, + directed_by: people_via_DirectedBy { + name, + } + }, +} + + +# List subset of fields for users +# query ListUsers @auth(level: PUBLIC) { +# users { +# id +# username +# favoriteActors: favoriteActors_on_user { +# actor { +# id +# name +# imageUrl +# } +# } +# favoriteMovies: favoriteMovies_on_user { +# movie { +# id +# title +# genre +# imageUrl +# tags +# } +# } +# reviews_on_user { +# id +# rating +# reviewText +# reviewDate +# movie { +# id +# title +# } +# } +# watchedMovies_on_user { +# movie { +# id +# title +# genre +# imageUrl +# } +# } +# } +# } + +# List movies of a certain genre +# query ListMoviesByGenre($genre: String!) @auth(level: PUBLIC) { +# mostPopular: movies( +# where: { genre: { eq: $genre } } +# orderBy: { rating: DESC } +# ) { +# id +# title +# imageUrl +# rating +# tags +# } +# mostRecent: movies( +# where: { genre: { eq: $genre } } +# orderBy: { releaseYear: DESC } +# ) { +# id +# title +# imageUrl +# rating +# tags +# } +# } + +# # List movies by the order of release +# query ListMoviesByReleaseYear @auth(level: PUBLIC) { +# movies(orderBy: [{ releaseYear: DESC }]) { +# id +# title +# imageUrl +# } +# } + +# # Get movie by id +# query GetMovieById($id: UUID!) @auth(level: PUBLIC) { +# movie(id: $id) { +# id +# title +# imageUrl +# releaseYear +# genre +# rating +# description +# tags +# metadata: movieMetadatas_on_movie { +# director +# } +# mainActors: actors_via_MovieActor(where: { role: { eq: "main" } }) { +# id +# name +# imageUrl +# } +# supportingActors: actors_via_MovieActor( +# where: { role: { eq: "supporting" } } +# ) { +# id +# name +# imageUrl +# } +# sequelTo { +# id +# title +# imageUrl +# } +# reviews: reviews_on_movie { +# id +# reviewText +# reviewDate +# rating +# user { +# id +# username +# } +# } +# } +# } + +# # Get actor by id +# query GetActorById($id: UUID!) @auth(level: PUBLIC) { +# actor(id: $id) { +# id +# name +# imageUrl +# biography +# mainActors: movies_via_MovieActor(where: { role: { eq: "main" } }) { +# id +# title +# genre +# tags +# imageUrl +# } +# supportingActors: movies_via_MovieActor( +# where: { role: { eq: "supporting" } } +# ) { +# id +# title +# genre +# tags +# imageUrl +# } +# } +# } + +# # User movie preferences +# query UserMoviePreferences($username: String!) @auth(level: USER) { +# users(where: { username: { eq: $username } }) { +# likedMovies: movies_via_Review(where: { rating: { ge: 4 } }) { +# title +# imageUrl +# genre +# description +# } +# dislikedMovies: movies_via_Review(where: { rating: { le: 2 } }) { +# title +# imageUrl +# genre +# description +# } +# } +# } + +# # Get movie metadata +# query GetMovieMetadata($id: UUID!) @auth(level: PUBLIC) { +# movie(id: $id) { +# movieMetadatas_on_movie { +# director +# } +# } +# } + +# # Get movie cast and actor roles +# query GetMovieCast($movieId: UUID!, $actorId: UUID!) @auth(level: PUBLIC) { +# movie(id: $movieId) { +# mainActors: actors_via_MovieActor(where: { role: { eq: "main" } }) { +# id +# name +# imageUrl +# } +# supportingActors: actors_via_MovieActor( +# where: { role: { eq: "supporting" } } +# ) { +# id +# name +# imageUrl +# } +# } +# actor(id: $actorId) { +# mainRoles: movies_via_MovieActor(where: { role: { eq: "main" } }) { +# id +# title +# imageUrl +# } +# supportingRoles: movies_via_MovieActor( +# where: { role: { eq: "supporting" } } +# ) { +# id +# title +# imageUrl +# } +# } +# } + +# # List movies by partial title match +# query ListMoviesByPartialTitle($input: String!) @auth(level: PUBLIC) { +# movies(where: { title: { contains: $input } }) { +# id +# title +# genre +# rating +# imageUrl +# } +# } + +# # Fetch a single movie using key scalars (same as get movie by id) +# query MovieByKey($key: Movie_Key!) @auth(level: PUBLIC) { +# movie(key: $key) { +# title +# imageUrl +# } +# } + +# # Fetch movies by title +# query MovieByTitle($title: String!) @auth(level: PUBLIC) { +# movies(where: { title: { eq: $title } }) { +# id +# title +# imageUrl +# genre +# rating +# } +# } + +# # Fetch top-rated movies by genre +# query MovieByTopRating($genre: String) @auth(level: PUBLIC) { +# mostPopular: movies( +# where: { genre: { eq: $genre } } +# orderBy: { rating: DESC } +# ) { +# id +# title +# imageUrl +# rating +# tags +# } +# } + +# # List movies by tag +# query ListMoviesByTag($tag: String!) @auth(level: PUBLIC) { +# movies(where: { tags: { includes: $tag } }) { +# id +# title +# imageUrl +# genre +# rating +# } +# } + +# # List top 10 movies +# query MoviesTop10 @auth(level: PUBLIC) { +# movies(orderBy: [{ rating: DESC }], limit: 10) { +# id +# title +# imageUrl +# rating +# genre +# tags +# metadata: movieMetadatas_on_movie { +# director +# } +# mainActors: actors_via_MovieActor(where: { role: { eq: "main" } }) { +# id +# name +# imageUrl +# } +# supportingActors: actors_via_MovieActor(where: { role: { eq: "supporting" } }) { +# id +# name +# imageUrl +# } +# } +# } + +# # List movies by release year range +# query MoviesByReleaseYear($min: Int, $max: Int) @auth(level: PUBLIC) { +# movies( +# where: { releaseYear: { le: $max, ge: $min } } +# orderBy: [{ releaseYear: ASC }] +# ) { +# id +# rating +# title +# imageUrl +# } +# } + +# # List recently released movies +# query MoviesRecentlyReleased @auth(level: PUBLIC) { +# movies(where: { releaseYear: { ge: 2010 } }) { +# id +# title +# rating +# imageUrl +# genre +# tags +# } +# } + +# # List movies with filtering on fields +# query ListMoviesFilter($genre: String, $limit: Int) @auth(level: PUBLIC) { +# movies(where: { genre: { eq: $genre } }, limit: $limit) { +# title +# imageUrl +# } +# } + +# # List movies by partial title string match +# query ListMoviesByTitleString( +# $prefix: String +# $suffix: String +# $contained: String +# ) @auth(level: PUBLIC) { +# prefixed: movies(where: { description: { startsWith: $prefix } }) { +# title +# } +# suffixed: movies(where: { description: { endsWith: $suffix } }) { +# title +# } +# contained: movies(where: { description: { contains: $contained } }) { +# title +# } +# } + +# # List movies by rating and genre with OR/AND filters +# query ListMoviesByRatingAndGenre($minRating: Float!, $genre: String) +# @auth(level: PUBLIC) { +# movies( +# where: { _or: [{ rating: { ge: $minRating } }, { genre: { eq: $genre } }] } +# ) { +# title +# imageUrl +# } +# } + +# # Get favorite movies by user ID +# query GetFavoriteMoviesById($id: String!) @auth(level: USER) { +# user(id: $id) { +# favoriteMovies_on_user { +# movie { +# id +# title +# genre +# imageUrl +# releaseYear +# rating +# description +# } +# } +# } +# } + +# # Get favorite actors by user ID +# query GetFavoriteActorsById($id: String!) @auth(level: USER) { +# user(id: $id) { +# favoriteActors_on_user { +# actor { +# id +# name +# imageUrl +# } +# } +# } +# } + +# # Get watched movies by user ID +# query GetWatchedMoviesByAuthId($id: String!) @auth(level: USER) { +# user(id: $id) { +# watchedMovies_on_user { +# movie { +# id +# title +# genre +# imageUrl +# releaseYear +# rating +# description +# } +# } +# } +# } + +# # Get user by ID +# query GetUserById($id: String!) @auth(level: USER) { +# user(id: $id) { +# id +# username +# reviews: reviews_on_user { +# id +# rating +# reviewDate +# reviewText +# movie { +# id +# title +# } +# } +# watched: watchedMovies_on_user { +# movie { +# id +# title +# genre +# imageUrl +# releaseYear +# rating +# description +# tags +# metadata: movieMetadatas_on_movie { +# director +# } +# } +# } +# favoriteMovies: favoriteMovies_on_user { +# movie { +# id +# title +# genre +# imageUrl +# releaseYear +# rating +# description +# tags +# metadata: movieMetadatas_on_movie { +# director +# } +# } +# } +# favoriteActors: favoriteActors_on_user { +# actor { +# id +# name +# imageUrl +# } +# } +# } +# } + +# # Check if a movie is watched by user +# query GetIfWatched($id: String!, $movieId: UUID!) @auth(level: USER) { +# watchedMovie(key: { userId: $id, movieId: $movieId }) { +# movieId +# } +# } + +# # Check if a movie is favorited by user +# query GetIfFavoritedMovie($id: String!, $movieId: UUID!) @auth(level: USER) { +# favoriteMovie(key: { userId: $id, movieId: $movieId }) { +# movieId +# } +# } + +# # Check if an actor is favorited by user +# query GetIfFavoritedActor($id: String!, $actorId: UUID!) @auth(level: USER) { +# favoriteActor(key: { userId: $id, actorId: $actorId }) { +# actorId +# } +# } + +# # Fuzzy search for movies, actors, and reviews +# query fuzzySearch( +# $input: String +# $minYear: Int! +# $maxYear: Int! +# $minRating: Float! +# $maxRating: Float! +# $genre: String! +# ) @auth(level: PUBLIC) { +# moviesMatchingTitle: movies( +# where: { +# _and: [ +# { releaseYear: { ge: $minYear } } +# { releaseYear: { le: $maxYear } } +# { rating: { ge: $minRating } } +# { rating: { le: $maxRating } } +# { genre: { contains: $genre } } +# { title: { contains: $input } } +# ] +# } +# ) { +# id +# title +# genre +# rating +# imageUrl +# } +# moviesMatchingDescription: movies( +# where: { +# _and: [ +# { releaseYear: { ge: $minYear } } +# { releaseYear: { le: $maxYear } } +# { rating: { ge: $minRating } } +# { rating: { le: $maxRating } } +# { genre: { contains: $genre } } +# { description: { contains: $input } } +# ] +# } +# ) { +# id +# title +# genre +# rating +# imageUrl +# } +# actorsMatchingName: actors(where: { name: { contains: $input } }) { +# id +# name +# imageUrl +# } +# reviewsMatchingText: reviews(where: { reviewText: { contains: $input } }) { +# id +# rating +# reviewText +# reviewDate +# movie { +# id +# title +# } +# user { +# id +# username +# } +# } +# } + +# Search movie descriptions using L2 similarity with Vertex AI +# query searchMovieDescriptionUsingL2Similarity($query: String!) +# @auth(level: PUBLIC) { +# movies_descriptionEmbedding_similarity( +# compare_embed: { model: "textembedding-gecko@003", text: $query } +# method: L2 +# within: 2 +# where: { description: { ne: "" } } +# limit: 5 +# ) { +# id +# title +# description +# tags +# rating +# imageUrl +# } +# } + +# # Search movie descriptions using L2 similarity with Vertex AI, with custom embeddings +# query searchMovieDescriptionUsingL2Similarity1($compare: Vector!, $within: Float, $excludesContent: String, $limit: Int) @auth(level: PUBLIC) { +# movies_descriptionEmbedding_similarity(compare: $compare, method: L2, within: $within, where: {description: {ne: $excludesContent}}, limit: $limit) { +# id +# title +# description +# } +# } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/dataconnect.yaml b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/dataconnect.yaml new file mode 100644 index 000000000000..06513791c29f --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/dataconnect.yaml @@ -0,0 +1,11 @@ +specVersion: "v1alpha" +serviceId: "dataconnect" +location: "us-west2" +schema: + source: "./schema" + datasource: + postgresql: + database: "dataconnect" + cloudSql: + instanceId: "fdc-test-north1" +connectorDirs: ["./connector"] diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/schema/schema.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/schema/schema.gql new file mode 100644 index 000000000000..605bb1a624d7 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/schema/schema.gql @@ -0,0 +1,114 @@ +type Person @table { + name: String! +} +type DirectedBy @table(key: ["movie", "directedby"]) { + directedby: Person! + movie: Movie! +} +type Movie @table { + title: String! + description: String + genre: String! + releaseYear: Int + rating: Float +} +# type Movie +# # The below parameter values are generated by default with @table, and can be edited manually. +# @table(name: "Movies", singular: "movie", plural: "movies", key: ["id"]) { +# # implicitly calls @col to generates a column name. ex: @col(name: "movie_id") +# # for UUID fields, @default(expr: "uuidV4()") is implicitly called +# id: UUID! +# title: String! +# imageUrl: String! +# releaseYear: Int +# genre: String +# rating: Float +# description: String +# tags: [String] +# # Vectors +# descriptionEmbedding: Vector @col(size:768) # vector +# # Self Joins +# sequelTo: Movie +# } + +# # Movie Metadata +# # Movie - MovieMetadata is a one-to-one relationship +# type MovieMetadata +# @table { +# # @ref creates a field in the current table (MovieMetadata) that holds the primary key of the referenced type +# # In this case, @ref(fields: "id") is implied +# movie: Movie! @ref +# # movieId: UUID <- this is created by the above @ref +# director: String +# # TODO: optional other fields +# } + +# # Actors +# # Suppose an actor can participate in multiple movies and movies can have multiple actors +# # Movie - Actors (or vice versa) is a many to many relationship +# type Actor @table { +# id: UUID! +# imageUrl: String! +# name: String! @col(name: "name", dataType: "varchar(30)") +# biography: String +# } + +# # Join table for many-to-many relationship for movies and actors +# # The 'key' param signifies the primary key(s) of this table +# # In this case, the keys are [movieId, actorId], the generated fields of the reference types [movie, actor] +# type MovieActor @table(key: ["movie", "actor"]) { +# # @ref creates a field in the current table (MovieActor) that holds the primary key of the referenced type +# # In this case, @ref(fields: "id") is implied +# movie: Movie! +# # movieId: UUID! <- this is created by the implied @ref, see: implicit.gql + +# actor: Actor! +# # actorId: UUID! <- this is created by the implied @ref, see: implicit.gql + +# role: String! # "main" or "supporting" +# # TODO: optional other fields +# } + +# # Users +# # Suppose a user can leave reviews for movies +# # user:reviews is a one to many relationship, movie:reviews is a one to many relationship, movie:user is a many to many relationship +# type User +# @table { +# id: String! @col(name: "user_auth") +# username: String! @col(name: "username", dataType: "varchar(50)") +# # The following are generated from the @ref in the Review table +# # reviews_on_user +# # movies_via_Review +# } + +# # Join table for many-to-many relationship for users and favorite movies +# type FavoriteMovie +# @table(name: "FavoriteMovies", key: ["user", "movie"]) { +# # @ref is implicit +# user: User! +# movie: Movie! +# } + +# # Join table for many-to-many relationship for users and favorite actors +# type FavoriteActor +# @table(name: "FavoriteActors", key: ["user", "actor"]) { +# user: User! +# actor: Actor! +# } + +# # Join table for many-to-many relationship for users and watched movies +# type WatchedMovie +# @table(name: "WatchedMovies", key: ["user", "movie"]) { +# user: User! +# movie: Movie! +# } + +# # Reviews +# type Review @table(name: "Reviews", key: ["movie", "user"]) { +# id: UUID! @default(expr: "uuidV4()") +# user: User! +# movie: Movie! +# rating: Int +# reviewText: String +# reviewDate: Date! @default(expr: "request.time") +# } \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/example/firebase.json b/packages/firebase_data_connect/firebase_data_connect/example/firebase.json new file mode 100644 index 000000000000..73f599717a9c --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/firebase.json @@ -0,0 +1,5 @@ +{ + "dataconnect": { + "source": "dataconnect" + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/flutter_01.png b/packages/firebase_data_connect/firebase_data_connect/example/flutter_01.png new file mode 100644 index 0000000000000000000000000000000000000000..5a2b64dcabc8f05d34d7ec930fd6706d48d18cea GIT binary patch literal 78324 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 literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/.gitignore b/packages/firebase_data_connect/firebase_data_connect/example/ios/.gitignore new file mode 100644 index 000000000000..7a7f9873ad7d --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Flutter/AppFrameworkInfo.plist b/packages/firebase_data_connect/firebase_data_connect/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..7c5696400627 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Flutter/Debug.xcconfig b/packages/firebase_data_connect/firebase_data_connect/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..ec97fc6f3021 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Flutter/Release.xcconfig b/packages/firebase_data_connect/firebase_data_connect/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..c4855bfe2000 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Podfile b/packages/firebase_data_connect/firebase_data_connect/example/ios/Podfile new file mode 100644 index 000000000000..3e44f9c6f789 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..fa521e1c50ab --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,749 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 44748ABD01BE3A9752CF3BFC /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4B076066C0A79965E90ABC43 /* GoogleService-Info.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B5F9716836A88E3577494FC0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CA1C958D561F10EF905ECE34 /* Pods_Runner.framework */; }; + D1F8DC9D90F1D5D2C3B3DB37 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B36940891A4846D9D3EB356F /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0D0A947B6A3F0BBD444EBA70 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 467C8E0E18669B1A171B55D8 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 4B076066C0A79965E90ABC43 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + 5363A146E4B954495368B56D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 779CFABD8818448B3B887A9C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 84542CEC9A0FBFF9A0A1DB37 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B36940891A4846D9D3EB356F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CA1C958D561F10EF905ECE34 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EF7D1184723DAE45AE37C68F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 87AC0902B538640350365D1E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D1F8DC9D90F1D5D2C3B3DB37 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B5F9716836A88E3577494FC0 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 4B076066C0A79965E90ABC43 /* GoogleService-Info.plist */, + EE2E4CC9AAC39360CE2CCED5 /* Pods */, + BDBC9B622C75229E8F7832D1 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + BDBC9B622C75229E8F7832D1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + CA1C958D561F10EF905ECE34 /* Pods_Runner.framework */, + B36940891A4846D9D3EB356F /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + EE2E4CC9AAC39360CE2CCED5 /* Pods */ = { + isa = PBXGroup; + children = ( + 5363A146E4B954495368B56D /* Pods-Runner.debug.xcconfig */, + 0D0A947B6A3F0BBD444EBA70 /* Pods-Runner.release.xcconfig */, + 84542CEC9A0FBFF9A0A1DB37 /* Pods-Runner.profile.xcconfig */, + 779CFABD8818448B3B887A9C /* Pods-RunnerTests.debug.xcconfig */, + EF7D1184723DAE45AE37C68F /* Pods-RunnerTests.release.xcconfig */, + 467C8E0E18669B1A171B55D8 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 0CB7FA20986D825D585CDAFF /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 87AC0902B538640350365D1E /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 3BA940C983F4E64846EDB4DB /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 07279BCD11CB9067B11BE810 /* [CP] Embed Pods Frameworks */, + 322723C62D13E9C2D8F6F2DC /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 44748ABD01BE3A9752CF3BFC /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 07279BCD11CB9067B11BE810 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 0CB7FA20986D825D585CDAFF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 322723C62D13E9C2D8F6F2DC /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 3BA940C983F4E64846EDB4DB /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.dataconnect.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 779CFABD8818448B3B887A9C /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.dataconnect.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EF7D1184723DAE45AE37C68F /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.dataconnect.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 467C8E0E18669B1A171B55D8 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.dataconnect.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.dataconnect.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.dataconnect.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..8e3ca5dfe193 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/AppDelegate.swift b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000000..626664468b89 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 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 literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Base.lproj/Main.storyboard b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Info.plist b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..fcfd18a5b8a3 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Info.plist @@ -0,0 +1,62 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.googleusercontent.apps.1033042013828-dakdhgkbr6dtt3att3j9da51dlric008 + + + + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSApplicationCategoryType + + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Runner-Bridging-Header.h b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000000..308a2a560b42 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/RunnerTests/RunnerTests.swift b/packages/firebase_data_connect/firebase_data_connect/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000000..20799f286423 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,14 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_director_to_movie.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_director_to_movie.dart new file mode 100644 index 000000000000..31f9e0f435ee --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_director_to_movie.dart @@ -0,0 +1,136 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of movies; + +class AddDirectorToMovie { + String name = "addDirectorToMovie"; + AddDirectorToMovie({required this.dataConnect}); + + Deserializer dataDeserializer = (String json) => + AddDirectorToMovieResponse.fromJson( + jsonDecode(json) as Map); + Serializer varsSerializer = + (AddDirectorToMovieVariables vars) => jsonEncode(vars.toJson()); + MutationRef ref( + {AddDirectorToMovieVariablesPersonId? personId, + String? movieId, + AddDirectorToMovieVariables? addDirectorToMovieVariables}) { + AddDirectorToMovieVariables vars1 = AddDirectorToMovieVariables( + personId: personId, + movieId: movieId, + ); + AddDirectorToMovieVariables vars = addDirectorToMovieVariables ?? vars1; + return dataConnect.mutation( + this.name, dataDeserializer, varsSerializer, vars); + } + + FirebaseDataConnect dataConnect; +} + +class AddDirectorToMovieDirectedByInsert { + late String directedbyId; + + late String movieId; + + AddDirectorToMovieDirectedByInsert.fromJson(Map json) + : directedbyId = json['directedbyId'], + movieId = json['movieId'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['directedbyId'] = directedbyId; + + json['movieId'] = movieId; + + return json; + } + + AddDirectorToMovieDirectedByInsert({ + required this.directedbyId, + required this.movieId, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class AddDirectorToMovieResponse { + late AddDirectorToMovieDirectedByInsert directedBy_insert; + + AddDirectorToMovieResponse.fromJson(Map json) + : directedBy_insert = AddDirectorToMovieDirectedByInsert.fromJson( + json['directedBy_insert']) {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['directedBy_insert'] = directedBy_insert.toJson(); + + return json; + } + + AddDirectorToMovieResponse({ + required this.directedBy_insert, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class AddDirectorToMovieVariablesPersonId { + late String id; + + AddDirectorToMovieVariablesPersonId.fromJson(Map json) + : id = json['id'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['id'] = id; + + return json; + } + + AddDirectorToMovieVariablesPersonId({ + required this.id, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class AddDirectorToMovieVariables { + late AddDirectorToMovieVariablesPersonId? personId; + + late String? movieId; + + AddDirectorToMovieVariables.fromJson(Map json) + : personId = + AddDirectorToMovieVariablesPersonId.fromJson(json['personId']), + movieId = json['movieId'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + if (personId != null) { + json['personId'] = personId!.toJson(); + } + + if (movieId != null) { + json['movieId'] = movieId; + } + + return json; + } + + AddDirectorToMovieVariables({ + this.personId, + this.movieId, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_person.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_person.dart new file mode 100644 index 000000000000..f71192de4435 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_person.dart @@ -0,0 +1,93 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of movies; + +class AddPerson { + String name = "addPerson"; + AddPerson({required this.dataConnect}); + + Deserializer dataDeserializer = (String json) => + AddPersonResponse.fromJson(jsonDecode(json) as Map); + Serializer varsSerializer = + (AddPersonVariables vars) => jsonEncode(vars.toJson()); + MutationRef ref( + {String? name, AddPersonVariables? addPersonVariables}) { + AddPersonVariables vars1 = AddPersonVariables( + name: name, + ); + AddPersonVariables vars = addPersonVariables ?? vars1; + return dataConnect.mutation( + this.name, dataDeserializer, varsSerializer, vars); + } + + FirebaseDataConnect dataConnect; +} + +class AddPersonPersonInsert { + late String id; + + AddPersonPersonInsert.fromJson(Map json) : id = json['id'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['id'] = id; + + return json; + } + + AddPersonPersonInsert({ + required this.id, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class AddPersonResponse { + late AddPersonPersonInsert person_insert; + + AddPersonResponse.fromJson(Map json) + : person_insert = AddPersonPersonInsert.fromJson(json['person_insert']) {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['person_insert'] = person_insert.toJson(); + + return json; + } + + AddPersonResponse({ + required this.person_insert, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class AddPersonVariables { + late String? name; + + AddPersonVariables.fromJson(Map json) + : name = json['name'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + if (name != null) { + json['name'] = name; + } + + return json; + } + + AddPersonVariables({ + this.name, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/create_movie.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/create_movie.dart new file mode 100644 index 000000000000..481a519b686f --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/create_movie.dart @@ -0,0 +1,129 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of movies; + +class CreateMovie { + String name = "createMovie"; + CreateMovie({required this.dataConnect}); + + Deserializer dataDeserializer = (String json) => + CreateMovieResponse.fromJson(jsonDecode(json) as Map); + Serializer varsSerializer = + (CreateMovieVariables vars) => jsonEncode(vars.toJson()); + MutationRef ref( + {required String title, + required int releaseYear, + required String genre, + double? rating, + String? description, + CreateMovieVariables? createMovieVariables}) { + CreateMovieVariables vars1 = CreateMovieVariables( + title: title, + releaseYear: releaseYear, + genre: genre, + rating: rating, + description: description, + ); + CreateMovieVariables vars = createMovieVariables ?? vars1; + return dataConnect.mutation( + this.name, dataDeserializer, varsSerializer, vars); + } + + FirebaseDataConnect dataConnect; +} + +class CreateMovieMovieInsert { + late String id; + + CreateMovieMovieInsert.fromJson(Map json) + : id = json['id'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['id'] = id; + + return json; + } + + CreateMovieMovieInsert({ + required this.id, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class CreateMovieResponse { + late CreateMovieMovieInsert movie_insert; + + CreateMovieResponse.fromJson(Map json) + : movie_insert = CreateMovieMovieInsert.fromJson(json['movie_insert']) {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['movie_insert'] = movie_insert.toJson(); + + return json; + } + + CreateMovieResponse({ + required this.movie_insert, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class CreateMovieVariables { + late String title; + + late int releaseYear; + + late String genre; + + late double? rating; + + late String? description; + + CreateMovieVariables.fromJson(Map json) + : title = json['title'], + releaseYear = json['releaseYear'], + genre = json['genre'], + rating = json['rating'], + description = json['description'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['title'] = title; + + json['releaseYear'] = releaseYear; + + json['genre'] = genre; + + if (rating != null) { + json['rating'] = rating; + } + + if (description != null) { + json['description'] = description; + } + + return json; + } + + CreateMovieVariables({ + required this.title, + required this.releaseYear, + required this.genre, + this.rating, + this.description, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies.dart new file mode 100644 index 000000000000..e3b2086b8478 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies.dart @@ -0,0 +1,102 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of movies; + +class ListMovies { + String name = "ListMovies"; + ListMovies({required this.dataConnect}); + + Deserializer dataDeserializer = (String json) => + ListMoviesResponse.fromJson(jsonDecode(json) as Map); + + QueryRef ref() { + return dataConnect.query( + this.name, dataDeserializer, emptySerializer, null); + } + + FirebaseDataConnect dataConnect; +} + +class ListMoviesMovies { + late String id; + + late String title; + + late List directed_by; + + ListMoviesMovies.fromJson(Map json) + : id = json['id'], + title = json['title'], + directed_by = (json['directed_by'] as List) + .map((e) => ListMoviesMoviesDirectedBy.fromJson(e)) + .toList() {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['id'] = id; + + json['title'] = title; + + json['directed_by'] = directed_by.map((e) => e.toJson()).toList(); + + return json; + } + + ListMoviesMovies({ + required this.id, + required this.title, + required this.directed_by, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class ListMoviesMoviesDirectedBy { + late String name; + + ListMoviesMoviesDirectedBy.fromJson(Map json) + : name = json['name'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['name'] = name; + + return json; + } + + ListMoviesMoviesDirectedBy({ + required this.name, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class ListMoviesResponse { + late List movies; + + ListMoviesResponse.fromJson(Map json) + : movies = (json['movies'] as List) + .map((e) => ListMoviesMovies.fromJson(e)) + .toList() {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['movies'] = movies.map((e) => e.toJson()).toList(); + + return json; + } + + ListMoviesResponse({ + required this.movies, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart new file mode 100644 index 000000000000..bc93d11d5910 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart @@ -0,0 +1,49 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library movies; + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'dart:convert'; + +part 'add_person.dart'; + +part 'add_director_to_movie.dart'; + +part 'create_movie.dart'; + +part 'list_movies.dart'; + +class MoviesConnector { + AddPerson get addPerson { + return AddPerson(dataConnect: dataConnect); + } + + AddDirectorToMovie get addDirectorToMovie { + return AddDirectorToMovie(dataConnect: dataConnect); + } + + CreateMovie get createMovie { + return CreateMovie(dataConnect: dataConnect); + } + + ListMovies get listMovies { + return ListMovies(dataConnect: dataConnect); + } + + static ConnectorConfig connectorConfig = ConnectorConfig( + 'us-west2', + 'movies', + 'dataconnect', + ); + + MoviesConnector({required this.dataConnect}); + static MoviesConnector get instance { + return MoviesConnector( + dataConnect: + FirebaseDataConnect.instanceFor(connectorConfig: connectorConfig)); + } + + FirebaseDataConnect dataConnect; +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/login.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/login.dart new file mode 100644 index 000000000000..f88ae430deaf --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/login.dart @@ -0,0 +1,85 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_data_connect_example/main.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +class Login extends StatefulWidget { + const Login({super.key}); + + @override + State createState() => _LoginState(); +} + +class _LoginState extends State { + Future signInWithGoogle() async { + // Trigger the authentication flow + if (kIsWeb) { + GoogleAuthProvider googleProvider = GoogleAuthProvider(); + + googleProvider + .addScope('https://www.googleapis.com/auth/contacts.readonly'); + + // Once signed in, return the UserCredential + return await FirebaseAuth.instance.signInWithPopup(googleProvider); + } else { + final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn(); + + // Obtain the auth details from the request + final GoogleSignInAuthentication? googleAuth = + await googleUser?.authentication; + + // Create a new credential + final credential = GoogleAuthProvider.credential( + accessToken: googleAuth?.accessToken, + idToken: googleAuth?.idToken, + ); + + // Once signed in, return the UserCredential + return await FirebaseAuth.instance.signInWithCredential(credential); + } + } + + void logIn() async { + final navigator = Navigator.of(context); + await signInWithGoogle(); + + navigator.push( + MaterialPageRoute( + builder: (context) => const MyHomePage( + title: "Data Connect Home Page", + )), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text("Login"), + ), + body: Center( + child: Container( + height: 150.0, + width: 190.0, + padding: const EdgeInsets.only(top: 40), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(200), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: TextButton( + onPressed: logIn, + child: const Text("Log in"), + ), + ), + ), + ), + ); + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/main.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/main.dart new file mode 100644 index 000000000000..e44256df786c --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/main.dart @@ -0,0 +1,260 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_data_connect_example/login.dart'; + +import 'package:flutter/material.dart'; +// Uncomment this line after running flutterfire configure +// import 'firebase_options.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; + +import 'generated/movies.dart'; + +const appCheckEnabled = false; +const configureEmulator = false; + +// Required for web. Set equal to `DefaultFirebaseOptions.currentPlatform` +FirebaseOptions? options; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: options); + if (appCheckEnabled) { + await FirebaseAppCheck.instance.activate( + // You can also use a `ReCaptchaEnterpriseProvider` provider instance as an + // argument for `webProvider` + webProvider: ReCaptchaV3Provider('your-site-key'), + // Default provider for Android is the Play Integrity provider. You can use the "AndroidProvider" enum to choose + // your preferred provider. Choose from: + // 1. Debug provider + // 2. Safety Net provider + // 3. Play Integrity provider + androidProvider: AndroidProvider.debug, + // Default provider for iOS/macOS is the Device Check provider. You can use the "AppleProvider" enum to choose + // your preferred provider. Choose from: + // 1. Debug provider + // 2. Device Check provider + // 3. App Attest provider + // 4. App Attest provider with fallback to Device Check provider (App Attest provider is only available on iOS 14.0+, macOS 14.0+) + appleProvider: AppleProvider.appAttest, + ); + } + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter DataConnect Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, + ), + home: const Login(), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class DataConnectWidget extends StatefulWidget { + const DataConnectWidget({super.key}); + @override + State createState() => _DataConnectWidgetState(); +} + +class _DataConnectWidgetState extends State { + final TextEditingController _genreController = TextEditingController(); + final TextEditingController _titleController = TextEditingController(); + DateTime _releaseYearDate = DateTime(1920); + List _movies = []; + double _rating = 0; + + Future triggerReload() async { + QueryRef ref = MoviesConnector.instance.listMovies.ref(); + ref.execute(); + } + + @override + void initState() { + super.initState(); + if (configureEmulator) { + int port = 9399; + MoviesConnector.instance.dataConnect + .useDataConnectEmulator('127.0.0.1', port); + } + + QueryRef ref = + MoviesConnector.instance.listMovies.ref(); + ref.subscribe().listen((event) { + setState(() { + _movies = event.data.movies; + }); + }).onError((e) { + _showError("Got an error: $e"); + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Flex(direction: Axis.vertical, children: [ + Flexible( + flex: 1, + child: TextFormField( + decoration: const InputDecoration( + border: UnderlineInputBorder(), + labelText: 'Name', + ), + controller: _titleController, + ), + ), + Flexible( + flex: 1, + child: TextFormField( + decoration: const InputDecoration( + border: UnderlineInputBorder(), + labelText: 'Genre', + ), + controller: _genreController, + )), + Flexible( + flex: 1, + child: RatingBar.builder( + initialRating: 3, + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemPadding: const EdgeInsets.symmetric(horizontal: 4.0), + itemBuilder: (context, _) => const Icon( + Icons.star, + color: Colors.amber, + ), + onRatingUpdate: (rating) { + _rating = rating; + }, + )), + Flexible( + flex: 1, + child: YearPicker( + firstDate: DateTime(1990), + lastDate: DateTime.now(), + selectedDate: _releaseYearDate, + onChanged: (value) { + setState(() { + _releaseYearDate = value; + }); + }, + )), + TextButton( + style: ButtonStyle( + foregroundColor: WidgetStateProperty.all(Colors.blue), + ), + onPressed: () async { + String title = _titleController.text; + String genre = _genreController.text; + if (title == '' || genre == '') { + return; + } + + MutationRef ref = MoviesConnector.instance.createMovie.ref( + title: title, + releaseYear: _releaseYearDate.year, + genre: genre, + rating: _rating); + try { + await ref.execute(); + triggerReload(); + } catch (e) { + _showError("unable to create a movie: $e"); + } + }, + child: const Text('Add Movie'), + ), + const Center( + child: Text( + "Movies", + style: TextStyle(fontSize: 35.0), + ), + ), + Expanded( + child: Column( + children: [ + Expanded( + child: RefreshIndicator( + onRefresh: () => triggerReload(), + child: ListView( + scrollDirection: Axis.vertical, + children: _movies + .map((movie) => Card( + child: Padding( + padding: const EdgeInsets.all(10), + child: Center( + child: Text( + movie.title, + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ), + ))) + .toList()), + ), + ) + ], + )) + ])); + } + + _showError(String message) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Something went wrong'), + content: SingleChildScrollView( + child: SelectableText(message), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + } +} + +class _MyHomePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(widget.title), + ), + body: const Center( + child: DataConnectWidget(), + ), + ); + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/.gitignore b/packages/firebase_data_connect/firebase_data_connect/example/macos/.gitignore new file mode 100644 index 000000000000..746adbb6b9e1 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/firebase_data_connect/firebase_data_connect/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Flutter/Flutter-Release.xcconfig b/packages/firebase_data_connect/firebase_data_connect/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Podfile b/packages/firebase_data_connect/firebase_data_connect/example/macos/Podfile new file mode 100644 index 000000000000..c795730db8ed --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..57bda15d2206 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,709 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 6A09C092ECC09506AD892B52 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 93CDFB6B9272C6F1BE9AB929 /* GoogleService-Info.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 93CDFB6B9272C6F1BE9AB929 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 93CDFB6B9272C6F1BE9AB929 /* GoogleService-Info.plist */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + 6A09C092ECC09506AD892B52 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.dataconnect.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.dataconnect.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.dataconnect.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..15368eccb25a --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..1d526a16ed0f --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/AppDelegate.swift b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..d53ef6437726 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..a2ec33f19f11 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..e2471a8d9ab4 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.firebase.dataconnect.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/Debug.xcconfig b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..36b0fd9464f4 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/Release.xcconfig b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..dff4f49561c8 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/Warnings.xcconfig b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000000..42bcbf4780b1 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/DebugProfile.entitlements b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..dddb8a30c851 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/GoogleService-Info.plist b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/GoogleService-Info.plist new file mode 100644 index 000000000000..60f43a3d0cee --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/GoogleService-Info.plist @@ -0,0 +1,38 @@ + + + + + CLIENT_ID + 406099696497-1ugbsqv8nkfn788ep0k233e750aupb7u.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.406099696497-1ugbsqv8nkfn788ep0k233e750aupb7u + ANDROID_CLIENT_ID + 406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com + API_KEY + AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c + GCM_SENDER_ID + 406099696497 + PLIST_VERSION + 1 + BUNDLE_ID + io.flutter.plugins.firebaseDatabaseExample + PROJECT_ID + flutterfire-e2e-tests + STORAGE_BUCKET + flutterfire-e2e-tests.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:406099696497:ios:e31ee2c5dc99d4743574d0 + DATABASE_URL + https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app + + \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Info.plist b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Info.plist new file mode 100644 index 000000000000..4789daa6a443 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/MainFlutterWindow.swift b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..3cc05eb23491 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Release.entitlements b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..852fa1a4728a --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/RunnerTests/RunnerTests.swift b/packages/firebase_data_connect/firebase_data_connect/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000000..07f3dbe7925c --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,14 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml b/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml new file mode 100644 index 000000000000..7f5c88cde257 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml @@ -0,0 +1,100 @@ +name: firebase_data_connect_example +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=3.2.0 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + firebase_core: ^3.2.0 + google_sign_in: ^6.1.0 + firebase_auth: ^5.1.2 + firebase_data_connect: + path: ../ + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.6 + flutter_rating_bar: ^4.0.1 + protobuf: ^3.1.0 + firebase_app_check: ^0.3.0+3 + +dev_dependencies: + build_runner: ^2.3.3 + + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^3.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/firebase_data_connect/firebase_data_connect/example/schema/schema.gql b/packages/firebase_data_connect/firebase_data_connect/example/schema/schema.gql new file mode 100644 index 000000000000..4b2f5711ca46 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/schema/schema.gql @@ -0,0 +1,23 @@ +# # Example schema +# # TODO: Replace with a really good illustrative example from devrel! +# type Product @table { +# name: String! +# price: Int! +# } + +# type Order @table { +# name: String! +# } + +# type OrderItem @table(key: ["order", "product"]) { +# order: Order! +# product: Product! +# quantity: Int! +# } +type Movie @table { + name: String! + genre: String! + description: String + await: String + release: Timestamp +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/test/widget_test.dart b/packages/firebase_data_connect/firebase_data_connect/example/test/widget_test.dart new file mode 100644 index 000000000000..5ef64fc01675 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/test/widget_test.dart @@ -0,0 +1,34 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:firebase_data_connect_example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/web/favicon.png b/packages/firebase_data_connect/firebase_data_connect/example/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/web/icons/Icon-192.png b/packages/firebase_data_connect/firebase_data_connect/example/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/web/icons/Icon-512.png b/packages/firebase_data_connect/firebase_data_connect/example/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/web/icons/Icon-maskable-192.png b/packages/firebase_data_connect/firebase_data_connect/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/web/icons/Icon-maskable-512.png b/packages/firebase_data_connect/firebase_data_connect/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/packages/firebase_data_connect/firebase_data_connect/example/web/index.html b/packages/firebase_data_connect/firebase_data_connect/example/web/index.html new file mode 100644 index 000000000000..301699c20263 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + flutterfire_firebase_data_connect + + + + + + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/web/manifest.json b/packages/firebase_data_connect/firebase_data_connect/example/web/manifest.json new file mode 100644 index 000000000000..096edf8fe4cd --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/packages/firebase_data_connect/firebase_data_connect/generate_proto.sh b/packages/firebase_data_connect/firebase_data_connect/generate_proto.sh new file mode 100755 index 000000000000..b8d3232eef71 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/generate_proto.sh @@ -0,0 +1,4 @@ +#!/bin/bash +rm -rf lib/src/generated +mkdir lib/src/generated +protoc --dart_out=grpc:lib/src/generated -I./protos/firebase -I./protos/google connector_service.proto google/protobuf/struct.proto graphql_error.proto --proto_path=./protos diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart b/packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart new file mode 100644 index 000000000000..63819ce1dfdf --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart @@ -0,0 +1,25 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library firebase_data_connect; + +import 'dart:async'; + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; + +import 'src/common/common_library.dart'; +import 'src/network/transport_library.dart' + if (dart.library.io) 'src/network/grpc_library.dart' + if (dart.library.html) 'src/network/rest_library.dart'; + +export 'src/common/common_library.dart'; + +part 'src/core/ref.dart'; +part 'src/firebase_data_connect.dart'; +part 'src/optional.dart'; +part 'src/timestamp.dart'; +part 'src/core/empty_serializer.dart'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart new file mode 100644 index 000000000000..c8edc7320221 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart @@ -0,0 +1,60 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library firebase_data_connect_common; + +import 'dart:convert'; + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; + +part 'dataconnect_error.dart'; +part 'dataconnect_options.dart'; + +/// Transport Options for connecting to a specific host. +class TransportOptions { + /// Constructor + TransportOptions(this.host, this.port, this.isSecure); + + /// Host to connect to + String host; + + /// Port to connect to + int? port; + + /// isSecure - use secure protocol + bool? isSecure; +} + +/// Interface for transports connecting to the DataConnect backend. +abstract class DataConnectTransport { + /// Constructor. + DataConnectTransport(this.transportOptions, this.options); + + /// Transport options. + TransportOptions transportOptions; + + /// DataConnect backend configuration. + DataConnectOptions options; + + /// FirebaseAuth to use to get auth token. + FirebaseAuth? auth; + + /// FirebaseAppCheck to use to get app check token. + FirebaseAppCheck? appCheck; + + /// Invokes corresponding query endpoint. + Future invokeQuery( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars); + + /// Invokes corresponding mutation endpoint. + Future invokeMutation( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart new file mode 100644 index 000000000000..00796307fc8b --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart @@ -0,0 +1,21 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_data_connect_common; + +/// Types of DataConnect errors that can occur. +enum DataConnectErrorCode { unavailable, unauthorized, other } + +/// Error thrown when DataConnect encounters an error. +class DataConnectError extends FirebaseException { + DataConnectError(this.dataConnectErrorCode, message) + : super( + plugin: 'Data Connect', + code: dataConnectErrorCode.toString(), + message: message); + final DataConnectErrorCode dataConnectErrorCode; +} + +typedef Serializer = String Function(Variables vars); +typedef Deserializer = Data Function(String data); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_options.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_options.dart new file mode 100644 index 000000000000..3d23ef63432c --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_options.dart @@ -0,0 +1,40 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_data_connect_common; + +/// ConnectorConfig options required for connecting to a Data Connect instance. +class ConnectorConfig { + /// Constructor + ConnectorConfig(this.location, this.connector, this.serviceId); + + /// location + String location; + + /// connector + String connector; + + /// serviceId + String serviceId; + + /// String representation of connectorConfig + String toJson() { + return jsonEncode({ + location: location, + connector: connector, + serviceId: serviceId, + }); + } +} + +/// DataConnectOptions includes the Project ID along with the existing ConnectorConfig. +class DataConnectOptions extends ConnectorConfig { + /// Constructor + DataConnectOptions( + this.projectId, String location, String connector, String serviceId) + : super(location, connector, serviceId); + + /// projectId for Firebase App + String projectId; +} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/empty_serializer.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/empty_serializer.dart new file mode 100644 index 000000000000..aace68dfad96 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/empty_serializer.dart @@ -0,0 +1,10 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_data_connect; + +// Empty serializer to be used when a null variable is passed. +String emptySerializer(void _) { + return ''; +} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart new file mode 100644 index 000000000000..228e65f2df90 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart @@ -0,0 +1,146 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_data_connect; + +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +/// Result of a query request. Created to hold extra variables in the future. +class QueryResult extends OperationResult { + QueryResult(super.dataConnect, super.data, super.ref); +} + +/// Reference to a specific query. +/// Contains variables, transport to execute queries, and serialization/deserialization strategies. +abstract class OperationRef { + /// Constructor + OperationRef(this.dataConnect, this.operationName, this._transport, + this.deserializer, this.serializer, this.variables); + Variables? variables; + String operationName; + DataConnectTransport _transport; + Deserializer deserializer; + Serializer serializer; + + FirebaseDataConnect dataConnect; + + Future> execute(); +} + +/// Tracks currently active queries, and emits events when a new query is executed. +class _QueryManager { + _QueryManager(this.dataConnect); + + /// FirebaseDataConnect instance; + FirebaseDataConnect dataConnect; + + /// Keeps track of what queries are currently active. + Map>> trackedQueries = {}; + bool containsQuery( + String queryName, Variables variables, String varsAsStr) { + String key = varsAsStr; + return (trackedQueries[queryName] != null) && + trackedQueries[queryName]![key] != null; + } + + Stream addQuery( + String queryName, Variables variables, String varsAsStr) { + // TODO(mtewani): Replace with more stable encoder + String key = varsAsStr; + if (trackedQueries[queryName] == null) { + trackedQueries[queryName] = {}; + } + if (trackedQueries[queryName]![key] == null) { + trackedQueries[queryName]![key] = StreamController.broadcast(); + } + return trackedQueries[queryName]![key]!.stream; + } + + Future triggerCallback( + String operationName, + String varsAsStr, + QueryRef ref, + Data? data, + Exception? error) async { + String key = varsAsStr; + if (trackedQueries[operationName] == null || + trackedQueries[operationName]![key] == null) { + return; + } + StreamController stream = trackedQueries[operationName]![key]!; + // TODO(mtewani): Prevent this from getting called multiple times. + stream.onCancel = () => stream.close(); + if (error != null) { + stream.addError(error); + } else { + stream.add(QueryResult(dataConnect, data as Data, ref)); + } + } +} + +class QueryRef extends OperationRef { + QueryRef( + FirebaseDataConnect dataConnect, + String operationName, + DataConnectTransport transport, + Deserializer deserializer, + this._queryManager, + Serializer serializer, + Variables? variables) + : super(dataConnect, operationName, transport, deserializer, serializer, + variables); + + _QueryManager _queryManager; + @override + Future> execute() async { + try { + Data data = await _transport.invokeQuery( + operationName, deserializer, serializer, variables); + QueryResult res = QueryResult(dataConnect, data, this); + await _queryManager.triggerCallback(operationName, + serializer(variables as Variables), this, res.data, null); + return res; + } on Exception catch (e) { + print(e); + await _queryManager.triggerCallback( + operationName, serializer(variables as Variables), this, null, e); + rethrow; + } + } + + Stream> subscribe() { + String varsSerialized = serializer(variables as Variables); + Stream> res = _queryManager + .addQuery(operationName, variables, varsSerialized) + .cast>(); + if (_queryManager.containsQuery(operationName, variables, varsSerialized)) { + this.execute().ignore(); + } + return res; + } +} + +class MutationRef extends OperationRef { + MutationRef( + FirebaseDataConnect dataConnect, + String operationName, + DataConnectTransport transport, + Deserializer deserializer, + Serializer serializer, + Variables? variables, + ) : super(dataConnect, operationName, transport, deserializer, serializer, + variables); + @override + Future> execute() async { + Data data = await _transport.invokeMutation( + operationName, deserializer, serializer, variables); + return OperationResult(dataConnect, data, this); + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/dataconnect_version.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/dataconnect_version.dart new file mode 100644 index 000000000000..812fd6096abe --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/dataconnect_version.dart @@ -0,0 +1,16 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// version number for the package, should be align with pubspec.yaml. +const packageVersion = '0.1.0'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart new file mode 100644 index 000000000000..47979a6ae22a --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart @@ -0,0 +1,120 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_data_connect; + +/// DataConnect class +class FirebaseDataConnect extends FirebasePluginPlatform { + /// Constructor for initializing Data Connect + FirebaseDataConnect._({ + required this.app, + required this.connectorConfig, + this.auth, + this.appCheck, + }) : _options = DataConnectOptions( + app.options.projectId, + connectorConfig.location, + connectorConfig.connector, + connectorConfig.serviceId), + super(app.name, 'plugins.flutter.io/firebase_data_connect') { + _queryManager = _QueryManager(this); + } + + /// QueryManager manages ongoing queries, and their subscriptions. + late _QueryManager _queryManager; + + /// FirebaseApp + FirebaseApp app; + + /// FirebaseAppCheck + FirebaseAppCheck? appCheck; + + /// Due to compatibility issues with grpc-web, we swap out the transport based on what platform the user is using. + /// For web, we use RestTransport. For mobile, we use GRPCTransport. + late DataConnectTransport transport; + + /// FirebaseAuth + FirebaseAuth? auth; + + /// ConnectorConfig + projectId + DataConnectOptions _options; + + /// Data Connect specific config information + ConnectorConfig connectorConfig; + + /// Custom transport options for connecting to the Data Connect service. + TransportOptions? _transportOptions; + + /// Checks whether the transport has been properly initialized. + void _checkTransport() { + _transportOptions ??= + TransportOptions('firebasedataconnect.googleapis.com', null, true); + transport = getTransport(_transportOptions!, _options, auth, appCheck); + } + + /// Returns a [QueryRef] object. + QueryRef query( + String operationName, + Deserializer dataDeserializer, + Serializer varsSerializer, + Variables? vars) { + _checkTransport(); + return QueryRef(this, operationName, transport, + dataDeserializer, _queryManager, varsSerializer, vars); + } + + /// Returns a [MutationRef] object. + MutationRef mutation( + String operationName, + Deserializer dataDeserializer, + Serializer varsSerializer, + Variables? vars) { + _checkTransport(); + return MutationRef( + this, operationName, transport, dataDeserializer, varsSerializer, vars); + } + + /// useDataConnectEmulator connects to the DataConnect emulator. + void useDataConnectEmulator(String host, int port, + {bool automaticHostMapping = true, bool isSecure = false}) { + String mappedHost = automaticHostMapping ? getMappedHost(host) : host; + _transportOptions = TransportOptions(mappedHost, port, isSecure); + } + + /// Currently cached DataConnect instances. Maps from app name to . + static final Map> _cachedInstances = + {}; + + /// Returns an instance using a specified [FirebaseApp]. + /// + /// If [app] is not provided, the default Firebase app will be used. + /// If pass in [appCheck], request session will get protected from abusing. + static FirebaseDataConnect instanceFor({ + FirebaseApp? app, + FirebaseAuth? auth, + FirebaseAppCheck? appCheck, + required ConnectorConfig connectorConfig, + }) { + app ??= Firebase.app(); + auth ??= FirebaseAuth.instanceFor(app: app); + appCheck ??= FirebaseAppCheck.instanceFor(app: app); + + if (_cachedInstances[app.name] != null && + _cachedInstances[app.name]![connectorConfig.toJson()] != null) { + return _cachedInstances[app.name]![connectorConfig.toJson()]!; + } + + FirebaseDataConnect newInstance = FirebaseDataConnect._( + app: app, + auth: auth, + appCheck: appCheck, + connectorConfig: connectorConfig); + if (_cachedInstances[app.name] == null) { + _cachedInstances[app.name] = {}; + } + _cachedInstances[app.name]![connectorConfig.toJson()] = newInstance; + + return newInstance; + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pb.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pb.dart new file mode 100644 index 000000000000..b062eb1bf50d --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pb.dart @@ -0,0 +1,408 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// +// Generated code. Do not modify. +// source: connector_service.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'google/protobuf/struct.pb.dart' as $1; +import 'graphql_error.pb.dart' as $2; + +/// The ExecuteQuery request to Firebase Data Connect. +class ExecuteQueryRequest extends $pb.GeneratedMessage { + factory ExecuteQueryRequest({ + $core.String? name, + $core.String? operationName, + $1.Struct? variables, + }) { + final $result = create(); + if (name != null) { + $result.name = name; + } + if (operationName != null) { + $result.operationName = operationName; + } + if (variables != null) { + $result.variables = variables; + } + return $result; + } + ExecuteQueryRequest._() : super(); + factory ExecuteQueryRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ExecuteQueryRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ExecuteQueryRequest', + package: const $pb.PackageName( + _omitMessageNames ? '' : 'google.firebase.dataconnect.v1alpha'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'name') + ..aOS(2, _omitFieldNames ? '' : 'operationName') + ..aOM<$1.Struct>(3, _omitFieldNames ? '' : 'variables', + subBuilder: $1.Struct.create) + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ExecuteQueryRequest clone() => ExecuteQueryRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ExecuteQueryRequest copyWith(void Function(ExecuteQueryRequest) updates) => + super.copyWith((message) => updates(message as ExecuteQueryRequest)) + as ExecuteQueryRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ExecuteQueryRequest create() => ExecuteQueryRequest._(); + ExecuteQueryRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ExecuteQueryRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ExecuteQueryRequest? _defaultInstance; + + /// The resource name of the connector to find the predefined query, in + /// the format: + /// ``` + /// projects/{project}/locations/{location}/services/{service}/connectors/{connector} + /// ``` + @$pb.TagNumber(1) + $core.String get name => $_getSZ(0); + @$pb.TagNumber(1) + set name($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasName() => $_has(0); + @$pb.TagNumber(1) + void clearName() => clearField(1); + + /// The name of the GraphQL operation name. + /// Required because all Connector operations must be named. + /// See https://graphql.org/learn/queries/#operation-name. + /// (-- api-linter: core::0122::name-suffix=disabled + /// aip.dev/not-precedent: Must conform to GraphQL HTTP spec standard. --) + @$pb.TagNumber(2) + $core.String get operationName => $_getSZ(1); + @$pb.TagNumber(2) + set operationName($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasOperationName() => $_has(1); + @$pb.TagNumber(2) + void clearOperationName() => clearField(2); + + /// Values for GraphQL variables provided in this request. + @$pb.TagNumber(3) + $1.Struct get variables => $_getN(2); + @$pb.TagNumber(3) + set variables($1.Struct v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasVariables() => $_has(2); + @$pb.TagNumber(3) + void clearVariables() => clearField(3); + @$pb.TagNumber(3) + $1.Struct ensureVariables() => $_ensure(2); +} + +/// The ExecuteMutation request to Firebase Data Connect. +class ExecuteMutationRequest extends $pb.GeneratedMessage { + factory ExecuteMutationRequest({ + $core.String? name, + $core.String? operationName, + $1.Struct? variables, + }) { + final $result = create(); + if (name != null) { + $result.name = name; + } + if (operationName != null) { + $result.operationName = operationName; + } + if (variables != null) { + $result.variables = variables; + } + return $result; + } + ExecuteMutationRequest._() : super(); + factory ExecuteMutationRequest.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ExecuteMutationRequest.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ExecuteMutationRequest', + package: const $pb.PackageName( + _omitMessageNames ? '' : 'google.firebase.dataconnect.v1alpha'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'name') + ..aOS(2, _omitFieldNames ? '' : 'operationName') + ..aOM<$1.Struct>(3, _omitFieldNames ? '' : 'variables', + subBuilder: $1.Struct.create) + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ExecuteMutationRequest clone() => + ExecuteMutationRequest()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ExecuteMutationRequest copyWith( + void Function(ExecuteMutationRequest) updates) => + super.copyWith((message) => updates(message as ExecuteMutationRequest)) + as ExecuteMutationRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ExecuteMutationRequest create() => ExecuteMutationRequest._(); + ExecuteMutationRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ExecuteMutationRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ExecuteMutationRequest? _defaultInstance; + + /// The resource name of the connector to find the predefined mutation, in + /// the format: + /// ``` + /// projects/{project}/locations/{location}/services/{service}/connectors/{connector} + /// ``` + @$pb.TagNumber(1) + $core.String get name => $_getSZ(0); + @$pb.TagNumber(1) + set name($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasName() => $_has(0); + @$pb.TagNumber(1) + void clearName() => clearField(1); + + /// The name of the GraphQL operation name. + /// Required because all Connector operations must be named. + /// See https://graphql.org/learn/queries/#operation-name. + /// (-- api-linter: core::0122::name-suffix=disabled + /// aip.dev/not-precedent: Must conform to GraphQL HTTP spec standard. --) + @$pb.TagNumber(2) + $core.String get operationName => $_getSZ(1); + @$pb.TagNumber(2) + set operationName($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasOperationName() => $_has(1); + @$pb.TagNumber(2) + void clearOperationName() => clearField(2); + + /// Values for GraphQL variables provided in this request. + @$pb.TagNumber(3) + $1.Struct get variables => $_getN(2); + @$pb.TagNumber(3) + set variables($1.Struct v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasVariables() => $_has(2); + @$pb.TagNumber(3) + void clearVariables() => clearField(3); + @$pb.TagNumber(3) + $1.Struct ensureVariables() => $_ensure(2); +} + +/// The ExecuteQuery response from Firebase Data Connect. +class ExecuteQueryResponse extends $pb.GeneratedMessage { + factory ExecuteQueryResponse({ + $1.Struct? data, + $core.Iterable<$2.GraphqlError>? errors, + }) { + final $result = create(); + if (data != null) { + $result.data = data; + } + if (errors != null) { + $result.errors.addAll(errors); + } + return $result; + } + ExecuteQueryResponse._() : super(); + factory ExecuteQueryResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ExecuteQueryResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ExecuteQueryResponse', + package: const $pb.PackageName( + _omitMessageNames ? '' : 'google.firebase.dataconnect.v1alpha'), + createEmptyInstance: create) + ..aOM<$1.Struct>(1, _omitFieldNames ? '' : 'data', + subBuilder: $1.Struct.create) + ..pc<$2.GraphqlError>( + 2, _omitFieldNames ? '' : 'errors', $pb.PbFieldType.PM, + subBuilder: $2.GraphqlError.create) + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ExecuteQueryResponse clone() => + ExecuteQueryResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ExecuteQueryResponse copyWith(void Function(ExecuteQueryResponse) updates) => + super.copyWith((message) => updates(message as ExecuteQueryResponse)) + as ExecuteQueryResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ExecuteQueryResponse create() => ExecuteQueryResponse._(); + ExecuteQueryResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ExecuteQueryResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ExecuteQueryResponse? _defaultInstance; + + /// The result of executing the requested operation. + @$pb.TagNumber(1) + $1.Struct get data => $_getN(0); + @$pb.TagNumber(1) + set data($1.Struct v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasData() => $_has(0); + @$pb.TagNumber(1) + void clearData() => clearField(1); + @$pb.TagNumber(1) + $1.Struct ensureData() => $_ensure(0); + + /// Errors of this response. + @$pb.TagNumber(2) + $core.List<$2.GraphqlError> get errors => $_getList(1); +} + +/// The ExecuteMutation response from Firebase Data Connect. +class ExecuteMutationResponse extends $pb.GeneratedMessage { + factory ExecuteMutationResponse({ + $1.Struct? data, + $core.Iterable<$2.GraphqlError>? errors, + }) { + final $result = create(); + if (data != null) { + $result.data = data; + } + if (errors != null) { + $result.errors.addAll(errors); + } + return $result; + } + ExecuteMutationResponse._() : super(); + factory ExecuteMutationResponse.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ExecuteMutationResponse.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ExecuteMutationResponse', + package: const $pb.PackageName( + _omitMessageNames ? '' : 'google.firebase.dataconnect.v1alpha'), + createEmptyInstance: create) + ..aOM<$1.Struct>(1, _omitFieldNames ? '' : 'data', + subBuilder: $1.Struct.create) + ..pc<$2.GraphqlError>( + 2, _omitFieldNames ? '' : 'errors', $pb.PbFieldType.PM, + subBuilder: $2.GraphqlError.create) + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ExecuteMutationResponse clone() => + ExecuteMutationResponse()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ExecuteMutationResponse copyWith( + void Function(ExecuteMutationResponse) updates) => + super.copyWith((message) => updates(message as ExecuteMutationResponse)) + as ExecuteMutationResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ExecuteMutationResponse create() => ExecuteMutationResponse._(); + ExecuteMutationResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ExecuteMutationResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ExecuteMutationResponse? _defaultInstance; + + /// The result of executing the requested operation. + @$pb.TagNumber(1) + $1.Struct get data => $_getN(0); + @$pb.TagNumber(1) + set data($1.Struct v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasData() => $_has(0); + @$pb.TagNumber(1) + void clearData() => clearField(1); + @$pb.TagNumber(1) + $1.Struct ensureData() => $_ensure(0); + + /// Errors of this response. + @$pb.TagNumber(2) + $core.List<$2.GraphqlError> get errors => $_getList(1); +} + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbenum.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbenum.dart new file mode 100644 index 000000000000..5512511ce026 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbenum.dart @@ -0,0 +1,14 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// +// Generated code. Do not modify. +// source: connector_service.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbgrpc.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbgrpc.dart new file mode 100644 index 000000000000..ee3296bb26f1 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbgrpc.dart @@ -0,0 +1,99 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// +// Generated code. Do not modify. +// source: connector_service.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:async' as $async; +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'connector_service.pb.dart' as $0; + +export 'connector_service.pb.dart'; + +@$pb.GrpcServiceName('google.firebase.dataconnect.v1alpha.ConnectorService') +class ConnectorServiceClient extends $grpc.Client { + ConnectorServiceClient($grpc.ClientChannel channel, + {$grpc.CallOptions? options, + $core.Iterable<$grpc.ClientInterceptor>? interceptors}) + : super(channel, options: options, interceptors: interceptors); + static final _$executeQuery = + $grpc.ClientMethod<$0.ExecuteQueryRequest, $0.ExecuteQueryResponse>( + '/google.firebase.dataconnect.v1alpha.ConnectorService/ExecuteQuery', + ($0.ExecuteQueryRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $0.ExecuteQueryResponse.fromBuffer(value)); + static final _$executeMutation = $grpc.ClientMethod<$0.ExecuteMutationRequest, + $0.ExecuteMutationResponse>( + '/google.firebase.dataconnect.v1alpha.ConnectorService/ExecuteMutation', + ($0.ExecuteMutationRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => + $0.ExecuteMutationResponse.fromBuffer(value)); + + $grpc.ResponseFuture<$0.ExecuteQueryResponse> executeQuery( + $0.ExecuteQueryRequest request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$executeQuery, request, options: options); + } + + $grpc.ResponseFuture<$0.ExecuteMutationResponse> executeMutation( + $0.ExecuteMutationRequest request, + {$grpc.CallOptions? options}) { + return $createUnaryCall(_$executeMutation, request, options: options); + } +} + +@$pb.GrpcServiceName('google.firebase.dataconnect.v1alpha.ConnectorService') +abstract class ConnectorServiceBase extends $grpc.Service { + ConnectorServiceBase() { + $addMethod( + $grpc.ServiceMethod<$0.ExecuteQueryRequest, $0.ExecuteQueryResponse>( + 'ExecuteQuery', + executeQuery_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.ExecuteQueryRequest.fromBuffer(value), + ($0.ExecuteQueryResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.ExecuteMutationRequest, + $0.ExecuteMutationResponse>( + 'ExecuteMutation', + executeMutation_Pre, + false, + false, + ($core.List<$core.int> value) => + $0.ExecuteMutationRequest.fromBuffer(value), + ($0.ExecuteMutationResponse value) => value.writeToBuffer())); + } + $core.String get $name => + 'google.firebase.dataconnect.v1alpha.ConnectorService'; + + $async.Future<$0.ExecuteQueryResponse> executeQuery_Pre( + $grpc.ServiceCall call, + $async.Future<$0.ExecuteQueryRequest> request) async { + return executeQuery(call, await request); + } + + $async.Future<$0.ExecuteMutationResponse> executeMutation_Pre( + $grpc.ServiceCall call, + $async.Future<$0.ExecuteMutationRequest> request) async { + return executeMutation(call, await request); + } + + $async.Future<$0.ExecuteQueryResponse> executeQuery( + $grpc.ServiceCall call, $0.ExecuteQueryRequest request); + $async.Future<$0.ExecuteMutationResponse> executeMutation( + $grpc.ServiceCall call, $0.ExecuteMutationRequest request); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbjson.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbjson.dart new file mode 100644 index 000000000000..7b44f9f9de90 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/connector_service.pbjson.dart @@ -0,0 +1,138 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// +// Generated code. Do not modify. +// source: connector_service.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use executeQueryRequestDescriptor instead') +const ExecuteQueryRequest$json = { + '1': 'ExecuteQueryRequest', + '2': [ + {'1': 'name', '3': 1, '4': 1, '5': 9, '8': {}, '10': 'name'}, + { + '1': 'operation_name', + '3': 2, + '4': 1, + '5': 9, + '8': {}, + '10': 'operationName' + }, + { + '1': 'variables', + '3': 3, + '4': 1, + '5': 11, + '6': '.google.protobuf.Struct', + '8': {}, + '10': 'variables' + }, + ], +}; + +/// Descriptor for `ExecuteQueryRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List executeQueryRequestDescriptor = $convert.base64Decode( + 'ChNFeGVjdXRlUXVlcnlSZXF1ZXN0EhcKBG5hbWUYASABKAlCA+BBAlIEbmFtZRIqCg5vcGVyYX' + 'Rpb25fbmFtZRgCIAEoCUID4EECUg1vcGVyYXRpb25OYW1lEjoKCXZhcmlhYmxlcxgDIAEoCzIX' + 'Lmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3RCA+BBAVIJdmFyaWFibGVz'); + +@$core.Deprecated('Use executeMutationRequestDescriptor instead') +const ExecuteMutationRequest$json = { + '1': 'ExecuteMutationRequest', + '2': [ + {'1': 'name', '3': 1, '4': 1, '5': 9, '8': {}, '10': 'name'}, + { + '1': 'operation_name', + '3': 2, + '4': 1, + '5': 9, + '8': {}, + '10': 'operationName' + }, + { + '1': 'variables', + '3': 3, + '4': 1, + '5': 11, + '6': '.google.protobuf.Struct', + '8': {}, + '10': 'variables' + }, + ], +}; + +/// Descriptor for `ExecuteMutationRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List executeMutationRequestDescriptor = $convert.base64Decode( + 'ChZFeGVjdXRlTXV0YXRpb25SZXF1ZXN0EhcKBG5hbWUYASABKAlCA+BBAlIEbmFtZRIqCg5vcG' + 'VyYXRpb25fbmFtZRgCIAEoCUID4EECUg1vcGVyYXRpb25OYW1lEjoKCXZhcmlhYmxlcxgDIAEo' + 'CzIXLmdvb2dsZS5wcm90b2J1Zi5TdHJ1Y3RCA+BBAVIJdmFyaWFibGVz'); + +@$core.Deprecated('Use executeQueryResponseDescriptor instead') +const ExecuteQueryResponse$json = { + '1': 'ExecuteQueryResponse', + '2': [ + { + '1': 'data', + '3': 1, + '4': 1, + '5': 11, + '6': '.google.protobuf.Struct', + '10': 'data' + }, + { + '1': 'errors', + '3': 2, + '4': 3, + '5': 11, + '6': '.google.firebase.dataconnect.v1alpha.GraphqlError', + '10': 'errors' + }, + ], +}; + +/// Descriptor for `ExecuteQueryResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List executeQueryResponseDescriptor = $convert.base64Decode( + 'ChRFeGVjdXRlUXVlcnlSZXNwb25zZRIrCgRkYXRhGAEgASgLMhcuZ29vZ2xlLnByb3RvYnVmLl' + 'N0cnVjdFIEZGF0YRJJCgZlcnJvcnMYAiADKAsyMS5nb29nbGUuZmlyZWJhc2UuZGF0YWNvbm5l' + 'Y3QudjFhbHBoYS5HcmFwaHFsRXJyb3JSBmVycm9ycw=='); + +@$core.Deprecated('Use executeMutationResponseDescriptor instead') +const ExecuteMutationResponse$json = { + '1': 'ExecuteMutationResponse', + '2': [ + { + '1': 'data', + '3': 1, + '4': 1, + '5': 11, + '6': '.google.protobuf.Struct', + '10': 'data' + }, + { + '1': 'errors', + '3': 2, + '4': 3, + '5': 11, + '6': '.google.firebase.dataconnect.v1alpha.GraphqlError', + '10': 'errors' + }, + ], +}; + +/// Descriptor for `ExecuteMutationResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List executeMutationResponseDescriptor = $convert.base64Decode( + 'ChdFeGVjdXRlTXV0YXRpb25SZXNwb25zZRIrCgRkYXRhGAEgASgLMhcuZ29vZ2xlLnByb3RvYn' + 'VmLlN0cnVjdFIEZGF0YRJJCgZlcnJvcnMYAiADKAsyMS5nb29nbGUuZmlyZWJhc2UuZGF0YWNv' + 'bm5lY3QudjFhbHBoYS5HcmFwaHFsRXJyb3JSBmVycm9ycw=='); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pb.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pb.dart new file mode 100644 index 000000000000..02ef0302a21b --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pb.dart @@ -0,0 +1,345 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// +// Generated code. Do not modify. +// source: google/protobuf/struct.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; +import 'package:protobuf/src/protobuf/mixins/well_known.dart' as $mixin; + +import 'struct.pbenum.dart'; + +export 'struct.pbenum.dart'; + +/// `Struct` represents a structured data value, consisting of fields +/// which map to dynamically typed values. In some languages, `Struct` +/// might be supported by a native representation. For example, in +/// scripting languages like JS a struct is represented as an +/// object. The details of that representation are described together +/// with the proto support for the language. +/// +/// The JSON representation for `Struct` is JSON object. +class Struct extends $pb.GeneratedMessage with $mixin.StructMixin { + factory Struct({ + $core.Map<$core.String, Value>? fields, + }) { + final $result = create(); + if (fields != null) { + $result.fields.addAll(fields); + } + return $result; + } + Struct._() : super(); + factory Struct.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Struct.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Struct', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'google.protobuf'), + createEmptyInstance: create, + toProto3Json: $mixin.StructMixin.toProto3JsonHelper, + fromProto3Json: $mixin.StructMixin.fromProto3JsonHelper) + ..m<$core.String, Value>(1, _omitFieldNames ? '' : 'fields', + entryClassName: 'Struct.FieldsEntry', + keyFieldType: $pb.PbFieldType.OS, + valueFieldType: $pb.PbFieldType.OM, + valueCreator: Value.create, + valueDefaultOrMaker: Value.getDefault, + packageName: const $pb.PackageName('google.protobuf')) + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Struct clone() => Struct()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Struct copyWith(void Function(Struct) updates) => + super.copyWith((message) => updates(message as Struct)) as Struct; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Struct create() => Struct._(); + Struct createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Struct getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Struct? _defaultInstance; + + /// Unordered map of dynamically typed values. + @$pb.TagNumber(1) + $core.Map<$core.String, Value> get fields => $_getMap(0); +} + +enum Value_Kind { + nullValue, + numberValue, + stringValue, + boolValue, + structValue, + listValue, + notSet +} + +/// `Value` represents a dynamically typed value which can be either +/// null, a number, a string, a boolean, a recursive struct value, or a +/// list of values. A producer of value is expected to set one of these +/// variants. Absence of any variant indicates an error. +/// +/// The JSON representation for `Value` is JSON value. +class Value extends $pb.GeneratedMessage with $mixin.ValueMixin { + factory Value({ + NullValue? nullValue, + $core.double? numberValue, + $core.String? stringValue, + $core.bool? boolValue, + Struct? structValue, + ListValue? listValue, + }) { + final $result = create(); + if (nullValue != null) { + $result.nullValue = nullValue; + } + if (numberValue != null) { + $result.numberValue = numberValue; + } + if (stringValue != null) { + $result.stringValue = stringValue; + } + if (boolValue != null) { + $result.boolValue = boolValue; + } + if (structValue != null) { + $result.structValue = structValue; + } + if (listValue != null) { + $result.listValue = listValue; + } + return $result; + } + Value._() : super(); + factory Value.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Value.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, Value_Kind> _Value_KindByTag = { + 1: Value_Kind.nullValue, + 2: Value_Kind.numberValue, + 3: Value_Kind.stringValue, + 4: Value_Kind.boolValue, + 5: Value_Kind.structValue, + 6: Value_Kind.listValue, + 0: Value_Kind.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Value', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'google.protobuf'), + createEmptyInstance: create, + toProto3Json: $mixin.ValueMixin.toProto3JsonHelper, + fromProto3Json: $mixin.ValueMixin.fromProto3JsonHelper) + ..oo(0, [1, 2, 3, 4, 5, 6]) + ..e(1, _omitFieldNames ? '' : 'nullValue', $pb.PbFieldType.OE, + defaultOrMaker: NullValue.NULL_VALUE, + valueOf: NullValue.valueOf, + enumValues: NullValue.values) + ..a<$core.double>( + 2, _omitFieldNames ? '' : 'numberValue', $pb.PbFieldType.OD) + ..aOS(3, _omitFieldNames ? '' : 'stringValue') + ..aOB(4, _omitFieldNames ? '' : 'boolValue') + ..aOM(5, _omitFieldNames ? '' : 'structValue', + subBuilder: Struct.create) + ..aOM(6, _omitFieldNames ? '' : 'listValue', + subBuilder: ListValue.create) + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Value clone() => Value()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Value copyWith(void Function(Value) updates) => + super.copyWith((message) => updates(message as Value)) as Value; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Value create() => Value._(); + Value createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Value getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Value? _defaultInstance; + + Value_Kind whichKind() => _Value_KindByTag[$_whichOneof(0)]!; + void clearKind() => clearField($_whichOneof(0)); + + /// Represents a null value. + @$pb.TagNumber(1) + NullValue get nullValue => $_getN(0); + @$pb.TagNumber(1) + set nullValue(NullValue v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasNullValue() => $_has(0); + @$pb.TagNumber(1) + void clearNullValue() => clearField(1); + + /// Represents a double value. + @$pb.TagNumber(2) + $core.double get numberValue => $_getN(1); + @$pb.TagNumber(2) + set numberValue($core.double v) { + $_setDouble(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasNumberValue() => $_has(1); + @$pb.TagNumber(2) + void clearNumberValue() => clearField(2); + + /// Represents a string value. + @$pb.TagNumber(3) + $core.String get stringValue => $_getSZ(2); + @$pb.TagNumber(3) + set stringValue($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasStringValue() => $_has(2); + @$pb.TagNumber(3) + void clearStringValue() => clearField(3); + + /// Represents a boolean value. + @$pb.TagNumber(4) + $core.bool get boolValue => $_getBF(3); + @$pb.TagNumber(4) + set boolValue($core.bool v) { + $_setBool(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasBoolValue() => $_has(3); + @$pb.TagNumber(4) + void clearBoolValue() => clearField(4); + + /// Represents a structured value. + @$pb.TagNumber(5) + Struct get structValue => $_getN(4); + @$pb.TagNumber(5) + set structValue(Struct v) { + setField(5, v); + } + + @$pb.TagNumber(5) + $core.bool hasStructValue() => $_has(4); + @$pb.TagNumber(5) + void clearStructValue() => clearField(5); + @$pb.TagNumber(5) + Struct ensureStructValue() => $_ensure(4); + + /// Represents a repeated `Value`. + @$pb.TagNumber(6) + ListValue get listValue => $_getN(5); + @$pb.TagNumber(6) + set listValue(ListValue v) { + setField(6, v); + } + + @$pb.TagNumber(6) + $core.bool hasListValue() => $_has(5); + @$pb.TagNumber(6) + void clearListValue() => clearField(6); + @$pb.TagNumber(6) + ListValue ensureListValue() => $_ensure(5); +} + +/// `ListValue` is a wrapper around a repeated field of values. +/// +/// The JSON representation for `ListValue` is JSON array. +class ListValue extends $pb.GeneratedMessage with $mixin.ListValueMixin { + factory ListValue({ + $core.Iterable? values, + }) { + final $result = create(); + if (values != null) { + $result.values.addAll(values); + } + return $result; + } + ListValue._() : super(); + factory ListValue.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ListValue.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ListValue', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'google.protobuf'), + createEmptyInstance: create, + toProto3Json: $mixin.ListValueMixin.toProto3JsonHelper, + fromProto3Json: $mixin.ListValueMixin.fromProto3JsonHelper) + ..pc(1, _omitFieldNames ? '' : 'values', $pb.PbFieldType.PM, + subBuilder: Value.create) + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ListValue clone() => ListValue()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ListValue copyWith(void Function(ListValue) updates) => + super.copyWith((message) => updates(message as ListValue)) as ListValue; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ListValue create() => ListValue._(); + ListValue createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ListValue getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ListValue? _defaultInstance; + + /// Repeated field of dynamically typed values. + @$pb.TagNumber(1) + $core.List get values => $_getList(0); +} + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbenum.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbenum.dart new file mode 100644 index 000000000000..d34c2a616ab3 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbenum.dart @@ -0,0 +1,39 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// +// Generated code. Do not modify. +// source: google/protobuf/struct.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +/// `NullValue` is a singleton enumeration to represent the null value for the +/// `Value` type union. +/// +/// The JSON representation for `NullValue` is JSON `null`. +class NullValue extends $pb.ProtobufEnum { + static const NullValue NULL_VALUE = + NullValue._(0, _omitEnumNames ? '' : 'NULL_VALUE'); + + static const $core.List values = [ + NULL_VALUE, + ]; + + static final $core.Map<$core.int, NullValue> _byValue = + $pb.ProtobufEnum.initByValue(values); + static NullValue? valueOf($core.int value) => _byValue[value]; + + const NullValue._($core.int v, $core.String n) : super(v, n); +} + +const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbjson.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbjson.dart new file mode 100644 index 000000000000..744bfdffbcb6 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pbjson.dart @@ -0,0 +1,138 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// +// Generated code. Do not modify. +// source: google/protobuf/struct.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use nullValueDescriptor instead') +const NullValue$json = { + '1': 'NullValue', + '2': [ + {'1': 'NULL_VALUE', '2': 0}, + ], +}; + +/// Descriptor for `NullValue`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List nullValueDescriptor = + $convert.base64Decode('CglOdWxsVmFsdWUSDgoKTlVMTF9WQUxVRRAA'); + +@$core.Deprecated('Use structDescriptor instead') +const Struct$json = { + '1': 'Struct', + '2': [ + { + '1': 'fields', + '3': 1, + '4': 3, + '5': 11, + '6': '.google.protobuf.Struct.FieldsEntry', + '10': 'fields' + }, + ], + '3': [Struct_FieldsEntry$json], +}; + +@$core.Deprecated('Use structDescriptor instead') +const Struct_FieldsEntry$json = { + '1': 'FieldsEntry', + '2': [ + {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, + { + '1': 'value', + '3': 2, + '4': 1, + '5': 11, + '6': '.google.protobuf.Value', + '10': 'value' + }, + ], + '7': {'7': true}, +}; + +/// Descriptor for `Struct`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List structDescriptor = $convert.base64Decode( + 'CgZTdHJ1Y3QSOwoGZmllbGRzGAEgAygLMiMuZ29vZ2xlLnByb3RvYnVmLlN0cnVjdC5GaWVsZH' + 'NFbnRyeVIGZmllbGRzGlEKC0ZpZWxkc0VudHJ5EhAKA2tleRgBIAEoCVIDa2V5EiwKBXZhbHVl' + 'GAIgASgLMhYuZ29vZ2xlLnByb3RvYnVmLlZhbHVlUgV2YWx1ZToCOAE='); + +@$core.Deprecated('Use valueDescriptor instead') +const Value$json = { + '1': 'Value', + '2': [ + { + '1': 'null_value', + '3': 1, + '4': 1, + '5': 14, + '6': '.google.protobuf.NullValue', + '9': 0, + '10': 'nullValue' + }, + {'1': 'number_value', '3': 2, '4': 1, '5': 1, '9': 0, '10': 'numberValue'}, + {'1': 'string_value', '3': 3, '4': 1, '5': 9, '9': 0, '10': 'stringValue'}, + {'1': 'bool_value', '3': 4, '4': 1, '5': 8, '9': 0, '10': 'boolValue'}, + { + '1': 'struct_value', + '3': 5, + '4': 1, + '5': 11, + '6': '.google.protobuf.Struct', + '9': 0, + '10': 'structValue' + }, + { + '1': 'list_value', + '3': 6, + '4': 1, + '5': 11, + '6': '.google.protobuf.ListValue', + '9': 0, + '10': 'listValue' + }, + ], + '8': [ + {'1': 'kind'}, + ], +}; + +/// Descriptor for `Value`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List valueDescriptor = $convert.base64Decode( + 'CgVWYWx1ZRI7CgpudWxsX3ZhbHVlGAEgASgOMhouZ29vZ2xlLnByb3RvYnVmLk51bGxWYWx1ZU' + 'gAUgludWxsVmFsdWUSIwoMbnVtYmVyX3ZhbHVlGAIgASgBSABSC251bWJlclZhbHVlEiMKDHN0' + 'cmluZ192YWx1ZRgDIAEoCUgAUgtzdHJpbmdWYWx1ZRIfCgpib29sX3ZhbHVlGAQgASgISABSCW' + 'Jvb2xWYWx1ZRI8CgxzdHJ1Y3RfdmFsdWUYBSABKAsyFy5nb29nbGUucHJvdG9idWYuU3RydWN0' + 'SABSC3N0cnVjdFZhbHVlEjsKCmxpc3RfdmFsdWUYBiABKAsyGi5nb29nbGUucHJvdG9idWYuTG' + 'lzdFZhbHVlSABSCWxpc3RWYWx1ZUIGCgRraW5k'); + +@$core.Deprecated('Use listValueDescriptor instead') +const ListValue$json = { + '1': 'ListValue', + '2': [ + { + '1': 'values', + '3': 1, + '4': 3, + '5': 11, + '6': '.google.protobuf.Value', + '10': 'values' + }, + ], +}; + +/// Descriptor for `ListValue`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List listValueDescriptor = $convert.base64Decode( + 'CglMaXN0VmFsdWUSLgoGdmFsdWVzGAEgAygLMhYuZ29vZ2xlLnByb3RvYnVmLlZhbHVlUgZ2YW' + 'x1ZXM='); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pb.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pb.dart new file mode 100644 index 000000000000..0493787e5821 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pb.dart @@ -0,0 +1,319 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// +// Generated code. Do not modify. +// source: graphql_error.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'google/protobuf/struct.pb.dart' as $1; + +/// GraphqlError conforms to the GraphQL error spec. +/// https://spec.graphql.org/draft/#sec-Errors +/// +/// Firebase Data Connect API surfaces `GraphqlError` in various APIs: +/// - Upon compile error, `UpdateSchema` and `UpdateConnector` return +/// Code.Invalid_Argument with a list of `GraphqlError` in error details. +/// - Upon query compile error, `ExecuteGraphql` and `ExecuteGraphqlRead` return +/// Code.OK with a list of `GraphqlError` in response body. +/// - Upon query execution error, `ExecuteGraphql`, `ExecuteGraphqlRead`, +/// `ExecuteMutation` and `ExecuteQuery` all return Code.OK with a list of +/// `GraphqlError` in response body. +class GraphqlError extends $pb.GeneratedMessage { + factory GraphqlError({ + $core.String? message, + $core.Iterable? locations, + $1.ListValue? path, + GraphqlErrorExtensions? extensions, + }) { + final $result = create(); + if (message != null) { + $result.message = message; + } + if (locations != null) { + $result.locations.addAll(locations); + } + if (path != null) { + $result.path = path; + } + if (extensions != null) { + $result.extensions = extensions; + } + return $result; + } + GraphqlError._() : super(); + factory GraphqlError.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory GraphqlError.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'GraphqlError', + package: const $pb.PackageName( + _omitMessageNames ? '' : 'google.firebase.dataconnect.v1alpha'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'message') + ..pc( + 2, _omitFieldNames ? '' : 'locations', $pb.PbFieldType.PM, + subBuilder: SourceLocation.create) + ..aOM<$1.ListValue>(3, _omitFieldNames ? '' : 'path', + subBuilder: $1.ListValue.create) + ..aOM(4, _omitFieldNames ? '' : 'extensions', + subBuilder: GraphqlErrorExtensions.create) + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + GraphqlError clone() => GraphqlError()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + GraphqlError copyWith(void Function(GraphqlError) updates) => + super.copyWith((message) => updates(message as GraphqlError)) + as GraphqlError; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GraphqlError create() => GraphqlError._(); + GraphqlError createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GraphqlError getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static GraphqlError? _defaultInstance; + + /// The detailed error message. + /// The message should help developer understand the underlying problem without + /// leaking internal data. + @$pb.TagNumber(1) + $core.String get message => $_getSZ(0); + @$pb.TagNumber(1) + set message($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasMessage() => $_has(0); + @$pb.TagNumber(1) + void clearMessage() => clearField(1); + + /// The source locations where the error occurred. + /// Locations should help developers and toolings identify the source of error + /// quickly. + /// + /// Included in admin endpoints (`ExecuteGraphql`, `ExecuteGraphqlRead`, + /// `UpdateSchema` and `UpdateConnector`) to reference the provided GraphQL + /// GQL document. + /// + /// Omitted in `ExecuteMutation` and `ExecuteQuery` since the caller shouldn't + /// have access access the underlying GQL source. + @$pb.TagNumber(2) + $core.List get locations => $_getList(1); + + /// The result field which could not be populated due to error. + /// + /// Clients can use path to identify whether a null result is intentional or + /// caused by a runtime error. + /// It should be a list of string or index from the root of GraphQL query + /// document. + @$pb.TagNumber(3) + $1.ListValue get path => $_getN(2); + @$pb.TagNumber(3) + set path($1.ListValue v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasPath() => $_has(2); + @$pb.TagNumber(3) + void clearPath() => clearField(3); + @$pb.TagNumber(3) + $1.ListValue ensurePath() => $_ensure(2); + + /// Additional error information. + @$pb.TagNumber(4) + GraphqlErrorExtensions get extensions => $_getN(3); + @$pb.TagNumber(4) + set extensions(GraphqlErrorExtensions v) { + setField(4, v); + } + + @$pb.TagNumber(4) + $core.bool hasExtensions() => $_has(3); + @$pb.TagNumber(4) + void clearExtensions() => clearField(4); + @$pb.TagNumber(4) + GraphqlErrorExtensions ensureExtensions() => $_ensure(3); +} + +/// SourceLocation references a location in a GraphQL source. +class SourceLocation extends $pb.GeneratedMessage { + factory SourceLocation({ + $core.int? line, + $core.int? column, + }) { + final $result = create(); + if (line != null) { + $result.line = line; + } + if (column != null) { + $result.column = column; + } + return $result; + } + SourceLocation._() : super(); + factory SourceLocation.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory SourceLocation.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'SourceLocation', + package: const $pb.PackageName( + _omitMessageNames ? '' : 'google.firebase.dataconnect.v1alpha'), + createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'line', $pb.PbFieldType.O3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'column', $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SourceLocation clone() => SourceLocation()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SourceLocation copyWith(void Function(SourceLocation) updates) => + super.copyWith((message) => updates(message as SourceLocation)) + as SourceLocation; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SourceLocation create() => SourceLocation._(); + SourceLocation createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SourceLocation getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SourceLocation? _defaultInstance; + + /// Line number starting at 1. + @$pb.TagNumber(1) + $core.int get line => $_getIZ(0); + @$pb.TagNumber(1) + set line($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasLine() => $_has(0); + @$pb.TagNumber(1) + void clearLine() => clearField(1); + + /// Column number starting at 1. + @$pb.TagNumber(2) + $core.int get column => $_getIZ(1); + @$pb.TagNumber(2) + set column($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasColumn() => $_has(1); + @$pb.TagNumber(2) + void clearColumn() => clearField(2); +} + +/// GraphqlErrorExtensions contains additional information of `GraphqlError`. +/// (-- TODO(b/305311379): include more detailed error fields: +/// go/firemat:api:gql-errors. --) +class GraphqlErrorExtensions extends $pb.GeneratedMessage { + factory GraphqlErrorExtensions({ + $core.String? file, + }) { + final $result = create(); + if (file != null) { + $result.file = file; + } + return $result; + } + GraphqlErrorExtensions._() : super(); + factory GraphqlErrorExtensions.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory GraphqlErrorExtensions.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'GraphqlErrorExtensions', + package: const $pb.PackageName( + _omitMessageNames ? '' : 'google.firebase.dataconnect.v1alpha'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'file') + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + GraphqlErrorExtensions clone() => + GraphqlErrorExtensions()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + GraphqlErrorExtensions copyWith( + void Function(GraphqlErrorExtensions) updates) => + super.copyWith((message) => updates(message as GraphqlErrorExtensions)) + as GraphqlErrorExtensions; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GraphqlErrorExtensions create() => GraphqlErrorExtensions._(); + GraphqlErrorExtensions createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GraphqlErrorExtensions getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static GraphqlErrorExtensions? _defaultInstance; + + /// The source file name where the error occurred. + /// Included only for `UpdateSchema` and `UpdateConnector`, it corresponds + /// to `File.path` of the provided `Source`. + @$pb.TagNumber(1) + $core.String get file => $_getSZ(0); + @$pb.TagNumber(1) + set file($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasFile() => $_has(0); + @$pb.TagNumber(1) + void clearFile() => clearField(1); +} + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = + $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbenum.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbenum.dart new file mode 100644 index 000000000000..25b956e9db18 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbenum.dart @@ -0,0 +1,14 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// +// Generated code. Do not modify. +// source: graphql_error.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbjson.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbjson.dart new file mode 100644 index 000000000000..5712719cf719 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/graphql_error.pbjson.dart @@ -0,0 +1,85 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// +// Generated code. Do not modify. +// source: graphql_error.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use graphqlErrorDescriptor instead') +const GraphqlError$json = { + '1': 'GraphqlError', + '2': [ + {'1': 'message', '3': 1, '4': 1, '5': 9, '10': 'message'}, + { + '1': 'locations', + '3': 2, + '4': 3, + '5': 11, + '6': '.google.firebase.dataconnect.v1alpha.SourceLocation', + '10': 'locations' + }, + { + '1': 'path', + '3': 3, + '4': 1, + '5': 11, + '6': '.google.protobuf.ListValue', + '10': 'path' + }, + { + '1': 'extensions', + '3': 4, + '4': 1, + '5': 11, + '6': '.google.firebase.dataconnect.v1alpha.GraphqlErrorExtensions', + '10': 'extensions' + }, + ], +}; + +/// Descriptor for `GraphqlError`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List graphqlErrorDescriptor = $convert.base64Decode( + 'CgxHcmFwaHFsRXJyb3ISGAoHbWVzc2FnZRgBIAEoCVIHbWVzc2FnZRJRCglsb2NhdGlvbnMYAi' + 'ADKAsyMy5nb29nbGUuZmlyZWJhc2UuZGF0YWNvbm5lY3QudjFhbHBoYS5Tb3VyY2VMb2NhdGlv' + 'blIJbG9jYXRpb25zEi4KBHBhdGgYAyABKAsyGi5nb29nbGUucHJvdG9idWYuTGlzdFZhbHVlUg' + 'RwYXRoElsKCmV4dGVuc2lvbnMYBCABKAsyOy5nb29nbGUuZmlyZWJhc2UuZGF0YWNvbm5lY3Qu' + 'djFhbHBoYS5HcmFwaHFsRXJyb3JFeHRlbnNpb25zUgpleHRlbnNpb25z'); + +@$core.Deprecated('Use sourceLocationDescriptor instead') +const SourceLocation$json = { + '1': 'SourceLocation', + '2': [ + {'1': 'line', '3': 1, '4': 1, '5': 5, '10': 'line'}, + {'1': 'column', '3': 2, '4': 1, '5': 5, '10': 'column'}, + ], +}; + +/// Descriptor for `SourceLocation`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sourceLocationDescriptor = $convert.base64Decode( + 'Cg5Tb3VyY2VMb2NhdGlvbhISCgRsaW5lGAEgASgFUgRsaW5lEhYKBmNvbHVtbhgCIAEoBVIGY2' + '9sdW1u'); + +@$core.Deprecated('Use graphqlErrorExtensionsDescriptor instead') +const GraphqlErrorExtensions$json = { + '1': 'GraphqlErrorExtensions', + '2': [ + {'1': 'file', '3': 1, '4': 1, '5': 9, '10': 'file'}, + ], +}; + +/// Descriptor for `GraphqlErrorExtensions`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List graphqlErrorExtensionsDescriptor = + $convert.base64Decode( + 'ChZHcmFwaHFsRXJyb3JFeHRlbnNpb25zEhIKBGZpbGUYASABKAlSBGZpbGU='); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_library.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_library.dart new file mode 100644 index 000000000000..641ebca98557 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_library.dart @@ -0,0 +1,19 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library firebase_data_connect_grpc; + +import 'dart:convert'; +import 'dart:developer'; + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:grpc/grpc.dart'; + +import '../common/common_library.dart'; +import '../dataconnect_version.dart'; +import '../generated/connector_service.pbgrpc.dart'; +import '../generated/google/protobuf/struct.pb.dart'; + +part 'grpc_transport.dart'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart new file mode 100644 index 000000000000..7b9cf4919232 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart @@ -0,0 +1,139 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_data_connect_grpc; + +/// Transport used for Android/iOS. Uses a GRPC transport instead of REST. +class GRPCTransport implements DataConnectTransport { + /// GRPCTransport creates a new channel + GRPCTransport(this.transportOptions, this.options, this.auth, this.appCheck) { + bool isSecure = + transportOptions.isSecure == null || transportOptions.isSecure == true; + channel = ClientChannel(transportOptions.host, + port: transportOptions.port ?? 443, + options: ChannelOptions( + credentials: (isSecure + ? const ChannelCredentials.secure() + : const ChannelCredentials.insecure()))); + stub = ConnectorServiceClient(channel); + name = + 'projects/${options.projectId}/locations/${options.location}/services/${options.serviceId}/connectors/${options.connector}'; + } + + /// FirebaseAuth + @override + FirebaseAuth? auth; + + /// FirebaseAppCheck + @override + FirebaseAppCheck? appCheck; + + /// Name of the endpoint. + late String name; + + /// ConnectorServiceClient used to execute the query/mutation. + late ConnectorServiceClient stub; + + /// ClientChannel used to configure connection to the GRPC server. + late ClientChannel channel; + + /// Current host configuration. + @override + TransportOptions transportOptions; + + /// Data Connect backend configuration options. + @override + DataConnectOptions options; + + Future> getMetadata() async { + String? authToken; + try { + authToken = await auth?.currentUser?.getIdToken(); + } catch (e) { + log('Unable to get auth token: $e'); + } + String? appCheckToken; + try { + appCheckToken = await appCheck?.getToken(); + } catch (e) { + log('Unable to get app check token: $e'); + } + Map metadata = { + 'x-goog-request-params': 'location=${options.location}&frontend=data', + 'x-goog-api-client': 'gl-dart/flutter fire/$packageVersion' + }; + + if (authToken != null) { + metadata['x-firebase-auth-token'] = authToken; + } + if (appCheckToken != null) { + metadata['X-Firebase-AppCheck'] = appCheckToken; + } + return metadata; + } + + /// Invokes GPRC query endpoint. + @override + Future invokeQuery( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + ) async { + ExecuteQueryResponse response; + + ExecuteQueryRequest request = + ExecuteQueryRequest(name: name, operationName: queryName); + if (vars != null && serializer != null) { + request.variables = getStruct(vars, serializer); + } + try { + response = await stub.executeQuery(request, + options: CallOptions(metadata: await getMetadata())); + return deserializer(jsonEncode(response.data.toProto3Json())); + } on Exception catch (e) { + throw DataConnectError(DataConnectErrorCode.other, + 'Failed to invoke operation: ${e.toString()}'); + } + } + + /// Converts the variables into a proto Struct. + Struct getStruct( + Variables vars, Serializer serializer) { + Struct struct = Struct.create(); + struct.mergeFromProto3Json(jsonDecode(serializer(vars))); + return struct; + } + + /// Invokes GPRC mutation endpoint. + @override + Future invokeMutation( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars) async { + ExecuteMutationResponse response; + ExecuteMutationRequest request = + ExecuteMutationRequest(name: name, operationName: queryName); + if (vars != null && serializer != null) { + request.variables = getStruct(vars, serializer); + } + try { + response = await stub.executeMutation(request, + options: CallOptions(metadata: await getMetadata())); + return deserializer(jsonEncode(response.data.toProto3Json())); + } on Exception catch (e) { + throw DataConnectError(DataConnectErrorCode.other, + 'Failed to invoke operation: ${e.toString()}'); + } + } +} + +/// Initializes GRPC transport for Data Connect. +DataConnectTransport getTransport( + TransportOptions transportOptions, + DataConnectOptions options, + FirebaseAuth? auth, + FirebaseAppCheck? appCheck) => + GRPCTransport(transportOptions, options, auth, appCheck); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart new file mode 100644 index 000000000000..917da7c3a0ae --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart @@ -0,0 +1,17 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library firebase_data_connect_rest; + +import 'dart:convert'; +import 'dart:developer'; + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:http/http.dart' as http; + +import '../common/common_library.dart'; +import '../dataconnect_version.dart'; + +part 'rest_transport.dart'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart new file mode 100644 index 000000000000..0a9599487cb1 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart @@ -0,0 +1,145 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_data_connect_rest; + +/// RestTransport makes requests out to the REST endpoints of the configured backend. +class RestTransport implements DataConnectTransport { + /// Initializes necessary protocol and port. + RestTransport(this.transportOptions, this.options, this.auth, this.appCheck) { + String protocol = 'http'; + if (transportOptions.isSecure == null || + transportOptions.isSecure == true) { + protocol += 's'; + } + String host = transportOptions.host; + int port = transportOptions.port ?? 443; + String project = options.projectId; + String location = options.location; + String service = options.serviceId; + String connector = options.connector; + _url = + '$protocol://$host:$port/v1alpha/projects/$project/locations/$location/services/$service/connectors/$connector'; + } + + @override + FirebaseAuth? auth; + + @override + FirebaseAppCheck? appCheck; + + /// Current endpoint URL. + late String _url; + + /// Current host configuration. + @override + TransportOptions transportOptions; + + /// Data Connect backend configuration options. + @override + DataConnectOptions options; + + /// Invokes the current operation, whether its a query or mutation. + Future invokeOperation( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + String endpoint) async { + String project = options.projectId; + String location = options.location; + String service = options.serviceId; + String connector = options.connector; + Map headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'x-goog-api-client': 'gl-dart/flutter fire/$packageVersion' + }; + String? authToken; + try { + authToken = await auth?.currentUser?.getIdToken(); + } catch (e) { + log('Unable to get auth token: $e'); + } + String? appCheckToken; + try { + appCheckToken = await appCheck?.getToken(); + } catch (e) { + print('Unable to get app check token: $e'); + } + if (authToken != null) { + headers['X-Firebase-Auth-Token'] = authToken; + } + if (appCheckToken != null) { + headers['X-Firebase-AppCheck'] = appCheckToken; + } + + Map body = { + 'name': + 'projects/$project/locations/$location/services/$service/connectors/$connector', + 'operationName': queryName, + }; + if (vars != null && serializer != null) { + body['variables'] = json.decode(serializer(vars)); + } + try { + http.Response r = await http.post(Uri.parse('$_url:$endpoint'), + body: json.encode(body), headers: headers); + if (r.statusCode != 200) { + Map bodyJson = + jsonDecode(r.body) as Map; + String message = + bodyJson.containsKey('message') ? bodyJson['message']! : r.body; + throw DataConnectError( + r.statusCode == 401 + ? DataConnectErrorCode.unauthorized + : DataConnectErrorCode.other, + "Received a status code of ${r.statusCode} with a message '${message}'"); + } + + /// The response we get is in the data field of the response + /// Once we get the data back, it's not quite json-encoded, + /// so we have to encode it and then send it to the user's deserializer. + return deserializer(jsonEncode(jsonDecode(r.body)['data'])); + } on Exception catch (e) { + if (e is DataConnectError) { + rethrow; + } + throw DataConnectError(DataConnectErrorCode.other, + 'Failed to invoke operation: ${e.toString()}'); + } + } + + /// Invokes query REST endpoint. + @override + Future invokeQuery( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + ) async { + return invokeOperation( + queryName, deserializer, serializer, vars, 'executeQuery'); + } + + /// Invokes mutation REST endpoint. + @override + Future invokeMutation( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + ) async { + return invokeOperation( + queryName, deserializer, serializer, vars, 'executeMutation'); + } +} + +/// Initializes Rest transport for Data Connect. +DataConnectTransport getTransport( + TransportOptions transportOptions, + DataConnectOptions options, + FirebaseAuth? auth, + FirebaseAppCheck? appCheck) => + RestTransport(transportOptions, options, auth, appCheck); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_library.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_library.dart new file mode 100644 index 000000000000..456b0817039e --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_library.dart @@ -0,0 +1,11 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library firebase_data_connect_transport; + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../common/common_library.dart'; + +part 'transport_stub.dart'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_stub.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_stub.dart new file mode 100644 index 000000000000..2a5fb717a199 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_stub.dart @@ -0,0 +1,56 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_data_connect_transport; + +/// Default TransportStub to satisfy compilation of the library. +class TransportStub implements DataConnectTransport { + /// Constructor. + TransportStub(this.transportOptions, this.options, this.auth, this.appCheck); + + /// FirebaseAuth + @override + FirebaseAuth? auth; + + /// FirebaseAppCheck + @override + FirebaseAppCheck? appCheck; + + /// DataConnect backend options. + @override + DataConnectOptions options; + + /// Network configuration options. + @override + TransportOptions transportOptions; + + /// Stub for invoking a mutation. + @override + Future invokeMutation( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars) async { + // TODO: implement invokeMutation + throw UnimplementedError(); + } + + /// Stub for invoking a query. + @override + Future invokeQuery( + String queryName, + Deserializer deserializer, + Serializer? serialize, + Variables? vars) async { + // TODO: implement invokeQuery + throw UnimplementedError(); + } +} + +DataConnectTransport getTransport( + TransportOptions transportOptions, + DataConnectOptions options, + FirebaseAuth? auth, + FirebaseAppCheck? appCheck) => + TransportStub(transportOptions, options, auth, appCheck); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/optional.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/optional.dart new file mode 100644 index 000000000000..a1298dd36d01 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/optional.dart @@ -0,0 +1,100 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_data_connect; + +/// Keeps track of whether the value has been set or not +enum OptionalState { unset, set } + +/// Optional Class that allows users to pass in null or undefined for properties on a class. +/// If the state value is set, then we make sure to include it in the request over the wire. +/// If it's unset, then the value is ignored when sending over the wire. +class Optional { + /// Instantiates deserializer. + Optional(this.deserializer); + + /// Instantiates deserializer and serializer. + Optional.optional(this.deserializer, this.serializer); + + /// State of the value. Is unset by default. + OptionalState state = OptionalState.unset; + + /// Serializer for value. + Serializer? serializer; + + /// Deserializer for value. + Deserializer deserializer; + + /// Current value. + T? _value; + + /// Sets the value for the variable, and the state to `Set`. + set value(T? val) { + _value = val; + state = OptionalState.set; + } + + /// Gets the current value of the object. + T? get value { + return _value; + } + + /// Converts json to the value. + void fromJson(dynamic json) { + if (json is List) { + value = json.map((e) => deserializer(e)) as T; + } else { + value = deserializer(json as String); + } + } + + /// Converts the value to String. + String toJson() { + if (_value != null) { + if (serializer != null) { + if (_value is List) { + return (_value! as List).map((e) => serializer!(e)).toString(); + } + return serializer!(_value as T); + } else { + return _value.toString(); + } + } + return ''; + } +} + +String nativeToJson(T type) { + switch (T.runtimeType) { + case bool: + case int: + case double: + case num: + return type.toString(); + case String: + return type as String; + default: + throw UnimplementedError( + 'This type is unimplemented: ${type.runtimeType}'); + } +} + +T nativeFromJson(String json) { + switch (T.runtimeType) { + case bool: + return bool.parse(json) as T; + case int: + return int.parse(json) as T; + case double: + double.parse(json); + case num: + return num.parse(json) as T; + case String: + return json as T; + default: + throw UnimplementedError( + 'This type is unimplemented: ${json.runtimeType}'); + } + throw Exception('Null!'); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/timestamp.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/timestamp.dart new file mode 100644 index 000000000000..fd3e954c83a4 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/timestamp.dart @@ -0,0 +1,64 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +part of firebase_data_connect; + +/// Timestamp class is a custom class that allows for storing of nanoseconds. +class Timestamp { + /// Constructor + Timestamp(this.nanoseconds, this.seconds); + // TODO(mtewani): Fix this so that it keeps track of positional arguments so you don't have to repeatedly search the string multiple times. + Timestamp.fromJson(String date) { + // ignore: use_raw_strings + var regex = RegExp( + r'^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{0,9})?(Z|[+-]\\d{2}:\\d{2})\$'); + if (!regex.hasMatch(date)) { + throw Exception('Invalid Date provided!'); + } + DateTime dateTime = DateTime.parse(date); + seconds = dateTime.second; + String nanoStr = ''; + int dotIdx = date.indexOf('.'); + if (dotIdx > -1) { + for (int i = dotIdx + 1; i < date.length; i++) { + if (int.tryParse(date[i]) != null) { + nanoStr += date[i]; + } else { + break; + } + } + } + if (nanoStr.isNotEmpty) { + nanoseconds = int.parse(nanoStr.padRight(9, '0')); + } + // TODO(mtewani): Add offset values. + if (date.contains('Z')) { + return; + } + int addIdx = date.indexOf('+'); + bool isAdd = addIdx > -1; + int signIdx = isAdd ? addIdx : date.indexOf('-'); + int timeHour = int.parse(date.substring(signIdx + 1, signIdx + 3)); + int timeMin = int.parse(date.substring(signIdx + 4, signIdx + 6)); + int timeZoneDiffer = timeHour * 3600 + timeMin * 60; + seconds = seconds + (isAdd ? -timeZoneDiffer : timeZoneDiffer); + } + String toJson() { + String secondsStr = + DateTime.fromMillisecondsSinceEpoch(seconds * 1000, isUtc: true) + .toIso8601String(); + String nanoStr = nanoseconds.toString().padRight(9, '0'); + return '${secondsStr.substring(0, nanoStr.length - 1)}.${nanoStr}Z'; + } + + DateTime toDateTime() { + return DateTime.utc((seconds * 1000 + (nanoseconds / 1000000)).floor()); + } + + /// Current nanoseconds + int nanoseconds = 0; + + /// Current seconds + int seconds = 0; +} diff --git a/packages/firebase_data_connect/firebase_data_connect/protos/connector_service.proto b/packages/firebase_data_connect/firebase_data_connect/protos/connector_service.proto new file mode 100644 index 000000000000..466815682df2 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/protos/connector_service.proto @@ -0,0 +1,105 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Adapted from http://google3/google/firebase/dataconnect/v1main/connector_service.proto;rcl=596717236 + +syntax = "proto3"; + +package google.firebase.dataconnect.v1alpha; + +import "google/api/field_behavior.proto"; +import "graphql_error.proto"; +import "google/protobuf/struct.proto"; + +option java_package = "google.firebase.dataconnect.proto"; +option java_multiple_files = true; + +// Firebase Data Connect provides means to deploy a set of predefined GraphQL +// operations (queries and mutations) as a Connector. +// +// Firebase developers can build mobile and web apps that uses Connectors +// to access Data Sources directly. Connectors allow operations without +// admin credentials and help Firebase customers control the API exposure. +// +// Note: `ConnectorService` doesn't check IAM permissions and instead developers +// must define auth policies on each pre-defined operation to secure this +// connector. The auth policies typically define rules on the Firebase Auth +// token. +service ConnectorService { + // Execute a predefined query in a Connector. + rpc ExecuteQuery(ExecuteQueryRequest) returns (ExecuteQueryResponse) { + } + + // Execute a predefined mutation in a Connector. + rpc ExecuteMutation(ExecuteMutationRequest) returns (ExecuteMutationResponse) { + } +} + +// The ExecuteQuery request to Firebase Data Connect. +message ExecuteQueryRequest { + // The resource name of the connector to find the predefined query, in + // the format: + // ``` + // projects/{project}/locations/{location}/services/{service}/connectors/{connector} + // ``` + string name = 1 [(google.api.field_behavior) = REQUIRED]; + + // The name of the GraphQL operation name. + // Required because all Connector operations must be named. + // See https://graphql.org/learn/queries/#operation-name. + // (-- api-linter: core::0122::name-suffix=disabled + // aip.dev/not-precedent: Must conform to GraphQL HTTP spec standard. --) + string operation_name = 2 [(google.api.field_behavior) = REQUIRED]; + + // Values for GraphQL variables provided in this request. + google.protobuf.Struct variables = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +// The ExecuteMutation request to Firebase Data Connect. +message ExecuteMutationRequest { + // The resource name of the connector to find the predefined mutation, in + // the format: + // ``` + // projects/{project}/locations/{location}/services/{service}/connectors/{connector} + // ``` + string name = 1 [(google.api.field_behavior) = REQUIRED]; + + // The name of the GraphQL operation name. + // Required because all Connector operations must be named. + // See https://graphql.org/learn/queries/#operation-name. + // (-- api-linter: core::0122::name-suffix=disabled + // aip.dev/not-precedent: Must conform to GraphQL HTTP spec standard. --) + string operation_name = 2 [(google.api.field_behavior) = REQUIRED]; + + // Values for GraphQL variables provided in this request. + google.protobuf.Struct variables = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +// The ExecuteQuery response from Firebase Data Connect. +message ExecuteQueryResponse { + // The result of executing the requested operation. + google.protobuf.Struct data = 1; + // Errors of this response. + repeated GraphqlError errors = 2; +} + +// The ExecuteMutation response from Firebase Data Connect. +message ExecuteMutationResponse { + // The result of executing the requested operation. + google.protobuf.Struct data = 1; + // Errors of this response. + repeated GraphqlError errors = 2; +} \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/protos/firebase/graphql_error.proto b/packages/firebase_data_connect/firebase_data_connect/protos/firebase/graphql_error.proto new file mode 100644 index 000000000000..86710653cf86 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/protos/firebase/graphql_error.proto @@ -0,0 +1,85 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Adapted from http://google3/google/firebase/dataconnect/v1main/graphql_error.proto;rcl=597595444 + +syntax = "proto3"; + +package google.firebase.dataconnect.v1alpha; + +import "google/protobuf/struct.proto"; + +option java_package = "google.firebase.dataconnect.proto"; +option java_multiple_files = true; + +// GraphqlError conforms to the GraphQL error spec. +// https://spec.graphql.org/draft/#sec-Errors +// +// Firebase Data Connect API surfaces `GraphqlError` in various APIs: +// - Upon compile error, `UpdateSchema` and `UpdateConnector` return +// Code.Invalid_Argument with a list of `GraphqlError` in error details. +// - Upon query compile error, `ExecuteGraphql` and `ExecuteGraphqlRead` return +// Code.OK with a list of `GraphqlError` in response body. +// - Upon query execution error, `ExecuteGraphql`, `ExecuteGraphqlRead`, +// `ExecuteMutation` and `ExecuteQuery` all return Code.OK with a list of +// `GraphqlError` in response body. +message GraphqlError { + // The detailed error message. + // The message should help developer understand the underlying problem without + // leaking internal data. + string message = 1; + + // The source locations where the error occurred. + // Locations should help developers and toolings identify the source of error + // quickly. + // + // Included in admin endpoints (`ExecuteGraphql`, `ExecuteGraphqlRead`, + // `UpdateSchema` and `UpdateConnector`) to reference the provided GraphQL + // GQL document. + // + // Omitted in `ExecuteMutation` and `ExecuteQuery` since the caller shouldn't + // have access access the underlying GQL source. + repeated SourceLocation locations = 2; + + // The result field which could not be populated due to error. + // + // Clients can use path to identify whether a null result is intentional or + // caused by a runtime error. + // It should be a list of string or index from the root of GraphQL query + // document. + google.protobuf.ListValue path = 3; + + // Additional error information. + GraphqlErrorExtensions extensions = 4; +} + +// SourceLocation references a location in a GraphQL source. +message SourceLocation { + // Line number starting at 1. + int32 line = 1; + // Column number starting at 1. + int32 column = 2; +} + +// GraphqlErrorExtensions contains additional information of `GraphqlError`. +// (-- TODO(b/305311379): include more detailed error fields: +// go/firemat:api:gql-errors. --) +message GraphqlErrorExtensions { + // The source file name where the error occurred. + // Included only for `UpdateSchema` and `UpdateConnector`, it corresponds + // to `File.path` of the provided `Source`. + string file = 1; +} \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/protos/google/api/field_behavior.proto b/packages/firebase_data_connect/firebase_data_connect/protos/google/api/field_behavior.proto new file mode 100644 index 000000000000..2865ba053739 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/protos/google/api/field_behavior.proto @@ -0,0 +1,104 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "FieldBehaviorProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.FieldOptions { + // A designation of a specific field behavior (required, output only, etc.) + // in protobuf messages. + // + // Examples: + // + // string name = 1 [(google.api.field_behavior) = REQUIRED]; + // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; + // google.protobuf.Duration ttl = 1 + // [(google.api.field_behavior) = INPUT_ONLY]; + // google.protobuf.Timestamp expire_time = 1 + // [(google.api.field_behavior) = OUTPUT_ONLY, + // (google.api.field_behavior) = IMMUTABLE]; + repeated google.api.FieldBehavior field_behavior = 1052 [packed = false]; +} + +// An indicator of the behavior of a given field (for example, that a field +// is required in requests, or given as output but ignored as input). +// This **does not** change the behavior in protocol buffers itself; it only +// denotes the behavior and may affect how API tooling handles the field. +// +// Note: This enum **may** receive new values in the future. +enum FieldBehavior { + // Conventional default for enums. Do not use this. + FIELD_BEHAVIOR_UNSPECIFIED = 0; + + // Specifically denotes a field as optional. + // While all fields in protocol buffers are optional, this may be specified + // for emphasis if appropriate. + OPTIONAL = 1; + + // Denotes a field as required. + // This indicates that the field **must** be provided as part of the request, + // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). + REQUIRED = 2; + + // Denotes a field as output only. + // This indicates that the field is provided in responses, but including the + // field in a request does nothing (the server *must* ignore it and + // *must not* throw an error as a result of the field's presence). + OUTPUT_ONLY = 3; + + // Denotes a field as input only. + // This indicates that the field is provided in requests, and the + // corresponding field is not included in output. + INPUT_ONLY = 4; + + // Denotes a field as immutable. + // This indicates that the field may be set once in a request to create a + // resource, but may not be changed thereafter. + IMMUTABLE = 5; + + // Denotes that a (repeated) field is an unordered list. + // This indicates that the service may provide the elements of the list + // in any arbitrary order, rather than the order the user originally + // provided. Additionally, the list's order may or may not be stable. + UNORDERED_LIST = 6; + + // Denotes that this field returns a non-empty default value if not set. + // This indicates that if the user provides the empty value in a request, + // a non-empty value will be returned. The user will not be aware of what + // non-empty value to expect. + NON_EMPTY_DEFAULT = 7; + + // Denotes that the field in a resource (a message annotated with + // google.api.resource) is used in the resource name to uniquely identify the + // resource. For AIP-compliant APIs, this should only be applied to the + // `name` field on the resource. + // + // This behavior should not be applied to references to other resources within + // the message. + // + // The identifier field of resources often have different field behavior + // depending on the request it is embedded in (e.g. for Create methods name + // is optional and unused, while for Update methods it is required). Instead + // of method-specific annotations, only `IDENTIFIER` is required. + IDENTIFIER = 8; +} diff --git a/packages/firebase_data_connect/firebase_data_connect/protos/google/struct.proto b/packages/firebase_data_connect/firebase_data_connect/protos/google/struct.proto new file mode 100644 index 000000000000..1bf0c1ad9586 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/protos/google/struct.proto @@ -0,0 +1,95 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/structpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "StructProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; + +// `Struct` represents a structured data value, consisting of fields +// which map to dynamically typed values. In some languages, `Struct` +// might be supported by a native representation. For example, in +// scripting languages like JS a struct is represented as an +// object. The details of that representation are described together +// with the proto support for the language. +// +// The JSON representation for `Struct` is JSON object. +message Struct { + // Unordered map of dynamically typed values. + map fields = 1; +} + +// `Value` represents a dynamically typed value which can be either +// null, a number, a string, a boolean, a recursive struct value, or a +// list of values. A producer of value is expected to set one of these +// variants. Absence of any variant indicates an error. +// +// The JSON representation for `Value` is JSON value. +message Value { + // The kind of value. + oneof kind { + // Represents a null value. + NullValue null_value = 1; + // Represents a double value. + double number_value = 2; + // Represents a string value. + string string_value = 3; + // Represents a boolean value. + bool bool_value = 4; + // Represents a structured value. + Struct struct_value = 5; + // Represents a repeated `Value`. + ListValue list_value = 6; + } +} + +// `NullValue` is a singleton enumeration to represent the null value for the +// `Value` type union. +// +// The JSON representation for `NullValue` is JSON `null`. +enum NullValue { + // Null value. + NULL_VALUE = 0; +} + +// `ListValue` is a wrapper around a repeated field of values. +// +// The JSON representation for `ListValue` is JSON array. +message ListValue { + // Repeated field of dynamically typed values. + repeated Value values = 1; +} diff --git a/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml b/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml new file mode 100644 index 000000000000..165c44c9bc6f --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml @@ -0,0 +1,29 @@ +name: firebase_data_connect +description: "Firebase Data Connect" +version: 0.1.0 +homepage: https://firebase.google.com/docs/data-connect/quickstart?platform=flutter +false_secrets: + - example/** + - dartpad/** + +environment: + sdk: '>=3.2.0 <4.0.0' + flutter: ">=3.3.0" + +dependencies: + firebase_app_check: ^0.3.0+3 + firebase_auth: ^5.1.2 + firebase_core: ^3.2.0 + firebase_core_platform_interface: ^5.1.0 + flutter: + sdk: flutter + grpc: ^3.2.4 + http: ^1.2.1 + protobuf: ^3.1.0 + +dev_dependencies: + flutter_lints: ^4.0.0 + flutter_test: + sdk: flutter + mockito: ^5.0.0 + plugin_platform_interface: ^2.1.3 From fa12459f35a67b6cd489db37d68a529e32ee7ce3 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Thu, 12 Sep 2024 13:19:39 +0200 Subject: [PATCH 2/2] test(fdc): add e2e and unit tests to fdc (#13224) * First pass at dataconnect * Got mutations to work * Finished WIP * Used generated SDK * Removed unrelated chagnes * Cleanup * Updated deps * Addressed comments * Removed prints * Removed unnecessary protos * Removed user class * Removed unnecessary class * Removed unnecessary deps * Added back config files * Fixed formatting * added index.html * Added license files * Fixed formatting * Fixed linting issues * Fixed formatting * Added protobuf as dependency * Added license * Ignored example dir for secret checking * Added changelog and readme * Updated package version * Implemented app check * Removed key * Revert "Removed key" This reverts commit 6a643f12ba1cf86f197904ff8803c8f98f89a612. * Removed key again * Added error catching implementation (#13163) * feat(dataconnect): Added error handling (#13175) * Added missing file * Removed unnecessary changes * Miscellaneous cleanup * Removed macros experiment * Removed .then statements * Removed debug prints * Addressed first pass of comments * Added movie_insert gql file * Added comments * Renamed package * Another package rename * Removed common package * Added missing files * Updated readme * Revert "Removed common package" This reverts commit 422bd80af0d603c9e756df40a52a5a43d174ce76. * Various improvements * Made changes per api proposal * Fixed initialization issues with generated SDK * Made changes according to api council review * Addressed Comments. Removed references to personal firebase projects Moved mapped host logic to new firebase_common package * Added missing license header to files * /s/query/operation * API Council Changes * Included list movies changes * API council updates * Addressed comments * Removed print * Added license header * Removed files that should be gitignored * Removed requirement on firebase_options * Added missing line * Removed windows and linux folders, updated changelog and gitignore * Updated version of firebase_data_connect * test(fdc): add e2e tests to the FDC package * add e2e tests to CI * test generated content * update * test * add postgres * more generation tests * update podfile * fix analyze * regenerate with latest version * add more queries * add more queries? * removing not generated commands * more unit tests * timestamp * licence * analyze * add dev dep * update generated * ignore generated for licence * add .firebaserc for dataconnect setup * fix ci * fix setup * test * more testing * improve * unset PGSERVICE * mote testing * okay? * maybe this? * update pubignore * update * ?? * hop * more * hophophop * more * web * tests * test * merge clean * regenerate * fix * fix * more tests * remove GoogleInfo Plist * fix options * test * fix emulator * clear text * wut? * fix generated code * public level * fix * fix generated * test * maybe * alors peut etre * tests done * update caching * e2e tests * tests done * fix * fix concurrency * fixes * fix * again * tests update * test --------- Co-authored-by: Maneesh Tewani Co-authored-by: Maneesh Tewani --- .github/workflows/e2e_tests_fdc.yaml | 230 +++++ .github/workflows/scripts/firebase.json | 10 +- melos.yaml | 2 + .../firebase_data_connect/example/.firebaserc | 12 + .../firebase_data_connect/example/.gitignore | 2 - .../firebase_data_connect/example/.pubignore | 1 + .../example/android/app/build.gradle | 3 - .../android/app/src/main/AndroidManifest.xml | 19 +- .../example/android/settings.gradle | 3 - .../.dataconnect/schema/main/input.gql | 2 + .../.dataconnect/schema/main/mutation.gql | 24 + .../.dataconnect/schema/main/query.gql | 6 +- .../.dataconnect/schema/main/relation.gql | 8 +- .../.dataconnect/schema/prelude.gql | 177 +++- .../dataconnect/connector/mutations.gql | 25 +- .../example/dataconnect/connector/queries.gql | 34 +- .../example/dataconnect/schema/schema.gql | 6 +- .../example/firebase.json | 7 +- .../example/integration_test/e2e_test.dart | 42 + .../integration_test/generation_e2e.dart | 70 ++ .../integration_test/instance_e2e.dart | 34 + .../example/integration_test/listen_e2e.dart | 78 ++ .../example/integration_test/query_e2e.dart | 123 +++ .../ios/Runner.xcodeproj/project.pbxproj | 4 - .../example/lib/firebase_options.dart | 98 +++ .../lib/generated/add_director_to_movie.dart | 8 +- .../example/lib/generated/add_person.dart | 6 +- .../example/lib/generated/create_movie.dart | 8 +- .../example/lib/generated/delete_movie.dart | 89 ++ .../example/lib/generated/list_movies.dart | 7 +- .../list_movies_by_partial_title.dart | 114 +++ .../example/lib/generated/list_persons.dart | 67 ++ .../example/lib/generated/movies.dart | 22 +- .../example/lib/login.dart | 33 +- .../example/lib/main.dart | 69 +- .../macos/Runner.xcodeproj/project.pbxproj | 4 - .../example/package-lock.json | 6 + .../example/pubspec.yaml | 76 +- .../example/start-firebase-emulator.sh | 3 + .../example/test/widget_test.dart | 34 - .../example/test_driver/integration_test.dart | 7 + .../lib/firebase_data_connect.dart | 3 +- .../lib/src/common/dataconnect_options.dart | 6 +- .../lib/src/core/ref.dart | 7 +- .../lib/src/firebase_data_connect.dart | 57 +- .../lib/src/network/rest_library.dart | 1 + .../lib/src/network/rest_transport.dart | 14 +- .../lib/src/optional.dart | 40 +- .../lib/src/timestamp.dart | 31 +- .../firebase_data_connect/pubspec.yaml | 7 +- .../test/src/common/common_library_test.dart | 130 +++ .../src/common/dataconnect_error_test.dart | 77 ++ .../src/common/dataconnect_options_test.dart | 80 ++ .../test/src/core/empty_serializer_test.dart | 27 + .../test/src/core/ref_test.dart | 87 ++ .../test/src/firebase_data_connect_test.dart | 175 ++++ .../src/firebase_data_connect_test.mocks.dart | 204 +++++ .../test/src/network/rest_transport_test.dart | 223 +++++ .../network/rest_transport_test.mocks.dart | 824 ++++++++++++++++++ .../test/src/network/transport_stub_test.dart | 87 ++ .../test/src/optional_test.dart | 91 ++ .../test/src/timestamp_test.dart | 64 ++ 62 files changed, 3445 insertions(+), 363 deletions(-) create mode 100644 .github/workflows/e2e_tests_fdc.yaml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/.firebaserc create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/.pubignore create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/integration_test/e2e_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/integration_test/generation_e2e.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/integration_test/instance_e2e.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/integration_test/listen_e2e.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/lib/firebase_options.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/lib/generated/delete_movie.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies_by_partial_title.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_persons.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/package-lock.json create mode 100755 packages/firebase_data_connect/firebase_data_connect/example/start-firebase-emulator.sh delete mode 100644 packages/firebase_data_connect/firebase_data_connect/example/test/widget_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/test_driver/integration_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_error_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_options_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test/src/core/empty_serializer_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test/src/core/ref_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test/src/network/transport_stub_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test/src/optional_test.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test/src/timestamp_test.dart diff --git a/.github/workflows/e2e_tests_fdc.yaml b/.github/workflows/e2e_tests_fdc.yaml new file mode 100644 index 000000000000..a756517226fb --- /dev/null +++ b/.github/workflows/e2e_tests_fdc.yaml @@ -0,0 +1,230 @@ +name: e2e-fdc + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'website/**' + - '**/example/**' + - '**/flutterfire_ui/**' + - '**.md' + push: + branches: + - master + paths-ignore: + - 'docs/**' + - 'website/**' + - '**/example/**' + - '**.md' + +jobs: + android: + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 + name: Install Node.js 20 + with: + node-version: '20' + - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 + with: + distribution: 'temurin' + java-version: '17' + - name: Firebase Emulator Cache + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + with: + path: ~/.cache/firebase/emulators + key: firebase-emulators-v3-fdc-${{ runner.os }} + restore-keys: firebase-emulators-v3 + - uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 + with: + channel: 'stable' + cache: true + - name: Setup PostgreSQL for Linux/macOS/Windows + uses: ikalnytskyi/action-setup-postgres@v6 + - uses: bluefireteam/melos-action@6085791af7036f6366c9a4b9d55105c0ef9c6388 + with: + run-bootstrap: false + melos-version: '5.3.0' + - name: 'Bootstrap package' + run: melos bootstrap --scope "firebase_data_connect*" + - name: 'Install Tools' + run: | + sudo npm i -g firebase-tools + - name: Start Firebase Emulator + run: | + cd ./packages/firebase_data_connect/firebase_data_connect/example + unset PGSERVICEFILE + firebase experiments:enable dataconnect + ./start-firebase-emulator.sh + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Gradle cache + uses: gradle/actions/setup-gradle@v3 + - name: AVD cache + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ runner.os }} + - name: Start AVD then run E2E tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 34 + target: google_apis + arch: x86_64 + working-directory: 'packages/firebase_data_connect/firebase_data_connect/example' + script: | + flutter test integration_test/e2e_test.dart --dart-define=CI=true -d emulator-5554 + + ios: + runs-on: macos-14 + timeout-minutes: 45 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 + name: Install Node.js 20 + with: + node-version: '20' + - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 + with: + distribution: 'temurin' + java-version: '17' + - name: Setup PostgreSQL for Linux/macOS/Windows + uses: ikalnytskyi/action-setup-postgres@v6 + - uses: hendrikmuhs/ccache-action@c92f40bee50034e84c763e33b317c77adaa81c92 + name: Xcode Compile Cache + with: + key: xcode-cache-${{ runner.os }} + max-size: 700M + - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + name: Pods Cache + id: pods-cache + with: + path: tests/ios/Pods + key: ${{ runner.os }}-fdc-pods-v3-${{ hashFiles('tests/ios/Podfile.lock') }} + restore-keys: ${{ runner.os }}-ios-pods-v2 + - name: Firebase Emulator Cache + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + with: + path: ~/.cache/firebase/emulators + key: firebase-emulators-v3-fdc-${{ runner.os }} + restore-keys: firebase-emulators-v3 + - uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 + with: + channel: 'stable' + cache: true + - uses: bluefireteam/melos-action@6085791af7036f6366c9a4b9d55105c0ef9c6388 + with: + run-bootstrap: false + melos-version: '5.3.0' + - name: 'Bootstrap package' + run: melos bootstrap --scope "firebase_data_connect*" + - name: 'Install Tools' + run: | + sudo npm i -g firebase-tools + - name: 'Build Application' + working-directory: 'packages/firebase_data_connect/firebase_data_connect/example' + run: | + export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" + export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros + export CCACHE_FILECLONE=true + export CCACHE_DEPEND=true + export CCACHE_INODECACHE=true + ccache -s + flutter build ios --no-codesign --simulator --debug --target=./integration_test/e2e_test.dart --dart-define=CI=true + ccache -s + - name: Start Firebase Emulator + run: | + sudo chown -R 501:20 "/Users/runner/.npm" + cd ./packages/firebase_data_connect/firebase_data_connect/example + unset PGSERVICEFILE + firebase experiments:enable dataconnect + ./start-firebase-emulator.sh + - name: 'E2E Tests' + working-directory: 'packages/firebase_data_connect/firebase_data_connect/example' + run: | + # Boot simulator and wait for System app to be ready. + # List of available simulators: https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md#installed-simulators + SIMULATOR="iPhone 15" + xcrun simctl bootstatus "$SIMULATOR" -b + xcrun simctl logverbose "$SIMULATOR" enable + # Sleep to allow simulator to settle. + sleep 15 + # Uncomment following line to have simulator logs printed out for debugging purposes. + # xcrun simctl spawn booted log stream --predicate 'eventMessage contains "flutter"' & + flutter test integration_test/e2e_test.dart -d "$SIMULATOR" --dart-define=CI=true + + web: + runs-on: macos-latest + timeout-minutes: 15 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 + name: Install Node.js 20 + with: + node-version: '20' + - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 + with: + distribution: 'temurin' + java-version: '17' + - name: Setup PostgreSQL for Linux/macOS/Windows + uses: ikalnytskyi/action-setup-postgres@v6 + - uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 + with: + channel: 'stable' + cache: true + - uses: bluefireteam/melos-action@6085791af7036f6366c9a4b9d55105c0ef9c6388 + with: + run-bootstrap: false + melos-version: '5.3.0' + - name: 'Bootstrap package' + run: melos bootstrap --scope "firebase_data_connect*" + - name: 'Install Tools' + run: sudo npm i -g firebase-tools + - name: Cache Firebase Emulator + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + with: + path: ~/.cache/firebase/emulators + key: firebase-emulators-v3-fdc-${{ runner.os }} + restore-keys: firebase-emulators-v3 + - name: Start Firebase Emulator + run: | + sudo chown -R 501:20 "/Users/runner/.npm" + cd ./packages/firebase_data_connect/firebase_data_connect/example + unset PGSERVICEFILE + firebase experiments:enable dataconnect + ./start-firebase-emulator.sh + - name: 'E2E Tests' + working-directory: 'packages/firebase_data_connect/firebase_data_connect/example' + # Web devices are not supported for the `flutter test` command yet. As a + # workaround we can use the `flutter drive` command. Tracking issue: + # https://github.com/flutter/flutter/issues/66264 + run: | + chromedriver --port=4444 --trace-buffer-size=100000 & + flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome --dart-define=CI=true | tee output.log + # We have to check the output for failed tests matching the string "[E]" + output=$( + android:icon="@mipmap/ic_launcher" + android:usesCleartextTraffic="true"> + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> - - + + - - + + - + \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/settings.gradle b/packages/firebase_data_connect/firebase_data_connect/example/android/settings.gradle index 7fb86d70412c..536165d35a42 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/android/settings.gradle +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/settings.gradle @@ -19,9 +19,6 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.0" apply false - // START: FlutterFire Configuration - id "com.google.gms.google-services" version "4.3.15" apply false - // END: FlutterFire Configuration id "org.jetbrains.kotlin.android" version "1.7.10" apply false } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/input.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/input.gql index e9da28933899..5fa1104b564e 100755 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/input.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/input.gql @@ -25,6 +25,8 @@ input DirectedBy_ListFilter { input DirectedBy_Order { movieId: OrderDirection directedbyId: OrderDirection + directedby: Person_Order + movie: Movie_Order } input Movie_Data { id: UUID diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/mutation.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/mutation.gql index 0feaae08b761..4daa8a6f0c64 100755 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/mutation.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/mutation.gql @@ -12,6 +12,18 @@ extend type Mutation { """ person_insert(data: Person_Data!): Person_Key! @fdc_generated(from: "Person", purpose: INSERT_SINGLE) """ + Insert DirectedBy entries into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + directedBy_insertMany(data: [DirectedBy_Data!]!): [DirectedBy_Key!]! @fdc_generated(from: "DirectedBy", purpose: INSERT_MULTIPLE) + """ + Insert Movie entries into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + movie_insertMany(data: [Movie_Data!]!): [Movie_Key!]! @fdc_generated(from: "Movie", purpose: INSERT_MULTIPLE) + """ + Insert Person entries into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + person_insertMany(data: [Person_Data!]!): [Person_Key!]! @fdc_generated(from: "Person", purpose: INSERT_MULTIPLE) + """ Insert or update a single DirectedBy into the table, based on the primary key. Returns the key of the newly inserted DirectedBy. """ directedBy_upsert(data: DirectedBy_Data!): DirectedBy_Key! @fdc_generated(from: "DirectedBy", purpose: UPSERT_SINGLE) @@ -24,6 +36,18 @@ extend type Mutation { """ person_upsert(data: Person_Data!): Person_Key! @fdc_generated(from: "Person", purpose: UPSERT_SINGLE) """ + Insert or update DirectedBy entries into the table, based on the primary key. Returns the key of the newly inserted DirectedBy. + """ + directedBy_upsertMany(data: [DirectedBy_Data!]): [DirectedBy_Key!]! @fdc_generated(from: "DirectedBy", purpose: UPSERT_MULTIPLE) + """ + Insert or update Movie entries into the table, based on the primary key. Returns the key of the newly inserted Movie. + """ + movie_upsertMany(data: [Movie_Data!]): [Movie_Key!]! @fdc_generated(from: "Movie", purpose: UPSERT_MULTIPLE) + """ + Insert or update Person entries into the table, based on the primary key. Returns the key of the newly inserted Person. + """ + person_upsertMany(data: [Person_Data!]): [Person_Key!]! @fdc_generated(from: "Person", purpose: UPSERT_MULTIPLE) + """ Update a single DirectedBy based on `id` or `key`, setting columns specified in `data`. Returns `null` if not found. """ directedBy_update(key: DirectedBy_Key, data: DirectedBy_Data!): DirectedBy_Key @fdc_generated(from: "DirectedBy", purpose: UPDATE_SINGLE) diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/query.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/query.gql index a1d2505e4111..db0f2136ff69 100755 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/query.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/query.gql @@ -14,13 +14,13 @@ extend type Query { """ List DirectedBy entries in the table, optionally filtered by `where` conditions. """ - directedBies(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE) + directedBies(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], offset: Int, limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE) """ List Movie entries in the table, optionally filtered by `where` conditions. """ - movies(where: Movie_Filter, orderBy: [Movie_Order!], limit: Int = 100): [Movie!]! @fdc_generated(from: "Movie", purpose: QUERY_MULTIPLE) + movies(where: Movie_Filter, orderBy: [Movie_Order!], offset: Int, limit: Int = 100): [Movie!]! @fdc_generated(from: "Movie", purpose: QUERY_MULTIPLE) """ List Person entries in the table, optionally filtered by `where` conditions. """ - people(where: Person_Filter, orderBy: [Person_Order!], limit: Int = 100): [Person!]! @fdc_generated(from: "Person", purpose: QUERY_MULTIPLE) + people(where: Person_Filter, orderBy: [Person_Order!], offset: Int, limit: Int = 100): [Person!]! @fdc_generated(from: "Person", purpose: QUERY_MULTIPLE) } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/relation.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/relation.gql index 0691f598203e..0f8049ef0be2 100755 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/relation.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/relation.gql @@ -2,19 +2,19 @@ extend type Movie { """ ✨ List DirectedBy entries in a one-to-many relationship with this object (i.e. where `DirectedBy.movie` equals this object). """ - directedBies_on_movie(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy.movie", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + directedBies_on_movie(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], offset: Int, limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy.movie", purpose: QUERY_MULTIPLE_ONE_TO_MANY) """ ✨ List related Person entries using DirectedBy as a join table (i.e. where an entry of DirectedBy exists whose `movie` == this and `directedby` == that). """ - people_via_DirectedBy(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [Person!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE_MANY_TO_MANY) + people_via_DirectedBy(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], offset: Int, limit: Int = 100): [Person!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE_MANY_TO_MANY) } extend type Person { """ ✨ List DirectedBy entries in a one-to-many relationship with this object (i.e. where `DirectedBy.directedby` equals this object). """ - directedBies_on_directedby(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy.directedby", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + directedBies_on_directedby(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], offset: Int, limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy.directedby", purpose: QUERY_MULTIPLE_ONE_TO_MANY) """ ✨ List related Movie entries using DirectedBy as a join table (i.e. where an entry of DirectedBy exists whose `directedby` == this and `movie` == that). """ - movies_via_DirectedBy(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [Movie!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE_MANY_TO_MANY) + movies_via_DirectedBy(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], offset: Int, limit: Int = 100): [Movie!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE_MANY_TO_MANY) } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/prelude.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/prelude.gql index a313edf0cca7..c75ac521add7 100755 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/prelude.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/prelude.gql @@ -46,22 +46,55 @@ directive @auth( """ expr: Boolean_Expr @fdc_oneOf(required: true) ) on QUERY | MUTATION -"Conditions on a string value" +"Query filter criteria for `String` scalar fields." input String_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: String @fdc_oneOf(group: "eq") + """ + Match if field is exactly equal to the result of the provided server value + expression. Currently only `auth.uid` is supported as an expression. + """ eq_expr: String_Expr @fdc_oneOf(group: "eq") + "Match if field is not equal to provided value." ne: String @fdc_oneOf(group: "ne") + """ + Match if field is not equal to the result of the provided server value + expression. Currently only `auth.uid` is supported as an expression. + """ ne_expr: String_Expr @fdc_oneOf(group: "ne") + "Match if field value is among the provided list of values." in: [String!] + "Match if field value is not among the provided list of values." nin: [String!] + "Match if field value is greater than the provided value." gt: String + "Match if field value is greater than or equal to the provided value." ge: String + "Match if field value is less than the provided value." lt: String + "Match if field value is less than or equal to the provided value." le: String + """ + Match if field value contains the provided value as a substring. Equivalent + to `LIKE '%value%'` + """ contains: String + """ + Match if field value starts with the provided value. Equivalent to + `LIKE 'value%'` + """ startsWith: String + """ + Match if field value ends with the provided value. Equivalent to + `LIKE '%value'` + """ endsWith: String + """ + Match if field value matches the provided pattern. See `String_Pattern` for + more details. + """ pattern: String_Pattern } @@ -70,135 +103,205 @@ The pattern match condition on a string. Specify either like or regex. https://www.postgresql.org/docs/current/functions-matching.html """ input String_Pattern { - "the LIKE expression to use" + "Match using the provided `LIKE` expression." like: String - "the POSIX regular expression" + "Match using the provided POSIX regular expression." regex: String - "when true, it's case-insensitive. In Postgres: ILIKE, ~*" + "When true, ignore case when matching." ignoreCase: Boolean - "when true, invert the condition. In Postgres: NOT LIKE, !~" + "When true, invert the match result. Equivalent to `NOT LIKE` or `!~`." invert: Boolean } -"Conditions on a string list" +"Query filter criteris for `[String!]` scalar fields." input String_ListFilter { + "Match if list field contains the provided value as a member." includes: String + "Match if list field does not contain the provided value as a member." excludes: String + "Match if list field contains all of the provided values as members." includesAll: [String!] + "Match if list field does not contain any of the provided values as members." excludesAll: [String!] } -"Conditions on a UUID value" +"Query filter criteria for `UUID` scalar fields." input UUID_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: UUID + "Match if field is not equal to provided value." ne: UUID + "Match if field value is among the provided list of values." in: [UUID!] + "Match if field value is not among the provided list of values." nin: [UUID!] } -"Conditions on a UUID list" +"Query filter criteris for `[UUID!]` scalar fields." input UUID_ListFilter { + "Match if list field contains the provided value as a member." includes: UUID + "Match if list field does not contain the provided value as a member." excludes: UUID + "Match if list field contains all of the provided values as members." includesAll: [UUID!] + "Match if list field does not contain any of the provided values as members." excludesAll: [UUID!] } -"Conditions on an Int value" +"Query filter criteria for `Int` scalar fields." input Int_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: Int + "Match if field is not equal to provided value." ne: Int + "Match if field value is among the provided list of values." in: [Int!] + "Match if field value is not among the provided list of values." nin: [Int!] + "Match if field value is greater than the provided value." gt: Int + "Match if field value is greater than or equal to the provided value." ge: Int + "Match if field value is less than the provided value." lt: Int + "Match if field value is less than or equal to the provided value." le: Int } -"Conditions on an Int list" +"Query filter criteris for `[Int!]` scalar fields." input Int_ListFilter { + "Match if list field contains the provided value as a member." includes: Int + "Match if list field does not contain the provided value as a member." excludes: Int + "Match if list field contains all of the provided values as members." includesAll: [Int!] + "Match if list field does not contain any of the provided values as members." excludesAll: [Int!] } -"Conditions on an Int64 value" +"Query filter criteria for `Int64` scalar fields." input Int64_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: Int64 + "Match if field is not equal to provided value." ne: Int64 + "Match if field value is among the provided list of values." in: [Int64!] + "Match if field value is not among the provided list of values." nin: [Int64!] + "Match if field value is greater than the provided value." gt: Int64 + "Match if field value is greater than or equal to the provided value." ge: Int64 + "Match if field value is less than the provided value." lt: Int64 + "Match if field value is less than or equal to the provided value." le: Int64 } -"Conditions on an Int64 list" +"Query filter criteria for `[Int64!]` scalar fields." input Int64_ListFilter { + "Match if list field contains the provided value as a member." includes: Int64 + "Match if list field does not contain the provided value as a member." excludes: Int64 + "Match if list field contains all of the provided values as members." includesAll: [Int64!] + "Match if list field does not contain any of the provided values as members." excludesAll: [Int64!] } -"Conditions on a Float value" +"Query filter criteria for `Float` scalar fields." input Float_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: Float + "Match if field is not equal to provided value." ne: Float + "Match if field value is among the provided list of values." in: [Float!] + "Match if field value is not among the provided list of values." nin: [Float!] + "Match if field value is greater than the provided value." gt: Float + "Match if field value is greater than or equal to the provided value." ge: Float + "Match if field value is less than the provided value." lt: Float + "Match if field value is less than or equal to the provided value." le: Float } -"Conditions on a Float list" +"Query filter criteria for `[Float!]` scalar fields." input Float_ListFilter { + "Match if list field contains the provided value as a member." includes: Float + "Match if list field does not contain the provided value as a member." excludes: Float + "Match if list field contains all of the provided values as members." includesAll: [Float!] + "Match if list field does not contain any of the provided values as members." excludesAll: [Float!] } -"Conditions on a Boolean value" +"Query filter criteria for `Boolean` scalar fields." input Boolean_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: Boolean + "Match if field is not equal to provided value." ne: Boolean + "Match if field value is among the provided list of values." in: [Boolean!] + "Match if field value is not among the provided list of values." nin: [Boolean!] } -"Conditions on a Boolean list" +"Query filter criteria for `[Boolean!]` scalar fields." input Boolean_ListFilter { + "Match if list field contains the provided value as a member." includes: Boolean + "Match if list field does not contain the provided value as a member." excludes: Boolean + "Match if list field contains all of the provided values as members." includesAll: [Boolean!] + "Match if list field does not contain any of the provided values as members." excludesAll: [Boolean!] } -"Conditions on an Any value" +"Query filter criteria for `Any` scalar fields." input Any_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: Any + "Match if field is not equal to provided value." ne: Any + "Match if field value is among the provided list of values." in: [Any!] + "Match if field value is not among the provided list of values." nin: [Any!] } -"Conditions on a Any list" +"Query filter criteria for `[Any!]` scalar fields." input Any_ListFilter { + "Match if list field contains the provided value as a member." includes: Any + "Match if list field does not contain the provided value as a member." excludes: Any + "Match if list field contains all of the provided values as members." includesAll: [Any!] + "Match if list field does not contain any of the provided values as members." excludesAll: [Any!] } """ @@ -278,13 +381,30 @@ directive @index( """ fields: [String!] """ - Only allowed when used on OBJECT. + Only allowed when used on OBJECT and BTREE index. Index order of each column. Default to all ASC. """ order: [IndexFieldOrder!] + """ + For array field, default to `GIN`. + For Vector field, default to `HNSW`. + """ + type: IndexType + """ + Only allowed when used on vector field. + The vector similarity method. Default to `INNER_PRODUCT`. + """ + vector_method: VectorSimilarityMethod ) repeatable on FIELD_DEFINITION | OBJECT enum IndexFieldOrder { ASC DESC } + +enum IndexType { + BTREE + GIN + HNSW + IVFFLAT +} type Query { _service: _Service! } @@ -860,6 +980,25 @@ enum Date_Interval @fdc_forbiddenAsFieldType { MONTH YEAR } +""" +Defines a unique constraint. + +Given `type TableName @table @unique(fields: [“fieldName”, “secondFieldName”])`, +`table_name_field_name_second_field_name_uidx` is the SQL unique index id. +Given `type TableName @table { fieldName: Int @unique } ` +`table_name_field_name_uidx` is the SQL unique index id. + +Override with `@unique(indexName)` in case of index name conflicts. +""" +directive @unique( + "The SQL database unique index name. Defaults to __uidx." + indexName: String + """ + Only allowed and required when used on OBJECT. + The fields to create a unique constraint on. + """ + fields: [String!] +) repeatable on FIELD_DEFINITION | OBJECT "Update input of a String value" input String_Update { set: String @fdc_oneOf(group: "set") diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql index a1d3e11e2137..a869f6fea236 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql @@ -1,14 +1,10 @@ # Create a movie based on user input mutation addPerson($name: String) @auth(level: PUBLIC) { - person_insert(data: { - name: $name - }) + person_insert(data: { name: $name }) } -mutation addDirectorToMovie($personId: Person_Key, $movieId: UUID) { - directedBy_insert(data: { - directedby: $personId, - movieId: $movieId - }) +mutation addDirectorToMovie($personId: Person_Key, $movieId: UUID) +@auth(level: PUBLIC) { + directedBy_insert(data: { directedby: $personId, movieId: $movieId }) } mutation createMovie( $title: String! @@ -24,11 +20,15 @@ mutation createMovie( genre: $genre rating: $rating description: $description - } ) } +# Delete a movie by its ID +mutation deleteMovie($id: UUID!) @auth(level: PUBLIC) { + movie_delete(id: $id) +} + # # Update movie information based on the provided ID # mutation updateMovie( # $id: UUID! @@ -54,11 +54,6 @@ mutation createMovie( # ) # } -# # Delete a movie by its ID -# mutation deleteMovie($id: UUID!) { -# movie_delete(id: $id) -# } - # # Delete movies with a rating lower than the specified minimum rating # mutation deleteUnpopularMovies($minRating: Float!) { # movie_deleteMany(where: { rating: { le: $minRating } }) @@ -118,4 +113,4 @@ mutation createMovie( # id_expr: "auth.uid", # username: $username # }) -# } \ No newline at end of file +# } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql index d3f265b1a2d9..25787ca9aa33 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql @@ -1,15 +1,30 @@ - # List subset of fields for movies query ListMovies @auth(level: USER) { movies { id - title, + title directed_by: people_via_DirectedBy { - name, + name } - }, + } } +# List movies by partial title match +query ListMoviesByPartialTitle($input: String!) @auth(level: PUBLIC) { + movies(where: { title: { contains: $input } }) { + id + title + genre + rating + } +} + +query ListPersons @auth(level: USER) { + people { + id + name + } +} # List subset of fields for users # query ListUsers @auth(level: PUBLIC) { @@ -215,17 +230,6 @@ query ListMovies @auth(level: USER) { # } # } -# # List movies by partial title match -# query ListMoviesByPartialTitle($input: String!) @auth(level: PUBLIC) { -# movies(where: { title: { contains: $input } }) { -# id -# title -# genre -# rating -# imageUrl -# } -# } - # # Fetch a single movie using key scalars (same as get movie by id) # query MovieByKey($key: Movie_Key!) @auth(level: PUBLIC) { # movie(key: $key) { diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/schema/schema.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/schema/schema.gql index 605bb1a624d7..8fa70b4ec39d 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/schema/schema.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/schema/schema.gql @@ -48,7 +48,7 @@ type Movie @table { # # Movie - Actors (or vice versa) is a many to many relationship # type Actor @table { # id: UUID! -# imageUrl: String! +# imageUrl: String! # name: String! @col(name: "name", dataType: "varchar(30)") # biography: String # } @@ -77,7 +77,7 @@ type Movie @table { # id: String! @col(name: "user_auth") # username: String! @col(name: "username", dataType: "varchar(50)") # # The following are generated from the @ref in the Review table -# # reviews_on_user +# # reviews_on_user # # movies_via_Review # } @@ -111,4 +111,4 @@ type Movie @table { # rating: Int # reviewText: String # reviewDate: Date! @default(expr: "request.time") -# } \ No newline at end of file +# } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/firebase.json b/packages/firebase_data_connect/firebase_data_connect/example/firebase.json index 73f599717a9c..2d00ab15a830 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/firebase.json +++ b/packages/firebase_data_connect/firebase_data_connect/example/firebase.json @@ -1,5 +1,10 @@ { "dataconnect": { "source": "dataconnect" + }, + "emulators": { + "auth": { + "port": 9099 + } } -} +} \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/e2e_test.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/e2e_test.dart new file mode 100644 index 000000000000..1ea81e9e0aa3 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/e2e_test.dart @@ -0,0 +1,42 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect_example/firebase_options.dart'; +import 'package:firebase_data_connect_example/generated/movies.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'generation_e2e.dart'; +import 'instance_e2e.dart'; +import 'listen_e2e.dart'; +import 'query_e2e.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('firebase_data_connect', () { + setUpAll(() async { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + final connector = MoviesConnector.connectorConfig; + + FirebaseDataConnect.instanceFor(connectorConfig: connector) + .useDataConnectEmulator('localhost', 9399); + await FirebaseAuth.instance.useAuthEmulator('localhost', 9099); + + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: 'test@mail.com', password: 'password'); + }); + + runInstanceTests(); + runQueryTests(); + runGenerationTest(); + runListenTests(); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/generation_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/generation_e2e.dart new file mode 100644 index 000000000000..242acecf666d --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/generation_e2e.dart @@ -0,0 +1,70 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect_example/generated/movies.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void runGenerationTest() { + group( + '$FirebaseDataConnect generation', + () { + late FirebaseDataConnect fdc; + + setUpAll(() async { + fdc = FirebaseDataConnect.instanceFor( + connectorConfig: MoviesConnector.connectorConfig, + ); + }); + + testWidgets('should have generated correct MoviesConnector', + (WidgetTester tester) async { + final connector = MoviesConnector(dataConnect: fdc); + expect(connector, isNotNull); + expect(connector.addPerson, isNotNull); + expect(connector.createMovie, isNotNull); + expect(connector.listMovies, isNotNull); + expect(connector.addDirectorToMovie, isNotNull); + }); + + testWidgets('should have generated correct MutationRef', + (WidgetTester tester) async { + final ref = MoviesConnector.instance.createMovie.ref( + genre: 'Action', + title: 'The Matrix', + releaseYear: 1999, + rating: 4.5, + ); + expect(ref, isNotNull); + expect(ref.execute, isNotNull); + }); + + testWidgets('should have generated correct QueryRef', + (WidgetTester tester) async { + final ref = MoviesConnector.instance.listMovies.ref(); + expect(ref, isNotNull); + expect(ref.execute, isNotNull); + }); + + testWidgets('should have generated correct MutationRef using name', + (WidgetTester tester) async { + final ref = MoviesConnector.instance.addPerson.ref( + name: 'Keanu Reeves', + ); + expect(ref, isNotNull); + expect(ref.execute, isNotNull); + }); + + testWidgets('should have generated correct MutationRef using nested id', + (WidgetTester tester) async { + final ref = MoviesConnector.instance.addDirectorToMovie.ref( + movieId: 'movieId', + personId: AddDirectorToMovieVariablesPersonId(id: 'personId'), + ); + expect(ref, isNotNull); + expect(ref.execute, isNotNull); + }); + }, + ); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/instance_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/instance_e2e.dart new file mode 100644 index 000000000000..475af5d79a5a --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/instance_e2e.dart @@ -0,0 +1,34 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect_example/generated/movies.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void runInstanceTests() { + group( + '$FirebaseDataConnect.instance', + () { + late FirebaseDataConnect fdc; + late FirebaseApp app; + + setUpAll(() async { + app = Firebase.app(); + fdc = FirebaseDataConnect.instanceFor( + app: app, + connectorConfig: MoviesConnector.connectorConfig, + ); + }); + + testWidgets('can instantiate', (WidgetTester tester) async { + expect(fdc, isNotNull); + }); + + testWidgets('can access app', (WidgetTester tester) async { + expect(fdc.app == app, isTrue); + }); + }, + ); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/listen_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/listen_e2e.dart new file mode 100644 index 000000000000..a41c3668d525 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/listen_e2e.dart @@ -0,0 +1,78 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect_example/generated/movies.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'query_e2e.dart'; + +void runListenTests() { + group( + '$FirebaseDataConnect.instance listen', + () { + setUp(() async { + await deleteAllMovies(); + }); + + testWidgets('should be able to listen to the list of movies', + (WidgetTester tester) async { + final initialValue = + await MoviesConnector.instance.listMovies.ref().execute(); + expect(initialValue.data.movies.length, 0, + reason: 'Initial movie list should be empty'); + + final Completer isReady = Completer(); + final Completer hasBeenListened = Completer(); + int count = 0; + + final listener = MoviesConnector.instance.listMovies + .ref() + .subscribe() + .listen((value) { + final movies = value.data.movies; + + if (count == 0) { + expect(movies.length, 0, + reason: 'First emission should contain an empty list'); + isReady.complete(); + } else { + expect(movies.length, 1, + reason: 'Second emission should contain one movie'); + expect(movies[0].title, 'The Matrix', + reason: 'The movie should be The Matrix'); + hasBeenListened.complete(true); + } + count++; + }); + + // Wait for the listener to be ready + await isReady.future; + + // Create the movie + await MoviesConnector.instance.createMovie + .ref( + genre: 'Action', + title: 'The Matrix', + releaseYear: 1999, + rating: 4.5, + ) + .execute(); + + await MoviesConnector.instance.listMovies.ref().execute(); + + // Wait for the listener to receive the movie update + final bool hasListenerReceived = await hasBeenListened.future; + + // Cancel the listener and wait for it to finish + await listener.cancel(); + + expect(hasListenerReceived, isTrue, + reason: 'The stream should have emitted new data'); + }); + }, + ); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart new file mode 100644 index 000000000000..55fd6e78de11 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart @@ -0,0 +1,123 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect_example/generated/movies.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future deleteAllMovies() async { + final value = await MoviesConnector.instance.listMovies.ref().execute(); + final result = value.data; + for (var movie in result.movies) { + await MoviesConnector.instance.deleteMovie.ref(id: movie.id).execute(); + } +} + +void runQueryTests() { + group( + '$FirebaseDataConnect.instance query', + () { + setUp(() async { + await deleteAllMovies(); + }); + + testWidgets('can query', (WidgetTester tester) async { + final value = await MoviesConnector.instance.listMovies.ref().execute(); + + final result = value.data; + expect(result.movies.length, 0); + }); + + testWidgets('can add a movie', (WidgetTester tester) async { + MutationRef ref = MoviesConnector.instance.createMovie.ref( + genre: 'Action', + title: 'The Matrix', + releaseYear: 1999, + rating: 4.5, + ); + + await ref.execute(); + + final value = await MoviesConnector.instance.listMovies.ref().execute(); + final result = value.data; + expect(result.movies.length, 1); + expect(result.movies[0].title, 'The Matrix'); + }); + + testWidgets('can add a director to a movie', (WidgetTester tester) async { + MutationRef ref = MoviesConnector.instance.addPerson.ref( + name: 'Keanu Reeves', + ); + + await ref.execute(); + + final personId = + (await MoviesConnector.instance.listPersons.ref().execute()) + .data + .people[0] + .id; + + final value = await MoviesConnector.instance.listMovies.ref().execute(); + final result = value.data; + expect(result.movies.length, 0); + + ref = MoviesConnector.instance.createMovie.ref( + genre: 'Action', + title: 'The Matrix', + releaseYear: 1999, + rating: 4.5, + ); + + await ref.execute(); + + final value2 = + await MoviesConnector.instance.listMovies.ref().execute(); + final result2 = value2.data; + expect(result2.movies.length, 1); + + final movieId = result2.movies[0].id; + + ref = MoviesConnector.instance.addDirectorToMovie.ref( + movieId: movieId, + personId: AddDirectorToMovieVariablesPersonId(id: personId), + ); + + await ref.execute(); + + final value3 = + await MoviesConnector.instance.listMovies.ref().execute(); + final result3 = value3.data; + expect(result3.movies.length, 1); + expect(result3.movies[0].directed_by.length, 1); + expect(result3.movies[0].directed_by[0].name, 'Keanu Reeves'); + }); + + testWidgets('can delete a movie', (WidgetTester tester) async { + MutationRef ref = MoviesConnector.instance.createMovie.ref( + genre: 'Action', + title: 'The Matrix', + releaseYear: 1999, + rating: 4.5, + ); + + await ref.execute(); + + final value = await MoviesConnector.instance.listMovies.ref().execute(); + final result = value.data; + expect(result.movies.length, 1); + + final movieId = result.movies[0].id; + + ref = MoviesConnector.instance.deleteMovie.ref(id: movieId); + + await ref.execute(); + + final value2 = + await MoviesConnector.instance.listMovies.ref().execute(); + final result2 = value2.data; + expect(result2.movies.length, 0); + }); + }, + ); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.pbxproj index fa521e1c50ab..26f10ca2a748 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 44748ABD01BE3A9752CF3BFC /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4B076066C0A79965E90ABC43 /* GoogleService-Info.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -50,7 +49,6 @@ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 467C8E0E18669B1A171B55D8 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 4B076066C0A79965E90ABC43 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 5363A146E4B954495368B56D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -115,7 +113,6 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, - 4B076066C0A79965E90ABC43 /* GoogleService-Info.plist */, EE2E4CC9AAC39360CE2CCED5 /* Pods */, BDBC9B622C75229E8F7832D1 /* Frameworks */, ); @@ -267,7 +264,6 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - 44748ABD01BE3A9752CF3BFC /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/firebase_options.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/firebase_options.dart new file mode 100644 index 000000000000..daecca3d327c --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/firebase_options.dart @@ -0,0 +1,98 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + return android; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyB7wZb2tO1-Fs6GbDADUSTs2Qs3w08Hovw', + appId: '1:406099696497:web:87e25e51afe982cd3574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + measurementId: 'G-JN95N1JV2E', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw', + appId: '1:406099696497:android:175ea7a64b2faf5e3574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c', + appId: '1:406099696497:ios:0670bc5fe8574a9c3574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + androidClientId: + '406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com', + iosClientId: + '406099696497-l9gojfp6b3h1cgie1se28a9ol9fmsvvk.apps.googleusercontent.com', + iosBundleId: 'io.flutter.plugins.firebase.firestore.example', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c', + appId: '1:406099696497:ios:0670bc5fe8574a9c3574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + androidClientId: + '406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com', + iosClientId: + '406099696497-l9gojfp6b3h1cgie1se28a9ol9fmsvvk.apps.googleusercontent.com', + iosBundleId: 'io.flutter.plugins.firebase.firestore.example', + ); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_director_to_movie.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_director_to_movie.dart index 31f9e0f435ee..0929d0a602b9 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_director_to_movie.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_director_to_movie.dart @@ -1,7 +1,3 @@ -// Copyright 2024, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - part of movies; class AddDirectorToMovie { @@ -128,8 +124,8 @@ class AddDirectorToMovieVariables { } AddDirectorToMovieVariables({ - this.personId, - this.movieId, + AddDirectorToMovieVariablesPersonId? this.personId, + String? this.movieId, }) { // TODO(mtewani): Only show this if there are optional fields. } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_person.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_person.dart index f71192de4435..9d418952fcd1 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_person.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_person.dart @@ -1,7 +1,3 @@ -// Copyright 2024, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - part of movies; class AddPerson { @@ -86,7 +82,7 @@ class AddPersonVariables { } AddPersonVariables({ - this.name, + String? this.name, }) { // TODO(mtewani): Only show this if there are optional fields. } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/create_movie.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/create_movie.dart index 481a519b686f..6d1e020cce3b 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/create_movie.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/create_movie.dart @@ -1,7 +1,3 @@ -// Copyright 2024, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - part of movies; class CreateMovie { @@ -121,8 +117,8 @@ class CreateMovieVariables { required this.title, required this.releaseYear, required this.genre, - this.rating, - this.description, + double? this.rating, + String? this.description, }) { // TODO(mtewani): Only show this if there are optional fields. } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/delete_movie.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/delete_movie.dart new file mode 100644 index 000000000000..5aeb6cdd1938 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/delete_movie.dart @@ -0,0 +1,89 @@ +part of movies; + +class DeleteMovie { + String name = "deleteMovie"; + DeleteMovie({required this.dataConnect}); + + Deserializer dataDeserializer = (String json) => + DeleteMovieResponse.fromJson(jsonDecode(json) as Map); + Serializer varsSerializer = + (DeleteMovieVariables vars) => jsonEncode(vars.toJson()); + MutationRef ref( + {required String id, DeleteMovieVariables? deleteMovieVariables}) { + DeleteMovieVariables vars1 = DeleteMovieVariables( + id: id, + ); + DeleteMovieVariables vars = deleteMovieVariables ?? vars1; + return dataConnect.mutation( + this.name, dataDeserializer, varsSerializer, vars); + } + + FirebaseDataConnect dataConnect; +} + +class DeleteMovieMovieDelete { + late String id; + + DeleteMovieMovieDelete.fromJson(Map json) + : id = json['id'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['id'] = id; + + return json; + } + + DeleteMovieMovieDelete({ + required this.id, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class DeleteMovieResponse { + late DeleteMovieMovieDelete? movie_delete; + + DeleteMovieResponse.fromJson(Map json) + : movie_delete = DeleteMovieMovieDelete.fromJson(json['movie_delete']) {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + if (movie_delete != null) { + json['movie_delete'] = movie_delete!.toJson(); + } + + return json; + } + + DeleteMovieResponse({ + DeleteMovieMovieDelete? movie_delete, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class DeleteMovieVariables { + late String id; + + DeleteMovieVariables.fromJson(Map json) : id = json['id'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['id'] = id; + + return json; + } + + DeleteMovieVariables({ + required this.id, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies.dart index e3b2086b8478..58e13493a23e 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies.dart @@ -1,7 +1,3 @@ -// Copyright 2024, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - part of movies; class ListMovies { @@ -12,8 +8,7 @@ class ListMovies { ListMoviesResponse.fromJson(jsonDecode(json) as Map); QueryRef ref() { - return dataConnect.query( - this.name, dataDeserializer, emptySerializer, null); + return dataConnect.query(this.name, dataDeserializer, null, null); } FirebaseDataConnect dataConnect; diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies_by_partial_title.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies_by_partial_title.dart new file mode 100644 index 000000000000..f06a7f68d304 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies_by_partial_title.dart @@ -0,0 +1,114 @@ +part of movies; + +class ListMoviesByPartialTitle { + String name = "ListMoviesByPartialTitle"; + ListMoviesByPartialTitle({required this.dataConnect}); + + Deserializer dataDeserializer = + (String json) => ListMoviesByPartialTitleResponse.fromJson( + jsonDecode(json) as Map); + Serializer varsSerializer = + (ListMoviesByPartialTitleVariables vars) => jsonEncode(vars.toJson()); + QueryRef + ref( + {required String input, + ListMoviesByPartialTitleVariables? + listMoviesByPartialTitleVariables}) { + ListMoviesByPartialTitleVariables vars1 = ListMoviesByPartialTitleVariables( + input: input, + ); + ListMoviesByPartialTitleVariables vars = + listMoviesByPartialTitleVariables ?? vars1; + return dataConnect.query(this.name, dataDeserializer, varsSerializer, vars); + } + + FirebaseDataConnect dataConnect; +} + +class ListMoviesByPartialTitleMovies { + late String id; + + late String title; + + late String genre; + + late double? rating; + + ListMoviesByPartialTitleMovies.fromJson(Map json) + : id = json['id'], + title = json['title'], + genre = json['genre'], + rating = json['rating'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['id'] = id; + + json['title'] = title; + + json['genre'] = genre; + + if (rating != null) { + json['rating'] = rating; + } + + return json; + } + + ListMoviesByPartialTitleMovies({ + required this.id, + required this.title, + required this.genre, + double? rating, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class ListMoviesByPartialTitleResponse { + late List movies; + + ListMoviesByPartialTitleResponse.fromJson(Map json) + : movies = (json['movies'] as List) + .map((e) => ListMoviesByPartialTitleMovies.fromJson(e)) + .toList() {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['movies'] = movies.map((e) => e.toJson()).toList(); + + return json; + } + + ListMoviesByPartialTitleResponse({ + required this.movies, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class ListMoviesByPartialTitleVariables { + late String input; + + ListMoviesByPartialTitleVariables.fromJson(Map json) + : input = json['input'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['input'] = input; + + return json; + } + + ListMoviesByPartialTitleVariables({ + required this.input, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_persons.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_persons.dart new file mode 100644 index 000000000000..8a4d7e4cb0a6 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_persons.dart @@ -0,0 +1,67 @@ +part of movies; + +class ListPersons { + String name = "ListPersons"; + ListPersons({required this.dataConnect}); + + Deserializer dataDeserializer = (String json) => + ListPersonsResponse.fromJson(jsonDecode(json) as Map); + + QueryRef ref() { + return dataConnect.query(this.name, dataDeserializer, null, null); + } + + FirebaseDataConnect dataConnect; +} + +class ListPersonsPeople { + late String id; + + late String name; + + ListPersonsPeople.fromJson(Map json) + : id = json['id'], + name = json['name'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['id'] = id; + + json['name'] = name; + + return json; + } + + ListPersonsPeople({ + required this.id, + required this.name, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class ListPersonsResponse { + late List people; + + ListPersonsResponse.fromJson(Map json) + : people = (json['people'] as List) + .map((e) => ListPersonsPeople.fromJson(e)) + .toList() {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['people'] = people.map((e) => e.toJson()).toList(); + + return json; + } + + ListPersonsResponse({ + required this.people, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart index bc93d11d5910..9c0f15b0ac39 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart @@ -1,7 +1,3 @@ -// Copyright 2024, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - library movies; import 'package:firebase_data_connect/firebase_data_connect.dart'; @@ -13,8 +9,14 @@ part 'add_director_to_movie.dart'; part 'create_movie.dart'; +part 'delete_movie.dart'; + part 'list_movies.dart'; +part 'list_movies_by_partial_title.dart'; + +part 'list_persons.dart'; + class MoviesConnector { AddPerson get addPerson { return AddPerson(dataConnect: dataConnect); @@ -28,10 +30,22 @@ class MoviesConnector { return CreateMovie(dataConnect: dataConnect); } + DeleteMovie get deleteMovie { + return DeleteMovie(dataConnect: dataConnect); + } + ListMovies get listMovies { return ListMovies(dataConnect: dataConnect); } + ListMoviesByPartialTitle get listMoviesByPartialTitle { + return ListMoviesByPartialTitle(dataConnect: dataConnect); + } + + ListPersons get listPersons { + return ListPersons(dataConnect: dataConnect); + } + static ConnectorConfig connectorConfig = ConnectorConfig( 'us-west2', 'movies', diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/login.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/login.dart index f88ae430deaf..5b5676e4f120 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/login.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/login.dart @@ -2,11 +2,11 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:math'; + import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_data_connect_example/main.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:google_sign_in/google_sign_in.dart'; class Login extends StatefulWidget { const Login({super.key}); @@ -16,32 +16,9 @@ class Login extends StatefulWidget { } class _LoginState extends State { - Future signInWithGoogle() async { - // Trigger the authentication flow - if (kIsWeb) { - GoogleAuthProvider googleProvider = GoogleAuthProvider(); - - googleProvider - .addScope('https://www.googleapis.com/auth/contacts.readonly'); - - // Once signed in, return the UserCredential - return await FirebaseAuth.instance.signInWithPopup(googleProvider); - } else { - final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn(); - - // Obtain the auth details from the request - final GoogleSignInAuthentication? googleAuth = - await googleUser?.authentication; - - // Create a new credential - final credential = GoogleAuthProvider.credential( - accessToken: googleAuth?.accessToken, - idToken: googleAuth?.idToken, - ); - - // Once signed in, return the UserCredential - return await FirebaseAuth.instance.signInWithCredential(credential); - } + Future signInWithGoogle() async { + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: '${Random().nextInt(100000)}@mail.com', password: 'password'); } void logIn() async { diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/main.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/main.dart index e44256df786c..10da1ce05f60 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/main.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/main.dart @@ -3,26 +3,24 @@ // BSD-style license that can be found in the LICENSE file. import 'package:firebase_app_check/firebase_app_check.dart'; -import 'package:firebase_data_connect_example/login.dart'; - -import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; // Uncomment this line after running flutterfire configure // import 'firebase_options.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart'; -import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_data_connect_example/firebase_options.dart'; +import 'package:firebase_data_connect_example/login.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'generated/movies.dart'; const appCheckEnabled = false; -const configureEmulator = false; - -// Required for web. Set equal to `DefaultFirebaseOptions.currentPlatform` -FirebaseOptions? options; +const configureEmulator = true; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(options: options); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); if (appCheckEnabled) { await FirebaseAppCheck.instance.activate( // You can also use a `ReCaptchaEnterpriseProvider` provider instance as an @@ -43,6 +41,15 @@ void main() async { appleProvider: AppleProvider.appAttest, ); } + if (configureEmulator) { + MoviesConnector.instance.dataConnect + .useDataConnectEmulator('127.0.0.1', 9399); + FirebaseAuth.instance.useAuthEmulator( + 'localhost', + 9099, + ); + } + runApp(const MyApp()); } @@ -62,12 +69,22 @@ class MyApp extends StatelessWidget { } } -class MyHomePage extends StatefulWidget { +class MyHomePage extends StatelessWidget { const MyHomePage({super.key, required this.title}); final String title; @override - State createState() => _MyHomePageState(); + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(title), + ), + body: const Center( + child: DataConnectWidget(), + ), + ); + } } class DataConnectWidget extends StatefulWidget { @@ -91,14 +108,10 @@ class _DataConnectWidgetState extends State { @override void initState() { super.initState(); - if (configureEmulator) { - int port = 9399; - MoviesConnector.instance.dataConnect - .useDataConnectEmulator('127.0.0.1', port); - } QueryRef ref = MoviesConnector.instance.listMovies.ref(); + ref.subscribe().listen((event) { setState(() { _movies = event.data.movies; @@ -173,10 +186,11 @@ class _DataConnectWidgetState extends State { } MutationRef ref = MoviesConnector.instance.createMovie.ref( - title: title, - releaseYear: _releaseYearDate.year, - genre: genre, - rating: _rating); + title: title, + releaseYear: _releaseYearDate.year, + genre: genre, + rating: _rating, + ); try { await ref.execute(); triggerReload(); @@ -243,18 +257,3 @@ class _DataConnectWidgetState extends State { ); } } - -class _MyHomePageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(widget.title), - ), - body: const Center( - child: DataConnectWidget(), - ), - ); - } -} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.pbxproj index 57bda15d2206..ac658b11a81e 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 6A09C092ECC09506AD892B52 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 93CDFB6B9272C6F1BE9AB929 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -78,7 +77,6 @@ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 93CDFB6B9272C6F1BE9AB929 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ @@ -127,7 +125,6 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - 93CDFB6B9272C6F1BE9AB929 /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -288,7 +285,6 @@ files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - 6A09C092ECC09506AD892B52 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/firebase_data_connect/firebase_data_connect/example/package-lock.json b/packages/firebase_data_connect/firebase_data_connect/example/package-lock.json new file mode 100644 index 000000000000..6e4aa92b1a0e --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "example", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml b/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml index 7f5c88cde257..fa7fcc9c1bc3 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml +++ b/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml @@ -1,44 +1,22 @@ name: firebase_data_connect_example -description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +description: 'Firebase Data Connect example app' + +publish_to: 'none' -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: '>=3.2.0 <4.0.0' -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter firebase_core: ^3.2.0 google_sign_in: ^6.1.0 firebase_auth: ^5.1.2 - firebase_data_connect: + firebase_data_connect: path: ../ - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 flutter_rating_bar: ^4.0.1 protobuf: ^3.1.0 @@ -50,51 +28,9 @@ dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^3.0.0 + integration_test: + sdk: flutter -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/firebase_data_connect/firebase_data_connect/example/start-firebase-emulator.sh b/packages/firebase_data_connect/firebase_data_connect/example/start-firebase-emulator.sh new file mode 100755 index 000000000000..c40ced9decb4 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/start-firebase-emulator.sh @@ -0,0 +1,3 @@ +#!/bin/bash +firebase emulators:start --project flutterfire-e2e-tests & +sleep 30 \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/example/test/widget_test.dart b/packages/firebase_data_connect/firebase_data_connect/example/test/widget_test.dart deleted file mode 100644 index 5ef64fc01675..000000000000 --- a/packages/firebase_data_connect/firebase_data_connect/example/test/widget_test.dart +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2024, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:firebase_data_connect_example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/test_driver/integration_test.dart b/packages/firebase_data_connect/firebase_data_connect/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..f1ac26f27b88 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart b/packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart index 63819ce1dfdf..3befd9588c65 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart @@ -10,6 +10,7 @@ import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; +import 'package:flutter/foundation.dart'; import 'src/common/common_library.dart'; import 'src/network/transport_library.dart' @@ -18,8 +19,8 @@ import 'src/network/transport_library.dart' export 'src/common/common_library.dart'; +part 'src/core/empty_serializer.dart'; part 'src/core/ref.dart'; part 'src/firebase_data_connect.dart'; part 'src/optional.dart'; part 'src/timestamp.dart'; -part 'src/core/empty_serializer.dart'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_options.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_options.dart index 3d23ef63432c..4f55b88cc784 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_options.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_options.dart @@ -21,9 +21,9 @@ class ConnectorConfig { /// String representation of connectorConfig String toJson() { return jsonEncode({ - location: location, - connector: connector, - serviceId: serviceId, + 'location': location, + 'connector': connector, + 'serviceId': serviceId, }); } } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart index 228e65f2df90..f9891bd43eb7 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart @@ -35,8 +35,9 @@ abstract class OperationRef { } /// Tracks currently active queries, and emits events when a new query is executed. -class _QueryManager { - _QueryManager(this.dataConnect); +@visibleForTesting +class QueryManager { + QueryManager(this.dataConnect); /// FirebaseDataConnect instance; FirebaseDataConnect dataConnect; @@ -97,7 +98,7 @@ class QueryRef extends OperationRef { : super(dataConnect, operationName, transport, deserializer, serializer, variables); - _QueryManager _queryManager; + QueryManager _queryManager; @override Future> execute() async { try { diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart index 47979a6ae22a..b68d2069298e 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart @@ -7,22 +7,23 @@ part of firebase_data_connect; /// DataConnect class class FirebaseDataConnect extends FirebasePluginPlatform { /// Constructor for initializing Data Connect - FirebaseDataConnect._({ + @visibleForTesting + FirebaseDataConnect({ required this.app, required this.connectorConfig, this.auth, this.appCheck, - }) : _options = DataConnectOptions( + }) : options = DataConnectOptions( app.options.projectId, connectorConfig.location, connectorConfig.connector, connectorConfig.serviceId), super(app.name, 'plugins.flutter.io/firebase_data_connect') { - _queryManager = _QueryManager(this); + _queryManager = QueryManager(this); } /// QueryManager manages ongoing queries, and their subscriptions. - late _QueryManager _queryManager; + late QueryManager _queryManager; /// FirebaseApp FirebaseApp app; @@ -38,30 +39,39 @@ class FirebaseDataConnect extends FirebasePluginPlatform { FirebaseAuth? auth; /// ConnectorConfig + projectId - DataConnectOptions _options; + @visibleForTesting + DataConnectOptions options; /// Data Connect specific config information ConnectorConfig connectorConfig; /// Custom transport options for connecting to the Data Connect service. - TransportOptions? _transportOptions; + @visibleForTesting + TransportOptions? transportOptions; /// Checks whether the transport has been properly initialized. - void _checkTransport() { - _transportOptions ??= + @visibleForTesting + void checkTransport() { + transportOptions ??= TransportOptions('firebasedataconnect.googleapis.com', null, true); - transport = getTransport(_transportOptions!, _options, auth, appCheck); + transport = getTransport(transportOptions!, options, auth, appCheck); } /// Returns a [QueryRef] object. QueryRef query( String operationName, Deserializer dataDeserializer, - Serializer varsSerializer, + Serializer? varsSerializer, Variables? vars) { - _checkTransport(); - return QueryRef(this, operationName, transport, - dataDeserializer, _queryManager, varsSerializer, vars); + checkTransport(); + return QueryRef( + this, + operationName, + transport, + dataDeserializer, + _queryManager, + varsSerializer ?? emptySerializer, + vars); } /// Returns a [MutationRef] object. @@ -70,7 +80,7 @@ class FirebaseDataConnect extends FirebasePluginPlatform { Deserializer dataDeserializer, Serializer varsSerializer, Variables? vars) { - _checkTransport(); + checkTransport(); return MutationRef( this, operationName, transport, dataDeserializer, varsSerializer, vars); } @@ -79,11 +89,12 @@ class FirebaseDataConnect extends FirebasePluginPlatform { void useDataConnectEmulator(String host, int port, {bool automaticHostMapping = true, bool isSecure = false}) { String mappedHost = automaticHostMapping ? getMappedHost(host) : host; - _transportOptions = TransportOptions(mappedHost, port, isSecure); + transportOptions = TransportOptions(mappedHost, port, isSecure); } /// Currently cached DataConnect instances. Maps from app name to . - static final Map> _cachedInstances = + @visibleForTesting + static final Map> cachedInstances = {}; /// Returns an instance using a specified [FirebaseApp]. @@ -100,20 +111,20 @@ class FirebaseDataConnect extends FirebasePluginPlatform { auth ??= FirebaseAuth.instanceFor(app: app); appCheck ??= FirebaseAppCheck.instanceFor(app: app); - if (_cachedInstances[app.name] != null && - _cachedInstances[app.name]![connectorConfig.toJson()] != null) { - return _cachedInstances[app.name]![connectorConfig.toJson()]!; + if (cachedInstances[app.name] != null && + cachedInstances[app.name]![connectorConfig.toJson()] != null) { + return cachedInstances[app.name]![connectorConfig.toJson()]!; } - FirebaseDataConnect newInstance = FirebaseDataConnect._( + FirebaseDataConnect newInstance = FirebaseDataConnect( app: app, auth: auth, appCheck: appCheck, connectorConfig: connectorConfig); - if (_cachedInstances[app.name] == null) { - _cachedInstances[app.name] = {}; + if (cachedInstances[app.name] == null) { + cachedInstances[app.name] = {}; } - _cachedInstances[app.name]![connectorConfig.toJson()] = newInstance; + cachedInstances[app.name]![connectorConfig.toJson()] = newInstance; return newInstance; } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart index 917da7c3a0ae..80b0923ed7b7 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart @@ -9,6 +9,7 @@ import 'dart:developer'; import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import '../common/common_library.dart'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart index 0a9599487cb1..1d1bffcf7446 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart @@ -19,7 +19,7 @@ class RestTransport implements DataConnectTransport { String location = options.location; String service = options.serviceId; String connector = options.connector; - _url = + url = '$protocol://$host:$port/v1alpha/projects/$project/locations/$location/services/$service/connectors/$connector'; } @@ -30,7 +30,15 @@ class RestTransport implements DataConnectTransport { FirebaseAppCheck? appCheck; /// Current endpoint URL. - late String _url; + @visibleForTesting + late String url; + + @visibleForTesting + setHttp(http.Client client) { + _client = client; + } + + http.Client _client = http.Client(); /// Current host configuration. @override @@ -84,7 +92,7 @@ class RestTransport implements DataConnectTransport { body['variables'] = json.decode(serializer(vars)); } try { - http.Response r = await http.post(Uri.parse('$_url:$endpoint'), + http.Response r = await _client.post(Uri.parse('$url:$endpoint'), body: json.encode(body), headers: headers); if (r.statusCode != 200) { Map bodyJson = diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/optional.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/optional.dart index a1298dd36d01..ee054b97aa0e 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/optional.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/optional.dart @@ -66,35 +66,21 @@ class Optional { } String nativeToJson(T type) { - switch (T.runtimeType) { - case bool: - case int: - case double: - case num: - return type.toString(); - case String: - return type as String; - default: - throw UnimplementedError( - 'This type is unimplemented: ${type.runtimeType}'); + if (type is bool || type is int || type is double || type is num) { + return type.toString(); + } else if (type is String) { + return type; + } else { + throw UnimplementedError('This type is unimplemented: ${type.runtimeType}'); } } T nativeFromJson(String json) { - switch (T.runtimeType) { - case bool: - return bool.parse(json) as T; - case int: - return int.parse(json) as T; - case double: - double.parse(json); - case num: - return num.parse(json) as T; - case String: - return json as T; - default: - throw UnimplementedError( - 'This type is unimplemented: ${json.runtimeType}'); - } - throw Exception('Null!'); + if (T == bool) return (json.toLowerCase() == 'true') as T; + if (T == int) return int.parse(json) as T; + if (T == double) return double.parse(json) as T; + if (T == num) return num.parse(json) as T; + if (T == String) return json as T; + + throw UnimplementedError('This type is unimplemented: ${T.runtimeType}'); } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/timestamp.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/timestamp.dart index fd3e954c83a4..d4d5343f2a67 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/timestamp.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/timestamp.dart @@ -6,18 +6,20 @@ part of firebase_data_connect; /// Timestamp class is a custom class that allows for storing of nanoseconds. class Timestamp { + // ignore: use_raw_strings + final regex = RegExp( + r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{0,9})?(Z|[+-]\d{2}:\d{2})$'); + /// Constructor Timestamp(this.nanoseconds, this.seconds); + // TODO(mtewani): Fix this so that it keeps track of positional arguments so you don't have to repeatedly search the string multiple times. Timestamp.fromJson(String date) { - // ignore: use_raw_strings - var regex = RegExp( - r'^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{0,9})?(Z|[+-]\\d{2}:\\d{2})\$'); if (!regex.hasMatch(date)) { throw Exception('Invalid Date provided!'); } DateTime dateTime = DateTime.parse(date); - seconds = dateTime.second; + seconds = dateTime.millisecondsSinceEpoch ~/ 1000; String nanoStr = ''; int dotIdx = date.indexOf('.'); if (dotIdx > -1) { @@ -32,28 +34,23 @@ class Timestamp { if (nanoStr.isNotEmpty) { nanoseconds = int.parse(nanoStr.padRight(9, '0')); } - // TODO(mtewani): Add offset values. - if (date.contains('Z')) { - return; - } - int addIdx = date.indexOf('+'); - bool isAdd = addIdx > -1; - int signIdx = isAdd ? addIdx : date.indexOf('-'); - int timeHour = int.parse(date.substring(signIdx + 1, signIdx + 3)); - int timeMin = int.parse(date.substring(signIdx + 4, signIdx + 6)); - int timeZoneDiffer = timeHour * 3600 + timeMin * 60; - seconds = seconds + (isAdd ? -timeZoneDiffer : timeZoneDiffer); } + String toJson() { String secondsStr = DateTime.fromMillisecondsSinceEpoch(seconds * 1000, isUtc: true) .toIso8601String(); + if (nanoseconds == 0) { + return secondsStr; + } String nanoStr = nanoseconds.toString().padRight(9, '0'); - return '${secondsStr.substring(0, nanoStr.length - 1)}.${nanoStr}Z'; + return '${secondsStr.substring(0, 19)}.${nanoStr}Z'; } DateTime toDateTime() { - return DateTime.utc((seconds * 1000 + (nanoseconds / 1000000)).floor()); + final string = toJson(); + final date = DateTime.parse(string); + return date; } /// Current nanoseconds diff --git a/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml b/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml index 165c44c9bc6f..c6807c19df0d 100644 --- a/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml +++ b/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml @@ -1,5 +1,5 @@ name: firebase_data_connect -description: "Firebase Data Connect" +description: 'Firebase Data Connect' version: 0.1.0 homepage: https://firebase.google.com/docs/data-connect/quickstart?platform=flutter false_secrets: @@ -8,7 +8,7 @@ false_secrets: environment: sdk: '>=3.2.0 <4.0.0' - flutter: ">=3.3.0" + flutter: '>=3.3.0' dependencies: firebase_app_check: ^0.3.0+3 @@ -22,8 +22,11 @@ dependencies: protobuf: ^3.1.0 dev_dependencies: + build_runner: ^2.4.12 flutter_lints: ^4.0.0 flutter_test: sdk: flutter mockito: ^5.0.0 plugin_platform_interface: ^2.1.3 + firebase_app_check_platform_interface: ^0.1.0+35 + firebase_auth_platform_interface: ^7.4.4 diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart new file mode 100644 index 000000000000..785d438a9b5f --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart @@ -0,0 +1,130 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +// Mock classes for Firebase dependencies +class MockFirebaseAuth extends Mock implements FirebaseAuth {} + +class MockFirebaseAppCheck extends Mock implements FirebaseAppCheck {} + +void main() { + group('TransportOptions', () { + test('should properly initialize with given parameters', () { + final transportOptions = TransportOptions('localhost', 8080, true); + + expect(transportOptions.host, 'localhost'); + expect(transportOptions.port, 8080); + expect(transportOptions.isSecure, true); + }); + + test('should allow null values for optional parameters', () { + final transportOptions = TransportOptions('localhost', null, null); + + expect(transportOptions.host, 'localhost'); + expect(transportOptions.port, null); + expect(transportOptions.isSecure, null); + }); + + test('should update properties correctly', () { + final transportOptions = TransportOptions('localhost', 8080, true); + + transportOptions.host = 'newhost'; + transportOptions.port = 9090; + transportOptions.isSecure = false; + + expect(transportOptions.host, 'newhost'); + expect(transportOptions.port, 9090); + expect(transportOptions.isSecure, false); + }); + }); + + group('DataConnectTransport', () { + late DataConnectTransport transport; + late TransportOptions transportOptions; + late DataConnectOptions dataConnectOptions; + late MockFirebaseAuth mockFirebaseAuth; + late MockFirebaseAppCheck mockFirebaseAppCheck; + + setUp(() { + transportOptions = TransportOptions('localhost', 8080, true); + dataConnectOptions = DataConnectOptions( + 'projectId', + 'location', + 'connector', + 'serviceId', + ); + mockFirebaseAuth = MockFirebaseAuth(); + mockFirebaseAppCheck = MockFirebaseAppCheck(); + + transport = TestDataConnectTransport( + transportOptions, + dataConnectOptions, + auth: mockFirebaseAuth, + appCheck: mockFirebaseAppCheck, + ); + }); + + test('should properly initialize with given parameters', () { + expect(transport.transportOptions.host, 'localhost'); + expect(transport.transportOptions.port, 8080); + expect(transport.transportOptions.isSecure, true); + }); + + test('should handle invokeQuery with proper deserializer', () async { + final queryName = 'testQuery'; + final deserializer = (json) => json; + final result = + await transport.invokeQuery(queryName, deserializer, null, null); + + expect(result, isNotNull); + }); + + test('should handle invokeMutation with proper deserializer', () async { + final queryName = 'testMutation'; + final deserializer = (json) => json; + final result = + await transport.invokeMutation(queryName, deserializer, null, null); + + expect(result, isNotNull); + }); + }); +} + +// Test class extending DataConnectTransport for testing purposes +class TestDataConnectTransport extends DataConnectTransport { + TestDataConnectTransport( + TransportOptions transportOptions, DataConnectOptions options, + {FirebaseAuth? auth, FirebaseAppCheck? appCheck}) + : super(transportOptions, options) { + this.auth = auth; + this.appCheck = appCheck; + } + + @override + Future invokeQuery( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + ) async { + // Simulate query invocation logic here + return deserializer('{}'); + } + + @override + Future invokeMutation( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + ) async { + // Simulate mutation invocation logic here + return deserializer('{}'); + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_error_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_error_test.dart new file mode 100644 index 000000000000..7892f72dbbfe --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_error_test.dart @@ -0,0 +1,77 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('DataConnectErrorCode', () { + test('should have the correct enum values', () { + expect(DataConnectErrorCode.unavailable.toString(), + 'DataConnectErrorCode.unavailable'); + expect(DataConnectErrorCode.unauthorized.toString(), + 'DataConnectErrorCode.unauthorized'); + expect( + DataConnectErrorCode.other.toString(), 'DataConnectErrorCode.other'); + }); + }); + + group('DataConnectError', () { + test('should initialize with correct error code and message', () { + final error = DataConnectError( + DataConnectErrorCode.unavailable, 'Service is unavailable'); + + expect(error.dataConnectErrorCode, DataConnectErrorCode.unavailable); + expect(error.plugin, 'Data Connect'); + expect(error.code, 'DataConnectErrorCode.unavailable'); + expect(error.message, 'Service is unavailable'); + }); + + test('should handle different error codes properly', () { + final unauthorizedError = DataConnectError( + DataConnectErrorCode.unauthorized, 'Unauthorized access'); + final otherError = DataConnectError( + DataConnectErrorCode.other, 'Unknown error occurred'); + + expect(unauthorizedError.dataConnectErrorCode, + DataConnectErrorCode.unauthorized); + expect(unauthorizedError.plugin, 'Data Connect'); + expect(unauthorizedError.code, 'DataConnectErrorCode.unauthorized'); + expect(unauthorizedError.message, 'Unauthorized access'); + + expect(otherError.dataConnectErrorCode, DataConnectErrorCode.other); + expect(otherError.plugin, 'Data Connect'); + expect(otherError.code, 'DataConnectErrorCode.other'); + expect(otherError.message, 'Unknown error occurred'); + }); + + test('should allow null message', () { + final error = DataConnectError(DataConnectErrorCode.unavailable, null); + + expect(error.message, null); + }); + }); + + group('Serializer and Deserializer', () { + test('should serialize variables into string format', () { + Serializer> serializer = + (Map vars) => vars.toString(); + + final inputVars = {'key1': 'value1', 'key2': 123}; + final serializedString = serializer(inputVars); + + expect(serializedString, "{key1: value1, key2: 123}"); + }); + + test('should deserialize string data into expected format', () { + Deserializer> deserializer = + (String data) => {'data': data}; + + final inputData = '{"message": "Hello World"}'; + final deserializedData = deserializer(inputData); + + expect(deserializedData, {'data': '{"message": "Hello World"}'}); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_options_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_options_test.dart new file mode 100644 index 000000000000..4c2932a00774 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_options_test.dart @@ -0,0 +1,80 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ConnectorConfig', () { + test('should initialize with correct parameters', () { + final config = ConnectorConfig('us-central1', 'cloud-sql', 'service-123'); + + expect(config.location, 'us-central1'); + expect(config.connector, 'cloud-sql'); + expect(config.serviceId, 'service-123'); + }); + + test('should return correct JSON representation', () { + final config = ConnectorConfig('us-central1', 'cloud-sql', 'service-123'); + + final jsonResult = config.toJson(); + final expectedJson = jsonEncode({ + 'location': 'us-central1', + 'connector': 'cloud-sql', + 'serviceId': 'service-123', + }); + + expect(jsonResult, expectedJson); + }); + + test('should handle empty string parameters in JSON', () { + final config = ConnectorConfig('', '', ''); + + final jsonResult = config.toJson(); + final expectedJson = jsonEncode({ + 'location': '', + 'connector': '', + 'serviceId': '', + }); + + expect(jsonResult, expectedJson); + }); + }); + + group('DataConnectOptions', () { + test( + 'should initialize with correct parameters and inherit from ConnectorConfig', + () { + final options = DataConnectOptions( + 'project-abc', 'us-central1', 'cloud-sql', 'service-123'); + + // Test inherited fields from ConnectorConfig + expect(options.location, 'us-central1'); + expect(options.connector, 'cloud-sql'); + expect(options.serviceId, 'service-123'); + + // Test new field specific to DataConnectOptions + expect(options.projectId, 'project-abc'); + }); + + test( + 'should return correct JSON representation for DataConnectOptions via ConnectorConfig toJson', + () { + final options = DataConnectOptions( + 'project-abc', 'us-central1', 'cloud-sql', 'service-123'); + + final jsonResult = options.toJson(); + final expectedJson = jsonEncode({ + 'location': 'us-central1', + 'connector': 'cloud-sql', + 'serviceId': 'service-123', + }); + + // Even though DataConnectOptions has a new field, toJson only reflects fields in ConnectorConfig + expect(jsonResult, expectedJson); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/core/empty_serializer_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/core/empty_serializer_test.dart new file mode 100644 index 000000000000..476a35f0768c --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/core/empty_serializer_test.dart @@ -0,0 +1,27 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('emptySerializer', () { + test('should return an empty string when null is passed', () { + final result = emptySerializer(null); + expect(result, ''); + }); + + test('should return an empty string when any value is passed', () { + final resultWithVoid = emptySerializer(null); // void type simulation + final resultWithInt = emptySerializer(42); + final resultWithString = emptySerializer('Some String'); + final resultWithList = emptySerializer([1, 2, 3]); + + expect(resultWithVoid, ''); + expect(resultWithInt, ''); + expect(resultWithString, ''); + expect(resultWithList, ''); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/core/ref_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/core/ref_test.dart new file mode 100644 index 000000000000..a8d320881a55 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/core/ref_test.dart @@ -0,0 +1,87 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: unused_local_variable + +import 'dart:async'; + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +// Mock classes +class MockDataConnectTransport extends Mock implements DataConnectTransport {} + +class MockFirebaseDataConnect extends Mock implements FirebaseDataConnect {} + +class MockQueryManager extends Mock implements QueryManager {} + +class MockOperationRef extends Mock implements OperationRef {} + +void main() { + group('OperationResult', () { + test('should initialize correctly with provided data and ref', () { + final mockData = 'sampleData'; + final mockRef = MockOperationRef(); + final mockFirebaseDataConnect = MockFirebaseDataConnect(); + + final result = + OperationResult(mockFirebaseDataConnect, mockData, mockRef); + + expect(result.data, mockData); + expect(result.ref, mockRef); + expect(result.dataConnect, mockFirebaseDataConnect); + }); + }); + + group('QueryResult', () { + test('should initialize correctly and inherit from OperationResult', () { + final mockData = 'sampleData'; + final mockRef = MockOperationRef(); + final mockFirebaseDataConnect = MockFirebaseDataConnect(); + + final queryResult = + QueryResult(mockFirebaseDataConnect, mockData, mockRef); + + expect(queryResult.data, mockData); + expect(queryResult.ref, mockRef); + expect(queryResult.dataConnect, mockFirebaseDataConnect); + }); + }); + + group('_QueryManager', () { + late MockFirebaseDataConnect mockDataConnect; + late QueryManager queryManager; + + setUp(() { + mockDataConnect = MockFirebaseDataConnect(); + queryManager = QueryManager(mockDataConnect); + }); + + test( + 'addQuery should create a new StreamController if query does not exist', + () { + final stream = + queryManager.addQuery('testQuery', 'variables', 'varsAsStr'); + + expect(queryManager.trackedQueries['testQuery'], isNotNull); + expect(queryManager.trackedQueries['testQuery']!['varsAsStr'], isNotNull); + expect(stream, isA()); + }); + }); + + group('MutationRef', () { + late MockDataConnectTransport mockTransport; + late MockFirebaseDataConnect mockDataConnect; + late Serializer serializer; + late Deserializer deserializer; + + setUp(() { + mockTransport = MockDataConnectTransport(); + mockDataConnect = MockFirebaseDataConnect(); + serializer = (data) => 'serializedData'; + deserializer = (data) => 'deserializedData'; + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.dart new file mode 100644 index 000000000000..c597b0471568 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.dart @@ -0,0 +1,175 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +import 'firebase_data_connect_test.mocks.dart'; + +class MockFirebaseAuth extends Mock implements FirebaseAuth {} + +class MockFirebaseAppCheck extends Mock implements FirebaseAppCheck {} + +class MockTransportOptions extends Mock implements TransportOptions {} + +class MockDataConnectTransport extends Mock implements DataConnectTransport {} + +class MockQueryManager extends Mock implements QueryManager {} + +void main() { + group('FirebaseDataConnect', () { + late MockFirebaseApp mockApp; + late MockFirebaseAuth mockAuth; + late MockFirebaseAppCheck mockAppCheck; + late MockConnectorConfig mockConnectorConfig; + + setUp(() { + mockApp = MockFirebaseApp(); + mockAuth = MockFirebaseAuth(); + mockAppCheck = MockFirebaseAppCheck(); + mockConnectorConfig = MockConnectorConfig(); + + when(mockApp.options).thenReturn(FirebaseOptions( + apiKey: 'fake_api_key', + appId: 'fake_app_id', + messagingSenderId: 'fake_messaging_sender_id', + projectId: 'fake_project_id', + )); + when(mockConnectorConfig.location).thenReturn('us-central1'); + when(mockConnectorConfig.connector).thenReturn('connector'); + when(mockConnectorConfig.serviceId).thenReturn('serviceId'); + }); + + test('constructor initializes with correct parameters', () { + final dataConnect = FirebaseDataConnect( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + expect(dataConnect.app, equals(mockApp)); + expect(dataConnect.auth, equals(mockAuth)); + expect(dataConnect.appCheck, equals(mockAppCheck)); + expect(dataConnect.connectorConfig, equals(mockConnectorConfig)); + expect(dataConnect.options.projectId, 'fake_project_id'); + }); + + test('checkTransport initializes transport correctly', () { + final dataConnect = FirebaseDataConnect( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + dataConnect.checkTransport(); + + expect(dataConnect.transport, isNotNull); + }); + + test('query method returns QueryRef', () { + final dataConnect = FirebaseDataConnect( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + final queryRef = dataConnect.query( + 'operationName', + (json) => json, + (variables) => variables.toString(), + null, + ); + + expect(queryRef, isA()); + }); + + test('mutation method returns MutationRef', () { + final dataConnect = FirebaseDataConnect( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + final mutationRef = dataConnect.mutation( + 'operationName', + (json) => json, + (variables) => variables.toString(), + null, + ); + + expect(mutationRef, isA()); + }); + + test('useDataConnectEmulator sets correct transport options', () { + final dataConnect = FirebaseDataConnect( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + dataConnect.useDataConnectEmulator('localhost', 8080); + + expect(dataConnect.transportOptions, isNotNull); + expect(dataConnect.transportOptions!.host, '10.0.2.2'); + expect(dataConnect.transportOptions!.port, 8080); + }); + + test('instanceFor returns cached instance if available', () { + FirebaseDataConnect.cachedInstances.clear(); // Clear cache first + + when(mockApp.name).thenReturn('appName'); + when(mockConnectorConfig.toJson()).thenReturn('connectorConfigStr'); + + final dataConnect = FirebaseDataConnect( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + FirebaseDataConnect.cachedInstances['appName'] = { + 'connectorConfigStr': dataConnect + }; + + final instance = FirebaseDataConnect.instanceFor( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + expect(instance, equals(dataConnect)); + }); + + test('instanceFor creates new instance if not cached', () { + FirebaseDataConnect.cachedInstances.clear(); // Clear cache first + + when(mockApp.name).thenReturn('appName'); + when(mockConnectorConfig.toJson()).thenReturn('connectorConfigStr'); + + final instance = FirebaseDataConnect.instanceFor( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + expect(instance, isA()); + expect( + FirebaseDataConnect.cachedInstances['appName']!['connectorConfigStr'], + equals(instance)); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart new file mode 100644 index 000000000000..3365f28aadab --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart @@ -0,0 +1,204 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Mocks generated by Mockito 5.4.4 from annotations +// in firebase_data_connect/test/src/firebase_data_connect_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:firebase_core/firebase_core.dart' as _i3; +import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart' + as _i2; +import 'package:firebase_data_connect/src/common/common_library.dart' as _i6; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i4; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeFirebaseOptions_0 extends _i1.SmartFake + implements _i2.FirebaseOptions { + _FakeFirebaseOptions_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [FirebaseApp]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFirebaseApp extends _i1.Mock implements _i3.FirebaseApp { + @override + String get name => (super.noSuchMethod( + Invocation.getter(#name), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#name), + ), + returnValueForMissingStub: _i4.dummyValue( + this, + Invocation.getter(#name), + ), + ) as String); + + @override + _i2.FirebaseOptions get options => (super.noSuchMethod( + Invocation.getter(#options), + returnValue: _FakeFirebaseOptions_0( + this, + Invocation.getter(#options), + ), + returnValueForMissingStub: _FakeFirebaseOptions_0( + this, + Invocation.getter(#options), + ), + ) as _i2.FirebaseOptions); + + @override + bool get isAutomaticDataCollectionEnabled => (super.noSuchMethod( + Invocation.getter(#isAutomaticDataCollectionEnabled), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i5.Future delete() => (super.noSuchMethod( + Invocation.method( + #delete, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setAutomaticDataCollectionEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method( + #setAutomaticDataCollectionEnabled, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setAutomaticResourceManagementEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method( + #setAutomaticResourceManagementEnabled, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [ConnectorConfig]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockConnectorConfig extends _i1.Mock implements _i6.ConnectorConfig { + @override + String get location => (super.noSuchMethod( + Invocation.getter(#location), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#location), + ), + returnValueForMissingStub: _i4.dummyValue( + this, + Invocation.getter(#location), + ), + ) as String); + + @override + set location(String? _location) => super.noSuchMethod( + Invocation.setter( + #location, + _location, + ), + returnValueForMissingStub: null, + ); + + @override + String get connector => (super.noSuchMethod( + Invocation.getter(#connector), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#connector), + ), + returnValueForMissingStub: _i4.dummyValue( + this, + Invocation.getter(#connector), + ), + ) as String); + + @override + set connector(String? _connector) => super.noSuchMethod( + Invocation.setter( + #connector, + _connector, + ), + returnValueForMissingStub: null, + ); + + @override + String get serviceId => (super.noSuchMethod( + Invocation.getter(#serviceId), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#serviceId), + ), + returnValueForMissingStub: _i4.dummyValue( + this, + Invocation.getter(#serviceId), + ), + ) as String); + + @override + set serviceId(String? _serviceId) => super.noSuchMethod( + Invocation.setter( + #serviceId, + _serviceId, + ), + returnValueForMissingStub: null, + ); + + @override + String toJson() => (super.noSuchMethod( + Invocation.method( + #toJson, + [], + ), + returnValue: _i4.dummyValue( + this, + Invocation.method( + #toJson, + [], + ), + ), + returnValueForMissingStub: _i4.dummyValue( + this, + Invocation.method( + #toJson, + [], + ), + ), + ) as String); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart new file mode 100644 index 000000000000..76aaf7aba059 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart @@ -0,0 +1,223 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect/src/network/rest_library.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'rest_transport_test.mocks.dart'; + +class MockFirebaseAuth extends Mock implements FirebaseAuth {} + +@GenerateMocks([http.Client, User, FirebaseAppCheck]) +void main() { + late RestTransport transport; + late MockClient mockHttpClient; + late MockFirebaseAuth mockAuth; + late MockFirebaseAppCheck mockAppCheck; + late MockUser mockUser; + + setUp(() { + mockHttpClient = MockClient(); + mockAuth = MockFirebaseAuth(); + mockAppCheck = MockFirebaseAppCheck(); + mockUser = MockUser(); + when(mockAuth.currentUser).thenReturn(mockUser); + + transport = RestTransport( + TransportOptions('testhost', 443, true), + DataConnectOptions( + 'testProject', + 'testLocation', + 'testConnector', + 'testService', + ), + mockAuth, + mockAppCheck, + ); + + transport.setHttp(mockHttpClient); + }); + + group('RestTransport', () { + test('should correctly initialize URL with secure protocol', () { + expect( + transport.url, + 'https://testhost:443/v1alpha/projects/testProject/locations/testLocation/services/testService/connectors/testConnector', + ); + }); + + test('should correctly initialize URL with insecure protocol', () { + final insecureTransport = RestTransport( + TransportOptions('testhost', 443, false), + DataConnectOptions( + 'testProject', + 'testLocation', + 'testConnector', + 'testService', + ), + mockAuth, + mockAppCheck, + ); + + expect( + insecureTransport.url, + 'http://testhost:443/v1alpha/projects/testProject/locations/testLocation/services/testService/connectors/testConnector', + ); + }); + + test('invokeOperation should return deserialized data', () async { + final mockResponse = http.Response('{"data": {"key": "value"}}', 200); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + final deserializer = (String data) => 'Deserialized Data'; + + final result = await transport.invokeOperation( + 'testQuery', + deserializer, + null, + null, + 'executeQuery', + ); + + expect(result, 'Deserialized Data'); + }); + + test('invokeOperation should throw unauthorized error on 401 response', + () async { + final mockResponse = http.Response('Unauthorized', 401); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + final deserializer = (String data) => 'Deserialized Data'; + + expect( + () => transport.invokeOperation( + 'testQuery', deserializer, null, null, 'executeQuery'), + throwsA(isA()), + ); + }); + + test('invokeOperation should throw other errors on non-200 responses', + () async { + final mockResponse = http.Response('{"message": "Some error"}', 500); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + final deserializer = (String data) => 'Deserialized Data'; + + expect( + () => transport.invokeOperation( + 'testQuery', deserializer, null, null, 'executeQuery'), + throwsA(isA()), + ); + }); + + test('invokeQuery should call invokeOperation with correct endpoint', + () async { + final mockResponse = http.Response('{"data": {"key": "value"}}', 200); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + final deserializer = (String data) => 'Deserialized Data'; + + await transport.invokeQuery('testQuery', deserializer, null, null); + + verify(mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: json.encode({ + 'name': + 'projects/testProject/locations/testLocation/services/testService/connectors/testConnector', + 'operationName': 'testQuery' + }), + )).called(1); + }); + + test('invokeMutation should call invokeOperation with correct endpoint', + () async { + final mockResponse = http.Response('{"data": {"key": "value"}}', 200); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + final deserializer = (String data) => 'Deserialized Mutation Data'; + + await transport.invokeMutation('testMutation', deserializer, null, null); + + verify(mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: json.encode({ + 'name': + 'projects/testProject/locations/testLocation/services/testService/connectors/testConnector', + 'operationName': 'testMutation' + }), + )).called(1); + }); + + test('invokeOperation should include auth and appCheck tokens in headers', + () async { + final mockResponse = http.Response('{"data": {"key": "value"}}', 200); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + when(mockUser.getIdToken()).thenAnswer((_) async => 'authToken123'); + when(mockAppCheck.getToken()).thenAnswer((_) async => 'appCheckToken123'); + + final deserializer = (String data) => 'Deserialized Data'; + + await transport.invokeOperation( + 'testQuery', deserializer, null, null, 'executeQuery'); + + verify(mockHttpClient.post( + any, + headers: argThat( + containsPair('X-Firebase-Auth-Token', 'authToken123'), + named: 'headers', + ), + body: anyNamed('body'), + )).called(1); + }); + + test( + 'invokeOperation should handle missing auth and appCheck tokens gracefully', + () async { + final mockResponse = http.Response('{"data": {"key": "value"}}', 200); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + when(mockUser.getIdToken()).thenThrow(Exception('Auth error')); + when(mockAppCheck.getToken()).thenThrow(Exception('AppCheck error')); + + final deserializer = (String data) => 'Deserialized Data'; + + await transport.invokeOperation( + 'testQuery', deserializer, null, null, 'executeQuery'); + + verify(mockHttpClient.post( + any, + headers: argThat( + isNot(contains('X-Firebase-Auth-Token')), + named: 'headers', + ), + body: anyNamed('body'), + )).called(1); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart new file mode 100644 index 000000000000..02270373a449 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart @@ -0,0 +1,824 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Mocks generated by Mockito 5.4.4 from annotations +// in firebase_data_connect/test/src/network/rest_transport_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i6; +import 'dart:convert' as _i7; +import 'dart:typed_data' as _i9; + +import 'package:firebase_app_check/firebase_app_check.dart' as _i10; +import 'package:firebase_app_check_platform_interface/firebase_app_check_platform_interface.dart' + as _i11; +import 'package:firebase_auth/firebase_auth.dart' as _i4; +import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart' + as _i3; +import 'package:firebase_core/firebase_core.dart' as _i5; +import 'package:http/http.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i8; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeStreamedResponse_1 extends _i1.SmartFake + implements _i2.StreamedResponse { + _FakeStreamedResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUserMetadata_2 extends _i1.SmartFake implements _i3.UserMetadata { + _FakeUserMetadata_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeMultiFactor_3 extends _i1.SmartFake implements _i4.MultiFactor { + _FakeMultiFactor_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeIdTokenResult_4 extends _i1.SmartFake implements _i3.IdTokenResult { + _FakeIdTokenResult_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUserCredential_5 extends _i1.SmartFake + implements _i4.UserCredential { + _FakeUserCredential_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeConfirmationResult_6 extends _i1.SmartFake + implements _i4.ConfirmationResult { + _FakeConfirmationResult_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUser_7 extends _i1.SmartFake implements _i4.User { + _FakeUser_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFirebaseApp_8 extends _i1.SmartFake implements _i5.FirebaseApp { + _FakeFirebaseApp_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i2.Client { + MockClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Future<_i2.Response> head( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + returnValue: _i6.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + )), + ) as _i6.Future<_i2.Response>); + + @override + _i6.Future<_i2.Response> get( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + returnValue: _i6.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + )), + ) as _i6.Future<_i2.Response>); + + @override + _i6.Future<_i2.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i7.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i6.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i6.Future<_i2.Response>); + + @override + _i6.Future<_i2.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i7.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i6.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i6.Future<_i2.Response>); + + @override + _i6.Future<_i2.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i7.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i6.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i6.Future<_i2.Response>); + + @override + _i6.Future<_i2.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i7.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i6.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i6.Future<_i2.Response>); + + @override + _i6.Future read( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + returnValue: _i6.Future.value(_i8.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + ) as _i6.Future); + + @override + _i6.Future<_i9.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #readBytes, + [url], + {#headers: headers}, + ), + returnValue: _i6.Future<_i9.Uint8List>.value(_i9.Uint8List(0)), + ) as _i6.Future<_i9.Uint8List>); + + @override + _i6.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => + (super.noSuchMethod( + Invocation.method( + #send, + [request], + ), + returnValue: + _i6.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( + this, + Invocation.method( + #send, + [request], + ), + )), + ) as _i6.Future<_i2.StreamedResponse>); + + @override + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [User]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUser extends _i1.Mock implements _i4.User { + MockUser() { + _i1.throwOnMissingStub(this); + } + + @override + bool get emailVerified => (super.noSuchMethod( + Invocation.getter(#emailVerified), + returnValue: false, + ) as bool); + + @override + bool get isAnonymous => (super.noSuchMethod( + Invocation.getter(#isAnonymous), + returnValue: false, + ) as bool); + + @override + _i3.UserMetadata get metadata => (super.noSuchMethod( + Invocation.getter(#metadata), + returnValue: _FakeUserMetadata_2( + this, + Invocation.getter(#metadata), + ), + ) as _i3.UserMetadata); + + @override + List<_i3.UserInfo> get providerData => (super.noSuchMethod( + Invocation.getter(#providerData), + returnValue: <_i3.UserInfo>[], + ) as List<_i3.UserInfo>); + + @override + String get uid => (super.noSuchMethod( + Invocation.getter(#uid), + returnValue: _i8.dummyValue( + this, + Invocation.getter(#uid), + ), + ) as String); + + @override + _i4.MultiFactor get multiFactor => (super.noSuchMethod( + Invocation.getter(#multiFactor), + returnValue: _FakeMultiFactor_3( + this, + Invocation.getter(#multiFactor), + ), + ) as _i4.MultiFactor); + + @override + _i6.Future delete() => (super.noSuchMethod( + Invocation.method( + #delete, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getIdToken([bool? forceRefresh = false]) => + (super.noSuchMethod( + Invocation.method( + #getIdToken, + [forceRefresh], + ), + returnValue: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i3.IdTokenResult> getIdTokenResult( + [bool? forceRefresh = false]) => + (super.noSuchMethod( + Invocation.method( + #getIdTokenResult, + [forceRefresh], + ), + returnValue: _i6.Future<_i3.IdTokenResult>.value(_FakeIdTokenResult_4( + this, + Invocation.method( + #getIdTokenResult, + [forceRefresh], + ), + )), + ) as _i6.Future<_i3.IdTokenResult>); + + @override + _i6.Future<_i4.UserCredential> linkWithCredential( + _i3.AuthCredential? credential) => + (super.noSuchMethod( + Invocation.method( + #linkWithCredential, + [credential], + ), + returnValue: _i6.Future<_i4.UserCredential>.value(_FakeUserCredential_5( + this, + Invocation.method( + #linkWithCredential, + [credential], + ), + )), + ) as _i6.Future<_i4.UserCredential>); + + @override + _i6.Future<_i4.UserCredential> linkWithProvider(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #linkWithProvider, + [provider], + ), + returnValue: _i6.Future<_i4.UserCredential>.value(_FakeUserCredential_5( + this, + Invocation.method( + #linkWithProvider, + [provider], + ), + )), + ) as _i6.Future<_i4.UserCredential>); + + @override + _i6.Future<_i4.UserCredential> reauthenticateWithProvider( + _i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #reauthenticateWithProvider, + [provider], + ), + returnValue: _i6.Future<_i4.UserCredential>.value(_FakeUserCredential_5( + this, + Invocation.method( + #reauthenticateWithProvider, + [provider], + ), + )), + ) as _i6.Future<_i4.UserCredential>); + + @override + _i6.Future<_i4.UserCredential> reauthenticateWithPopup( + _i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #reauthenticateWithPopup, + [provider], + ), + returnValue: _i6.Future<_i4.UserCredential>.value(_FakeUserCredential_5( + this, + Invocation.method( + #reauthenticateWithPopup, + [provider], + ), + )), + ) as _i6.Future<_i4.UserCredential>); + + @override + _i6.Future reauthenticateWithRedirect(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #reauthenticateWithRedirect, + [provider], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i4.UserCredential> linkWithPopup(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #linkWithPopup, + [provider], + ), + returnValue: _i6.Future<_i4.UserCredential>.value(_FakeUserCredential_5( + this, + Invocation.method( + #linkWithPopup, + [provider], + ), + )), + ) as _i6.Future<_i4.UserCredential>); + + @override + _i6.Future linkWithRedirect(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #linkWithRedirect, + [provider], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i4.ConfirmationResult> linkWithPhoneNumber( + String? phoneNumber, [ + _i4.RecaptchaVerifier? verifier, + ]) => + (super.noSuchMethod( + Invocation.method( + #linkWithPhoneNumber, + [ + phoneNumber, + verifier, + ], + ), + returnValue: + _i6.Future<_i4.ConfirmationResult>.value(_FakeConfirmationResult_6( + this, + Invocation.method( + #linkWithPhoneNumber, + [ + phoneNumber, + verifier, + ], + ), + )), + ) as _i6.Future<_i4.ConfirmationResult>); + + @override + _i6.Future<_i4.UserCredential> reauthenticateWithCredential( + _i3.AuthCredential? credential) => + (super.noSuchMethod( + Invocation.method( + #reauthenticateWithCredential, + [credential], + ), + returnValue: _i6.Future<_i4.UserCredential>.value(_FakeUserCredential_5( + this, + Invocation.method( + #reauthenticateWithCredential, + [credential], + ), + )), + ) as _i6.Future<_i4.UserCredential>); + + @override + _i6.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future sendEmailVerification( + [_i3.ActionCodeSettings? actionCodeSettings]) => + (super.noSuchMethod( + Invocation.method( + #sendEmailVerification, + [actionCodeSettings], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i4.User> unlink(String? providerId) => (super.noSuchMethod( + Invocation.method( + #unlink, + [providerId], + ), + returnValue: _i6.Future<_i4.User>.value(_FakeUser_7( + this, + Invocation.method( + #unlink, + [providerId], + ), + )), + ) as _i6.Future<_i4.User>); + + @override + _i6.Future updateEmail(String? newEmail) => (super.noSuchMethod( + Invocation.method( + #updateEmail, + [newEmail], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future updatePassword(String? newPassword) => (super.noSuchMethod( + Invocation.method( + #updatePassword, + [newPassword], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future updatePhoneNumber( + _i3.PhoneAuthCredential? phoneCredential) => + (super.noSuchMethod( + Invocation.method( + #updatePhoneNumber, + [phoneCredential], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future updateDisplayName(String? displayName) => + (super.noSuchMethod( + Invocation.method( + #updateDisplayName, + [displayName], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future updatePhotoURL(String? photoURL) => (super.noSuchMethod( + Invocation.method( + #updatePhotoURL, + [photoURL], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future updateProfile({ + String? displayName, + String? photoURL, + }) => + (super.noSuchMethod( + Invocation.method( + #updateProfile, + [], + { + #displayName: displayName, + #photoURL: photoURL, + }, + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future verifyBeforeUpdateEmail( + String? newEmail, [ + _i3.ActionCodeSettings? actionCodeSettings, + ]) => + (super.noSuchMethod( + Invocation.method( + #verifyBeforeUpdateEmail, + [ + newEmail, + actionCodeSettings, + ], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); +} + +/// A class which mocks [FirebaseAppCheck]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFirebaseAppCheck extends _i1.Mock implements _i10.FirebaseAppCheck { + MockFirebaseAppCheck() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.FirebaseApp get app => (super.noSuchMethod( + Invocation.getter(#app), + returnValue: _FakeFirebaseApp_8( + this, + Invocation.getter(#app), + ), + ) as _i5.FirebaseApp); + + @override + set app(_i5.FirebaseApp? _app) => super.noSuchMethod( + Invocation.setter( + #app, + _app, + ), + returnValueForMissingStub: null, + ); + + @override + _i6.Stream get onTokenChange => (super.noSuchMethod( + Invocation.getter(#onTokenChange), + returnValue: _i6.Stream.empty(), + ) as _i6.Stream); + + @override + Map get pluginConstants => (super.noSuchMethod( + Invocation.getter(#pluginConstants), + returnValue: {}, + ) as Map); + + @override + _i6.Future activate({ + _i11.WebProvider? webProvider, + _i11.AndroidProvider? androidProvider = _i11.AndroidProvider.playIntegrity, + _i11.AppleProvider? appleProvider = _i11.AppleProvider.deviceCheck, + }) => + (super.noSuchMethod( + Invocation.method( + #activate, + [], + { + #webProvider: webProvider, + #androidProvider: androidProvider, + #appleProvider: appleProvider, + }, + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getToken([bool? forceRefresh]) => (super.noSuchMethod( + Invocation.method( + #getToken, + [forceRefresh], + ), + returnValue: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setTokenAutoRefreshEnabled( + bool? isTokenAutoRefreshEnabled) => + (super.noSuchMethod( + Invocation.method( + #setTokenAutoRefreshEnabled, + [isTokenAutoRefreshEnabled], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getLimitedUseToken() => (super.noSuchMethod( + Invocation.method( + #getLimitedUseToken, + [], + ), + returnValue: _i6.Future.value(_i8.dummyValue( + this, + Invocation.method( + #getLimitedUseToken, + [], + ), + )), + ) as _i6.Future); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/network/transport_stub_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/network/transport_stub_test.dart new file mode 100644 index 000000000000..eb2484625cac --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/network/transport_stub_test.dart @@ -0,0 +1,87 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect/src/network/transport_library.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +// Create mock classes for FirebaseAuth, FirebaseAppCheck, and other dependencies. +class MockFirebaseAuth extends Mock implements FirebaseAuth {} + +class MockFirebaseAppCheck extends Mock implements FirebaseAppCheck {} + +class MockTransportOptions extends Mock implements TransportOptions {} + +class MockDataConnectOptions extends Mock implements DataConnectOptions {} + +void main() { + group('TransportStub', () { + late MockFirebaseAuth mockAuth; + late MockFirebaseAppCheck mockAppCheck; + late MockTransportOptions mockTransportOptions; + late MockDataConnectOptions mockDataConnectOptions; + + setUp(() { + mockAuth = MockFirebaseAuth(); + mockAppCheck = MockFirebaseAppCheck(); + mockTransportOptions = MockTransportOptions(); + mockDataConnectOptions = MockDataConnectOptions(); + }); + + test('constructor initializes with correct parameters', () { + final transportStub = TransportStub( + mockTransportOptions, + mockDataConnectOptions, + mockAuth, + mockAppCheck, + ); + + expect(transportStub.auth, equals(mockAuth)); + expect(transportStub.appCheck, equals(mockAppCheck)); + expect(transportStub.transportOptions, equals(mockTransportOptions)); + expect(transportStub.options, equals(mockDataConnectOptions)); + }); + + test('invokeMutation throws UnimplementedError', () async { + final transportStub = TransportStub( + mockTransportOptions, + mockDataConnectOptions, + mockAuth, + mockAppCheck, + ); + + expect( + () async => await transportStub.invokeMutation( + 'queryName', + (json) => json, + null, + null, + ), + throwsA(isA()), + ); + }); + + test('invokeQuery throws UnimplementedError', () async { + final transportStub = TransportStub( + mockTransportOptions, + mockDataConnectOptions, + mockAuth, + mockAppCheck, + ); + + expect( + () async => await transportStub.invokeQuery( + 'queryName', + (json) => json, + null, + null, + ), + throwsA(isA()), + ); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/optional_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/optional_test.dart new file mode 100644 index 000000000000..47cdd1778ed8 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/optional_test.dart @@ -0,0 +1,91 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: unused_local_variable + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; + +typedef Serializer = String Function(T value); +typedef Deserializer = T Function(String json); + +void main() { + group('Optional', () { + late Deserializer stringDeserializer; + late Serializer stringSerializer; + late Deserializer intDeserializer; + late Serializer intSerializer; + + setUp(() { + stringDeserializer = (json) => json; + stringSerializer = (value) => value.toString(); + intDeserializer = (json) => int.parse(json); + intSerializer = (value) => value.toString(); + }); + + test('constructor initializes with deserializer', () { + final optional = Optional(stringDeserializer); + expect(optional.deserializer, equals(stringDeserializer)); + expect(optional.state, equals(OptionalState.unset)); + expect(optional.value, isNull); + }); + + test('constructor initializes with deserializer and serializer', () { + final optional = Optional.optional(stringDeserializer, stringSerializer); + expect(optional.deserializer, equals(stringDeserializer)); + expect(optional.serializer, equals(stringSerializer)); + }); + + test('value setter updates value and sets state', () { + final optional = Optional(stringDeserializer); + + optional.value = 'Test'; + expect(optional.value, equals('Test')); + expect(optional.state, equals(OptionalState.set)); + }); + + test('fromJson correctly deserializes and sets value', () { + final optional = Optional(stringDeserializer); + + optional.fromJson('Test'); + expect(optional.value, equals('Test')); + expect(optional.state, equals(OptionalState.set)); + }); + + test('toJson correctly serializes the value', () { + final optional = Optional.optional(stringDeserializer, stringSerializer); + + optional.value = 'Test'; + expect(optional.toJson(), equals('Test')); + }); + + test('toJson returns empty string when value is null', () { + final optional = Optional.optional(stringDeserializer, stringSerializer); + + optional.value = null; + expect(optional.toJson(), equals('')); + }); + + test('nativeToJson correctly serializes primitive types', () { + expect(nativeToJson(42), equals('42')); + expect(nativeToJson(true), equals('true')); + expect(nativeToJson('Test'), equals('Test')); + }); + + test('nativeFromJson correctly deserializes primitive types', () { + expect(nativeFromJson('42'), equals(42)); + expect(nativeFromJson('true'), equals(true)); + expect(nativeFromJson('Test'), equals('Test')); + }); + + test('nativeToJson throws UnimplementedError for unsupported types', () { + expect(() => nativeToJson(DateTime.now()), throwsUnimplementedError); + }); + + test('nativeFromJson throws UnimplementedError for unsupported types', () { + expect(() => nativeFromJson('2024-01-01'), + throwsUnimplementedError); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/timestamp_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/timestamp_test.dart new file mode 100644 index 000000000000..a54d2c1a69f2 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/timestamp_test.dart @@ -0,0 +1,64 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Timestamp', () { + test('constructor initializes with correct nanoseconds and seconds', () { + final timestamp = Timestamp(500, 864000); // Example timestamp values + expect(timestamp.nanoseconds, 500); + expect(timestamp.seconds, 864000); + }); + + test('fromJson throws exception for invalid date format', () { + expect(() => Timestamp.fromJson('invalid-date'), throwsException); + }); + + test('fromJson correctly parses date with nanoseconds and UTC (Z) format', + () { + final timestamp = Timestamp.fromJson('1970-01-11T00:00:00.123456789Z'); + expect(timestamp.seconds, 864000); + expect(timestamp.nanoseconds, 123456789); + }); + + test('fromJson correctly parses date without nanoseconds', () { + final timestamp = Timestamp.fromJson('1970-01-11T00:00:00Z'); + expect(timestamp.seconds, 864000); + expect(timestamp.nanoseconds, 0); + }); + + test('fromJson correctly handles timezones with positive offset', () { + final timestamp = Timestamp.fromJson('1970-01-11T00:00:00+02:00'); + expect(timestamp.seconds, + 864000 - (2 * 3600)); // Adjusts by the positive timezone offset + }); + + test('fromJson correctly handles timezones with negative offset', () { + final timestamp = Timestamp.fromJson('1970-01-11T00:00:00-05:00'); + expect(timestamp.seconds, + 864000 + (5 * 3600)); // Adjusts by the negative timezone offset + }); + + test('toJson correctly serializes to ISO8601 string with nanoseconds', () { + final timestamp = Timestamp(123456789, 864000); // Example timestamp + final json = timestamp.toJson(); + expect(json, '1970-01-11T00:00:00.123456789Z'); + }); + + test('toJson correctly serializes to ISO8601 string without nanoseconds', + () { + final timestamp = Timestamp(0, 864000); // No nanoseconds + final json = timestamp.toJson(); + expect(json, '1970-01-11T00:00:00.000Z'); + }); + + test('toDateTime correctly converts to DateTime object', () { + final timestamp = Timestamp(0, 864000); // Example timestamp + final dateTime = timestamp.toDateTime(); + expect(dateTime, DateTime.utc(1970, 1, 11, 0, 0, 0, 0)); + }); + }); +}