diff --git a/.gitignore b/.gitignore index ff7b6c5..292489e 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,4 @@ scripts xcodebuild.log Package.resolved +GoogleService-Info.plist diff --git a/Examples/FriendlyFlix/.editorconfig b/Examples/FriendlyFlix/.editorconfig new file mode 100644 index 0000000..4d5d604 --- /dev/null +++ b/Examples/FriendlyFlix/.editorconfig @@ -0,0 +1,8 @@ +[*.swift] +indent_style = space +indent_size = 2 +tab_width = 2 +end_of_line = crlf +insert_final_newline = true +max_line_length = 100 +trim_trailing_whitespace = true \ No newline at end of file diff --git a/Examples/FriendlyFlix/.firebase/.graphqlrc b/Examples/FriendlyFlix/.firebase/.graphqlrc new file mode 100644 index 0000000..4028adf --- /dev/null +++ b/Examples/FriendlyFlix/.firebase/.graphqlrc @@ -0,0 +1 @@ +{"schema":["../dataconnect/schema/**/*.gql","../dataconnect/.dataconnect/**/*.gql"],"document":["../dataconnect/movie-connector/**/*.gql"]} \ No newline at end of file diff --git a/Examples/FriendlyFlix/.swift-format b/Examples/FriendlyFlix/.swift-format new file mode 100644 index 0000000..23f51c8 --- /dev/null +++ b/Examples/FriendlyFlix/.swift-format @@ -0,0 +1,70 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentation" : { + "spaces" : 2 + }, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineLength" : 100, + "maximumBlankLines" : 1, + "multiElementCollectionTrailingCommas" : true, + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether" : false, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : false, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : true, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + }, + "spacesAroundRangeFormationOperators" : false, + "tabWidth" : 8, + "version" : 1 +} diff --git a/Examples/FriendlyFlix/LICENSE b/Examples/FriendlyFlix/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/Examples/FriendlyFlix/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file diff --git a/Examples/FriendlyFlix/README.md b/Examples/FriendlyFlix/README.md new file mode 100644 index 0000000..e3b182c --- /dev/null +++ b/Examples/FriendlyFlix/README.md @@ -0,0 +1,73 @@ +# FriendlyFlix + +## Introduction + +This quickstart is a movie tracker app to demonstrate the use of Firebase Data Connect + with a Cloud SQL database. + +For more information about Firebase Data Connect visit [the docs](https://firebase.google.com/docs/data-connect/). + +## Getting Started + +Follow these steps to get up and running with Firebase Data Connect. For more detailed instructions, +check out the [official documentation](https://firebase.google.com/docs/data-connect/quickstart). + +### 0. Prerequisites +- Latest version of [Xcode](https://developer.apple.com/xcode/) +- Latest version of [Visual Studio Code](https://code.visualstudio.com/) +- The [Firebase Data Connect VS Code Extension](https://marketplace.visualstudio.com/items?itemName=GoogleCloudTools.firebase-dataconnect-vscode) + +### 1. Connect to your Firebase project + +1. If you haven't already, create a Firebase project. + * In the [Firebase console](https://console.firebase.google.com), click + **Add project**, then follow the on-screen instructions. + +2. Upgrade your project to the Blaze plan. This lets you create a Cloud SQL + for PostgreSQL instance. + + > Note: Though you set up billing in your Blaze upgrade, you won't be + charged for usage of Firebase Data Connect or the + [default Cloud SQL for PostgreSQL configuration](https://firebase.google.com/docs/data-connect/#pricing) during the preview. + +3. Navigate to the [Data Connect section](https://console.firebase.google.com/u/0/project/_/dataconnect) + of the Firebase console, click on the "Get Started" button and follow the setup workflow: + - Select a location for your Cloud SQL for PostgreSQL database (this sample uses `us-central1`). If you choose a different location, you'll also need to change the `data-connect-ios-sdk/Examples/FriendlyFlix/dataconnect/dataconnect.yaml` file. + - Select the option to create a new Cloud SQL instance and fill in the following fields: + - Service ID: `dataconnect` + - Cloud SQL Instance ID: `fdc-sql` + - Database name: `fdcdb` +4. Allow some time for the Cloud SQL instance to be provisioned. After it's provisioned, the instance + can be managed in the [Cloud Console](https://console.cloud.google.com/sql). + +5. If you haven’t already, add an iOS app to your Firebase project, using `com.google.firebase.samples.FriendlyFlix` as the bundle ID. + Click **Download GoogleService-Info.plist** to obtain your Firebase config file. + +### 2. Cloning the repository + +1. Clone this repository to your local machine: + ```sh + git clone https://github.com/firebase/data-connect-ios-sdk.git + ``` + +2. Move the `GoogleService-Info.plist` config file (downloaded in the previous step) into the root folder of the sample app in the + `data-connect-ios-sdk/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/GoogleService-Info.plist` directory. + +### 3. Open in Visual Studio Code (VS Code) + +1. Open the `data-connect-ios-sdk/Examples/FriendlyFlix` directory in VS Code. +2. Click on the Firebase Data Connect icon on the VS Code sidebar to load the Extension. + a. Sign in with your Google Account if you haven't already. +3. Click on "Connect a Firebase project" and choose the project where you have set up Data Connect. +4. Click on "Start Emulators" - this should generate the Swift SDK for you and start the emulators. + +### 4. Populate the database +In VS Code, open the `data-connect-ios-sdk/Examples/FriendlyFlix/dataconnect/data_seed.gql` file and click the + `Run (local)` button at the top of the file. + +If you’d like to confirm that the data was correctly inserted, +open `data-connect-ios-sdk/Examples/FriendlyFlix/dataconnect/movie-connector/queries.gql` and run the `ListMovies` query. + +### 5. Running the app + +Press the Run button in Xcode to run the sample app on the iOS Simulator. diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix.xcodeproj/project.pbxproj b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix.xcodeproj/project.pbxproj new file mode 100644 index 0000000..db1fdc0 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix.xcodeproj/project.pbxproj @@ -0,0 +1,389 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 88896F392CCA5AD80089A19C /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 88896F382CCA5AD80089A19C /* NukeUI */; }; + 88BA433B2CC937E10063E309 /* FirebaseDataConnect in Frameworks */ = {isa = PBXBuildFile; productRef = 88BA433A2CC937E10063E309 /* FirebaseDataConnect */; }; + 88BA43402CC938250063E309 /* FriendlyFlixSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 88BA433F2CC938250063E309 /* FriendlyFlixSDK */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 88A9E6342CA834C600B3C4EF /* FriendlyFlix.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FriendlyFlix.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 88A9E6362CA834C600B3C4EF /* FriendlyFlix */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = FriendlyFlix; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 88A9E6312CA834C600B3C4EF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 88896F392CCA5AD80089A19C /* NukeUI in Frameworks */, + 88BA433B2CC937E10063E309 /* FirebaseDataConnect in Frameworks */, + 88BA43402CC938250063E309 /* FriendlyFlixSDK in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 88A9E62B2CA834C600B3C4EF = { + isa = PBXGroup; + children = ( + 88A9E6362CA834C600B3C4EF /* FriendlyFlix */, + 88A9E6352CA834C600B3C4EF /* Products */, + ); + sourceTree = ""; + }; + 88A9E6352CA834C600B3C4EF /* Products */ = { + isa = PBXGroup; + children = ( + 88A9E6342CA834C600B3C4EF /* FriendlyFlix.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 88A9E6332CA834C600B3C4EF /* FriendlyFlix */ = { + isa = PBXNativeTarget; + buildConfigurationList = 88A9E6422CA834C700B3C4EF /* Build configuration list for PBXNativeTarget "FriendlyFlix" */; + buildPhases = ( + 88A9E6302CA834C600B3C4EF /* Sources */, + 88A9E6312CA834C600B3C4EF /* Frameworks */, + 88A9E6322CA834C600B3C4EF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 88A9E6362CA834C600B3C4EF /* FriendlyFlix */, + ); + name = FriendlyFlix; + packageProductDependencies = ( + 88BA433A2CC937E10063E309 /* FirebaseDataConnect */, + 88BA433F2CC938250063E309 /* FriendlyFlixSDK */, + 88896F382CCA5AD80089A19C /* NukeUI */, + ); + productName = FriendlyFlix; + productReference = 88A9E6342CA834C600B3C4EF /* FriendlyFlix.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 88A9E62C2CA834C600B3C4EF /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + 88A9E6332CA834C600B3C4EF = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = 88A9E62F2CA834C600B3C4EF /* Build configuration list for PBXProject "FriendlyFlix" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 88A9E62B2CA834C600B3C4EF; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 88BA43392CC937E10063E309 /* XCRemoteSwiftPackageReference "data-connect-ios-sdk" */, + 88BA433E2CC938250063E309 /* XCLocalSwiftPackageReference "../FriendlyFlixSDK" */, + 88896F372CCA5AD80089A19C /* XCRemoteSwiftPackageReference "Nuke" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 88A9E6352CA834C600B3C4EF /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 88A9E6332CA834C600B3C4EF /* FriendlyFlix */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 88A9E6322CA834C600B3C4EF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 88A9E6302CA834C600B3C4EF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 88A9E6402CA834C700B3C4EF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 88A9E6412CA834C700B3C4EF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 88A9E6432CA834C700B3C4EF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"FriendlyFlix/Preview Content\""; + DEVELOPMENT_TEAM = YGAZHQXHH4; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.samples.FriendlyFlix; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 88A9E6442CA834C700B3C4EF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"FriendlyFlix/Preview Content\""; + DEVELOPMENT_TEAM = YGAZHQXHH4; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.samples.FriendlyFlix; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 88A9E62F2CA834C600B3C4EF /* Build configuration list for PBXProject "FriendlyFlix" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 88A9E6402CA834C700B3C4EF /* Debug */, + 88A9E6412CA834C700B3C4EF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 88A9E6422CA834C700B3C4EF /* Build configuration list for PBXNativeTarget "FriendlyFlix" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 88A9E6432CA834C700B3C4EF /* Debug */, + 88A9E6442CA834C700B3C4EF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 88BA433E2CC938250063E309 /* XCLocalSwiftPackageReference "../FriendlyFlixSDK" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../FriendlyFlixSDK; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 88896F372CCA5AD80089A19C /* XCRemoteSwiftPackageReference "Nuke" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kean/Nuke?tab=readme-ov-file"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 12.8.0; + }; + }; + 88BA43392CC937E10063E309 /* XCRemoteSwiftPackageReference "data-connect-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/firebase/data-connect-ios-sdk"; + requirement = { + branch = main; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 88896F382CCA5AD80089A19C /* NukeUI */ = { + isa = XCSwiftPackageProductDependency; + package = 88896F372CCA5AD80089A19C /* XCRemoteSwiftPackageReference "Nuke" */; + productName = NukeUI; + }; + 88BA433A2CC937E10063E309 /* FirebaseDataConnect */ = { + isa = XCSwiftPackageProductDependency; + package = 88BA43392CC937E10063E309 /* XCRemoteSwiftPackageReference "data-connect-ios-sdk" */; + productName = FirebaseDataConnect; + }; + 88BA433F2CC938250063E309 /* FriendlyFlixSDK */ = { + isa = XCSwiftPackageProductDependency; + productName = FriendlyFlixSDK; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 88A9E62C2CA834C600B3C4EF /* Project object */; +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix.xcodeproj/xcshareddata/xcschemes/FriendlyFlix.xcscheme b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix.xcodeproj/xcshareddata/xcschemes/FriendlyFlix.xcscheme new file mode 100644 index 0000000..3e0042c --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix.xcodeproj/xcshareddata/xcschemes/FriendlyFlix.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/App/FriendlyFlixApp.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/App/FriendlyFlixApp.swift new file mode 100644 index 0000000..6bda7ab --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/App/FriendlyFlixApp.swift @@ -0,0 +1,60 @@ +// 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. + +import Firebase +import FirebaseAuth +@preconcurrency import FirebaseDataConnect +import FriendlyFlixSDK +import SwiftUI + +@main +struct FriendlyFlixApp: App { + private func loadRocketSimConnect() { + #if DEBUG + guard Bundle( + path: "/Applications/RocketSim.app/Contents/Frameworks/RocketSimConnectLinker.nocache.framework" + )? + .load() == true else { + print("Failed to load linker framework") + return + } + print("RocketSim Connect successfully linked") + #endif + } + + var authenticationService: AuthenticationService? + + init() { + loadRocketSimConnect() + FirebaseApp.configure() + + authenticationService = AuthenticationService() + authenticationService?.onSignUp { user in + print( + "User signed in \(user.displayName ?? "(no fullname)") with email \(user.email ?? "(no email)")" + ) + let userName = String(user.email?.split(separator: "@").first ?? "(unknown)") + Task { + try await DataConnect.friendlyFlixConnector.upsertUserMutation.execute(username: userName) + } + } + } + + var body: some Scene { + WindowGroup { + RootView() + .environment(authenticationService) + } + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/App/RootView.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/App/RootView.swift new file mode 100644 index 0000000..4a3d33a --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/App/RootView.swift @@ -0,0 +1,48 @@ +// 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. + +import SwiftUI + +struct RootView: View { + @Environment(AuthenticationService.self) private var authenticationViewModel + + var body: some View { + @Bindable var authenticationViewModel = authenticationViewModel + TabView { + HomeScreen() + .tabItem { + Label("Home", systemImage: "house") + } + SearchScreen() + .tabItem { + Label("Search", systemImage: "magnifyingglass") + } + LibraryScreen() + .tabItem { + Label("Library", systemImage: "rectangle.on.rectangle") + } + } + .sheet(isPresented: $authenticationViewModel.presentingAuthenticationDialog) { + AuthenticationScreen() + } + .sheet(isPresented: $authenticationViewModel.presentingAccountDialog) { + AccountScreen() + } + } +} + +#Preview { + RootView() + .environment(AuthenticationService()) +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Assets.xcassets/Contents.json b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AccountScreen.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AccountScreen.swift new file mode 100644 index 0000000..2412352 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AccountScreen.swift @@ -0,0 +1,76 @@ +// 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. + +import SwiftUI + +struct AccountScreen: View { + @Environment(\.dismiss) var dismiss + @Environment(AuthenticationService.self) var authenticationService + + private var displayName: String { + authenticationService.user?.displayName ?? "(not set)" + } + + private var email: String { + authenticationService.user?.email ?? "" + } + + private func signOut() { + do { + try authenticationService.signOut() + dismiss() + } catch {} + } +} + +extension AccountScreen { + var body: some View { + NavigationStack { + List { + Section { + HStack(alignment: .center) { + Image(systemName: "person.circle.fill") + .resizable() + .scaledToFit() + .frame(height: 48) + VStack(alignment: .leading) { + Text(displayName) + Text(email) + } + } + } + + Section { + Button(action: signOut) { + Text("Sign out") + } + } + } + .navigationTitle("Account") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem { + Button(action: { dismiss() }) { + Text("Done") + } + } + } + } + } +} + +#Preview { + AccountScreen() + .environment(AuthenticationService()) +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AuthenticationScreen.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AuthenticationScreen.swift new file mode 100644 index 0000000..55a8e76 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AuthenticationScreen.swift @@ -0,0 +1,183 @@ +// 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. + +import AuthenticationServices +import SwiftUI + +private enum FocusableField: Hashable { + case email + case password + case confirmPassword +} + +private enum AuthenticationFlow { + case login + case signUp +} + +struct AuthenticationScreen: View { + @Environment(AuthenticationService.self) var authenticationService + @Environment(\.colorScheme) private var colorScheme + @Environment(\.dismiss) private var dismiss + + @State private var email = "" + @State private var password = "" + @State private var confirmPassword = "" + + @State private var flow: AuthenticationFlow = .login + + @State private var errorMessage = "" + @State private var displayName = "" + + private var isValid: Bool { + return if flow == .login { + !email.isEmpty && !password.isEmpty + } else { + !email.isEmpty && !password.isEmpty && password == confirmPassword + } + } + + private func switchFlow() { + flow = flow == .login ? .signUp : .login + errorMessage = "" + } + + @FocusState private var focus: FocusableField? + + private func signInWithEmailPassword() { + Task { + do { + try await authenticationService.signInWithEmailPassword(email: email, password: password) + dismiss() + } catch { + print(error.localizedDescription) + errorMessage = error.localizedDescription + } + } + } + + private func signUpWithEmailPassword() { + Task { + do { + try await authenticationService.signUpWithEmailPassword(email: email, password: password) + errorMessage = "" + dismiss() + } catch { + print(error.localizedDescription) + errorMessage = error.localizedDescription + } + } + } +} + +extension AuthenticationScreen { + var body: some View { + VStack { + Text("Welcome to FriendlyFlix") + .font(.largeTitle) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + + HStack { + Image(systemName: "at") + TextField("Email", text: $email) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focus, equals: .email) + .submitLabel(.next) + .onSubmit { + self.focus = .password + } + } + .padding(.vertical, 6) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 4) + + HStack { + Image(systemName: "lock") + SecureField("Password", text: $password) + .focused($focus, equals: .password) + .submitLabel(.go) + .onSubmit { + signInWithEmailPassword() + } + } + .padding(.vertical, 6) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 8) + + if flow == .signUp { + HStack { + Image(systemName: "lock") + SecureField("Confirm password", text: $confirmPassword) + .focused($focus, equals: .confirmPassword) + .submitLabel(.go) + .onSubmit { + signUpWithEmailPassword() + } + } + .padding(.vertical, 6) + .background(Divider(), alignment: .bottom) + .padding(.bottom, 8) + } + + if !errorMessage.isEmpty { + VStack { + Text(errorMessage) + .foregroundColor(Color(UIColor.systemRed)) + } + } + + Button(action: { + if flow == .login { signInWithEmailPassword() } + else { signUpWithEmailPassword() } + }) { + if authenticationService.authenticationState != .authenticating { + Text(flow == .login ? "Log in with password" : "Sign up") + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } else { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + } + } + .disabled(!isValid) + .frame(maxWidth: .infinity) + .buttonStyle(.borderedProminent) + + HStack { + Text(flow == .login ? "Don't have an account yet?" : "Already have an account?") + Button(action: { + withAnimation { + switchFlow() + } + }) { + Text(flow == .signUp ? "Log in" : "Sign up") + .fontWeight(.semibold) + .foregroundColor(.blue) + } + } + .padding([.top, .bottom], 50) + } + .padding() + .frame(maxHeight: .infinity, alignment: .bottom) + } +} + +#Preview { + AuthenticationScreen() + .environment(AuthenticationService()) +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AuthenticationService.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AuthenticationService.swift new file mode 100644 index 0000000..98eb0cb --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AuthenticationService.swift @@ -0,0 +1,70 @@ +// 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. + +@preconcurrency import FirebaseAuth +import Foundation +import Observation + +enum AuthenticationState { + case unauthenticated + case authenticating + case authenticated +} + +@Observable @MainActor +class AuthenticationService { + var presentingAuthenticationDialog = false + var presentingAccountDialog = false + + var authenticationState: AuthenticationState = .unauthenticated + var user: User? + + private var authenticationListener: AuthStateDidChangeListenerHandle? + + init() { + authenticationListener = Auth.auth().addStateDidChangeListener { auth, user in + if let user { + self.authenticationState = .authenticated + self.user = user + } else { + self.authenticationState = .unauthenticated + } + } + } + + private var onSignUp: ((User) -> Void)? + public func onSignUp(_ action: @escaping (User) -> Void) { + onSignUp = action + } + + func signInWithEmailPassword(email: String, password: String) async throws { + try await Auth.auth().signIn(withEmail: email, password: password) + authenticationState = .authenticated + } + + func signUpWithEmailPassword(email: String, password: String) async throws { + try await Auth.auth().createUser(withEmail: email, password: password) + + if let onSignUp, let user = Auth.auth().currentUser { + onSignUp(user) + } + + authenticationState = .authenticated + } + + func signOut() throws { + try Auth.auth().signOut() + authenticationState = .unauthenticated + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AuthenticationToolbarButton.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AuthenticationToolbarButton.swift new file mode 100644 index 0000000..f7bf15e --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Authentication/AuthenticationToolbarButton.swift @@ -0,0 +1,41 @@ +// 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. + +import SwiftUI + +struct AuthenticationToolbarButton: View { + @Environment(AuthenticationService.self) var authenticationService + + private func onButtonTapped() { + if authenticationService.authenticationState == .unauthenticated { + authenticationService.presentingAuthenticationDialog.toggle() + } else { + authenticationService.presentingAccountDialog.toggle() + } + } +} + +extension AuthenticationToolbarButton { + var body: some View { + Button(action: onButtonTapped) { + Image(systemName: authenticationService + .authenticationState == .unauthenticated ? "person.circle" : "person.circle.fill") + } + } +} + +#Preview { + AuthenticationToolbarButton() + .environment(AuthenticationService()) +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Details/MovieCardView.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Details/MovieCardView.swift new file mode 100644 index 0000000..10f066e --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Details/MovieCardView.swift @@ -0,0 +1,143 @@ +// 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. + +@preconcurrency import FirebaseDataConnect +import FriendlyFlixSDK +import NukeUI +import SwiftUI + +struct MovieCardView: View { + @Environment(\.dismiss) private var dismiss + private var connector = DataConnect.friendlyFlixConnector + + private var showDetails: Bool = false + private var movie: Movie + + public init(showDetails: Bool, movie: Movie) { + self.showDetails = showDetails + self.movie = movie + + isFavouriteRef = connector.getIfFavoritedMovieQuery.ref(movieId: movie.id) + } + + // MARK: - Favourite handling + + private let isFavouriteRef: QueryRefObservation< + GetIfFavoritedMovieQuery.Data, + GetIfFavoritedMovieQuery.Variables + > + private var isFavourite: Bool { + isFavouriteRef.data?.favorite_movie?.movieId != nil + } + + private func toggleFavourite() { + Task { + if isFavourite { + let _ = try await connector.deleteFavoritedMovieMutation.execute(movieId: movie.id) + let _ = try await isFavouriteRef.execute() + } else { + let _ = try await connector.addFavoritedMovieMutation.execute(movieId: movie.id) + let _ = try await isFavouriteRef.execute() + } + } + } +} + +extension MovieCardView { + var body: some View { + CardView(showDetails: showDetails) { + if let imageUrl = URL(string: movie.imageUrl) { + LazyImage(url: imageUrl) { state in + if let image = state.image { + image + .resizable() + .scaledToFill() + .frame(maxWidth: .infinity) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + } else if state.error != nil { + Color.red + .frame(maxWidth: .infinity) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .redacted(if: true) + } else { + Image(systemName: "photo.artframe") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: .infinity) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .redacted(reason: .placeholder) + } + } + .frame(maxWidth: .infinity) + } + } heroTitle: { + VStack(alignment: .leading) { + Spacer() + HStack { + VStack(alignment: .leading) { + Text(movie.title) + .multilineTextAlignment(.leading) + .font(.title) + .bold() + if let releaseYear = movie.releaseYear { + Text("Released: \(format: releaseYear, using: .none)") + } + } + Spacer() + } + .padding() + .background(.thinMaterial) + } + } details: { + MovieDetailsView(movie: movie) + } + .toolbar { + ToolbarItem { + Button { + toggleFavourite() + } label: { + Image(systemName: isFavourite ? "heart.fill" : "heart") + .font(.headline) + .foregroundColor(.white) + .frame(width: 30, height: 30) + .background(Color.black.opacity(0.6)) + .clipShape(Circle()) + } + } + ToolbarItem { + Button { + dismiss() + } label: { + Image(systemName: "xmark") + .font(.headline) + .foregroundColor(.white) + .frame(width: 30, height: 30) + .background(Color.black.opacity(0.6)) + .clipShape(Circle()) + } + } + } + .task { + do { + let _ = try await isFavouriteRef.execute() + } catch { + print(error) + } + } + } +} + +#Preview { + MovieCardView(showDetails: true, movie: Movie.mock) +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Details/MovieDetailsView.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Details/MovieDetailsView.swift new file mode 100644 index 0000000..198a275 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Details/MovieDetailsView.swift @@ -0,0 +1,164 @@ +// 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. + +import FirebaseDataConnect +import FriendlyFlixSDK +import NukeUI +import SwiftUI + +struct MovieDetailsView: View { + private var movie: Movie + + private var movieDetails: MovieDetails? { + DataConnect.friendlyFlixConnector + .getMovieByIdQuery + .ref(id: movie.id) + .data?.movie.map { movieDetails in + MovieDetails( + title: movieDetails.title, + description: movieDetails.description ?? "", + releaseYear: movieDetails.releaseYear, + rating: movieDetails.rating ?? 0, + imageUrl: movieDetails.imageUrl, + mainActors: movieDetails.mainActors.map { mainActor in + MovieActor(id: mainActor.id, + name: mainActor.name, + imageUrl: mainActor.imageUrl) + }, + supportingActors: movieDetails.supportingActors.map { supportingActor in + MovieActor(id: supportingActor.id, + name: supportingActor.name, + imageUrl: supportingActor.imageUrl) + }, + reviews: movieDetails.reviews.map { review in + Review(id: review.id, + reviewText: review.reviewText ?? "", + rating: review.rating ?? 0, + userName: review.user.username) + } + ) + } + } + + public init(movie: Movie) { + self.movie = movie + } +} + +extension MovieDetailsView { + var body: some View { + VStack(alignment: .leading, spacing: 10) { + // description + VStack(alignment: .leading, spacing: 10) { + Text("About") + .font(.title2) + .bold() + .unredacted() + + Text(movie.description) + .font(.body) + HStack { + Spacer() + } + } + + if let movieDetails { + if !movieDetails.mainActors.isEmpty { + actorsSection(title: "Main actors", actors: movieDetails.mainActors) + } + if !movieDetails.supportingActors.isEmpty { + actorsSection(title: "Supporting actors", actors: movieDetails.supportingActors) + } + + // Reviews + DetailsSection("Ratings & Reviews") { + VStack(alignment: .leading) { + HStack(alignment: .center) { + Text("\(movieDetails.rating, specifier: "%.1f")") + .font(.system(size: 64, weight: .bold)) + Spacer() + VStack(alignment: .trailing) { + StarRatingView(rating: Double(movieDetails.rating)) + Text("23 Ratings") + .font(.title) + .bold() + } + } + Text("Most Helpful Reviews") + .font(.title3) + .bold() + ScrollView(.horizontal) { + LazyHStack { + ForEach(movieDetails.reviews) { review in + MovieReviewCard(title: "Feedback", + rating: Double(review.rating), + reviewerName: review.userName, + review: review.reviewText) + .frame(width: 350) + } + } + .scrollTargetLayout() + } + .scrollTargetBehavior(.viewAligned) + .scrollIndicators(.hidden) + } + } + } + } + .padding() + } +} + +extension MovieDetailsView { + func actorsSection(title: String, actors: [MovieActor]) -> some View { + DetailsSection(title) { + ScrollView(.horizontal) { + LazyHStack { + ForEach(actors, id: \.id) { actor in + VStack(alignment: .center) { + if let imageUrl = URL(string: actor.imageUrl) { + LazyImage(url: imageUrl) { state in + if let image = state.image { + image + .resizable() + .scaledToFill() + .frame(width: 96, height: 96, alignment: .center) + .clipShape(Circle()) + } else if state.error != nil { + Color.red + .redacted(if: true) + } else { + Image(systemName: "person.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 96, height: 96, alignment: .center) + .redacted(reason: .placeholder) + .clipShape(Circle()) + } + } + } + Text(actor.name) + .font(.headline) + } + .padding(.horizontal, 8) + } + } + } + } + } +} + +#Preview { + MovieDetailsView(movie: Movie.mock) +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Home/DetailsSection.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Home/DetailsSection.swift new file mode 100644 index 0000000..9bf34d9 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Home/DetailsSection.swift @@ -0,0 +1,76 @@ +// 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. + +import SwiftUI + +struct DetailsSection: View where Title: View, Details: View { + var title: () -> Title + var content: () -> Details + + init(@ViewBuilder _ title: @escaping () -> Title, @ViewBuilder content: @escaping () -> Details) { + self.title = title + self.content = content + } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .center) { + title() + .font(.title2) + .bold() + Image(systemName: "chevron.right") + .font(.title3) + .bold() + .foregroundStyle(Color.secondary) + Spacer() + } + .padding(.bottom, 8) + + content() + } + .padding(.bottom, 20) + } +} + +extension DetailsSection where Title == Text { + init(_ title: Text, content: @escaping () -> Details) { + self.title = { title } + self.content = { content() } + } + + init(_ title: any StringProtocol, content: @escaping () -> Details) { + self.title = { Text(title) } + self.content = { content() } + } +} + +#Preview { + ScrollView { + DetailsSection(Text("Title")) { + Text("Details go here") + } + + DetailsSection("Title as string") { + Text("Details go here") + } + + DetailsSection { + NavigationLink(value: Movie.mock) { + Text("Movie") + } + } content: { + Text("Details go here") + } + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Home/HomeScreen.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Home/HomeScreen.swift new file mode 100644 index 0000000..3f34f2b --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Home/HomeScreen.swift @@ -0,0 +1,128 @@ +// 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. + +@preconcurrency import FirebaseDataConnect +import FriendlyFlixSDK +import SwiftUI + +struct HomeScreen: View { + @Namespace private var namespace + @Environment(AuthenticationService.self) var authenticationService + + private var connector = DataConnect.friendlyFlixConnector + + private var isSignedIn: Bool { + authenticationService.user != nil + } + + private func mapMovie(_ listMovie: ListMoviesQuery.Data.Movie) -> Movie { + .init( + title: listMovie.title, + description: listMovie.description ?? "", + releaseYear: listMovie.releaseYear, + rating: listMovie.rating, + imageUrl: listMovie.imageUrl + ) + } + + private var heroMovies: [Movie] { + connector.listMoviesQuery + .ref { optionalVars in + optionalVars.limit = 3 + optionalVars.orderByReleaseYear = .DESC + } + .data?.movies.map(Movie.init) ?? [] + } + + private var topMovies: [Movie] { + connector.listMoviesQuery + .ref { optionalVars in + optionalVars.limit = 5 + optionalVars.orderByRating = .DESC + } + .data?.movies.map(Movie.init) ?? [] + } + + let watchListRef: QueryRefObservation< + GetUserFavoriteMoviesQuery.Data, + GetUserFavoriteMoviesQuery.Variables + > + private var watchList: [Movie] { + watchListRef.data?.user?.favoriteMovies.map(Movie.init) ?? [] + } + + init() { + watchListRef = connector.getUserFavoriteMoviesQuery.ref() + } +} + +extension HomeScreen { + var body: some View { + NavigationStack { + ScrollView { + TabView { + ForEach(heroMovies) { movie in + NavigationLink(value: movie) { + MovieTeaserView( + title: movie.title, + subtitle: movie.description, + imageUrl: movie.imageUrl + ) + .matchedTransitionSource(id: movie.id, in: namespace) + } + .buttonStyle(.noHighlight) + } + } + .frame(height: 600) + .navigationDestination(for: Movie.self) { movie in + MovieCardView(showDetails: true, movie: movie) + .navigationTransition(.zoom(sourceID: movie.id, in: namespace)) + } + .tabViewStyle(.page(indexDisplayMode: .always)) + + Group { + MovieListSection(namespace: namespace, title: "Top Movies", movies: topMovies) + if isSignedIn { + MovieListSection(namespace: namespace, title: "Watch List", movies: watchList) + .onAppear { + Task { + try await watchListRef.execute() + } + } + } + } + .navigationDestination(for: [Movie].self) { movies in + MovieListScreen(namespace: namespace, movies: movies) + } + .navigationDestination(for: SectionedMovie.self) { sectionedMovie in + MovieCardView(showDetails: true, movie: sectionedMovie.movie) + .navigationTransition(.zoom(sourceID: sectionedMovie.id, in: namespace)) + } + .padding() + } + .navigationTitle("Home") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + AuthenticationToolbarButton() + } + } + .ignoresSafeArea() + } + } +} + +#Preview { + HomeScreen() + .environment(AuthenticationService()) +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Home/MovieTeaserView.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Home/MovieTeaserView.swift new file mode 100644 index 0000000..df00f83 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Home/MovieTeaserView.swift @@ -0,0 +1,77 @@ +// 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. + +import NukeUI +import SwiftUI + +struct MovieTeaserView: View { + var title: String + var subtitle: String + var imageUrl: String + + var body: some View { + ZStack(alignment: .bottom) { + GeometryReader { geometry in + if let imageUrl = URL(string: imageUrl) { + LazyImage(url: imageUrl) { state in + if let image = state.image { + image + .resizable() + .scaledToFill() + .frame(width: geometry.size.width) + .clipped() + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + } else if state.error != nil { + Color.red + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .redacted(if: true) + } else { + Image(systemName: "photo.artframe") + .resizable() + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .redacted(reason: .placeholder) + } + } + } + } + VStack { + Text(title) + .font(.largeTitle) + .foregroundStyle(.white) + Text(subtitle) + .font(.body) + .foregroundStyle(.white) + } + .padding(.horizontal) + .padding(.vertical, 60) + .background { + LinearGradient( + gradient: Gradient(colors: [.clear, .black.opacity(0.9)]), + startPoint: .top, + endPoint: .bottom + ) + } + } + } +} + +#Preview { + let movie = Movie.mock + MovieTeaserView( + title: movie.title, + subtitle: movie.description, + imageUrl: movie.imageUrl + ) + .frame(maxHeight: 400) +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Library/LibraryScreen.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Library/LibraryScreen.swift new file mode 100644 index 0000000..cf3a5c7 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Library/LibraryScreen.swift @@ -0,0 +1,101 @@ +// 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. + +@preconcurrency import FirebaseDataConnect +import FriendlyFlixSDK +import SwiftUI + +struct LibraryScreen: View { + @Namespace var namespace + @Environment(AuthenticationService.self) var authenticationService + + private var connector = DataConnect.friendlyFlixConnector + + private var isSignedIn: Bool { + authenticationService.user != nil + } + + init() { + watchListRef = connector.getUserFavoriteMoviesQuery.ref() + } + + private let watchListRef: QueryRefObservation< + GetUserFavoriteMoviesQuery.Data, + GetUserFavoriteMoviesQuery.Variables + > + private var watchList: [Movie] { + watchListRef.data?.user?.favoriteMovies.map(Movie.init) ?? [] + } + + private func presentSignInDialog() { + authenticationService.presentingAuthenticationDialog.toggle() + } +} + +extension LibraryScreen { + var body: some View { + NavigationStack { + ScrollView { + if isSignedIn { + Group { + MovieListSection(namespace: namespace, title: "Watch List", movies: watchList) + .onAppear { + Task { + try await watchListRef.execute() + } + } + // TODO: insert section with list of all movies the user has rated + } + .padding() + } + } + .navigationTitle("Library") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + AuthenticationToolbarButton() + } + } + .navigationDestination(for: Movie.self) { movie in + MovieCardView(showDetails: true, movie: movie) + .navigationTransition(.zoom(sourceID: movie.id, in: namespace)) + } + .navigationDestination(for: [Movie].self) { movies in + MovieListScreen(namespace: namespace, movies: movies) + } + .navigationDestination(for: SectionedMovie.self) { sectionedMovie in + MovieCardView(showDetails: true, movie: sectionedMovie.movie) + .navigationTransition(.zoom(sourceID: sectionedMovie.id, in: namespace)) + } + } + .overlay { + if !isSignedIn { + ContentUnavailableView { + Label("Your library is empty", systemImage: "rectangle.on.rectangle.slash") + } description: { + VStack { + Text("Your watch list and favourites will appear here once you sign in.") + Button(action: presentSignInDialog) { + Text("Sign in") + } + } + } + } + } + } +} + +#Preview { + LibraryScreen() + .environment(AuthenticationService()) +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/MovieList/MovieListScreen.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/MovieList/MovieListScreen.swift new file mode 100644 index 0000000..573b736 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/MovieList/MovieListScreen.swift @@ -0,0 +1,44 @@ +// 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. + +import SwiftUI + +struct MovieListScreen: View { + var namespace: Namespace.ID + var movies: [Movie] + + var body: some View { + List { + ForEach(movies) { movie in + MovieListRowView( + title: movie.title, + subtitle: movie.description, + imageUrl: movie.imageUrl + ) + .matchedTransitionSource(id: movie.id, in: namespace) + .navigationLink(value: movie, hideChevron: true) + } + } + .listStyle(.plain) + } +} + +#Preview { + @Previewable @Namespace var namespace + NavigationStack { + MovieListScreen(namespace: namespace, movies: Movie.mockList) + .navigationTitle("Continue watching?") + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Reviews/MovieReviewCard.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Reviews/MovieReviewCard.swift new file mode 100644 index 0000000..e8211e0 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Reviews/MovieReviewCard.swift @@ -0,0 +1,64 @@ +// 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. + +import SwiftUI + +struct MovieReviewCard: View { + var title: String + var rating: Double + var reviewerName: String + var review: String + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(title) + .font(.headline) + HStack { + StarRatingView(rating: rating) + Text("·") + Text(reviewerName) + } + .font(.subheadline) + Text(review) + Spacer() + } + .padding(16) + .frame(height: 200) + .background(Color(UIColor.secondarySystemBackground)) + .clipShape( + UnevenRoundedRectangle( + cornerRadii: .init( + topLeading: 16, + bottomLeading: 16, + bottomTrailing: 16, + topTrailing: 16 + ), + style: .continuous + ) + ) + } +} + +#Preview { + ScrollView { + MovieReviewCard( + title: "Really great", + rating: 4.5, + reviewerName: "John Doe", + review: + "Velit officia quis ut ut dolor velit voluptate magna Lorem. Sint do ex adipisicing laboris magna et duis aute fugiat culpa minim id culpa nulla do. Occaecat in anim ad Lorem eu aute consectetur excepteur fugiat laboris eiusmod. Et tempor Lorem quis eu magna cillum adipisicing consectetur." + ) + .padding() + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Reviews/StarRatingView.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Reviews/StarRatingView.swift new file mode 100644 index 0000000..789171e --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Reviews/StarRatingView.swift @@ -0,0 +1,38 @@ +// 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. + +import SwiftUI + +struct StarRatingView: View { + var rating: Double + + var body: some View { + HStack(spacing: 4) { + ForEach(0 ..< 5) { index in + Image(systemName: self.starType(for: index)) + .foregroundColor(.yellow) + } + } + } + + func starType(for index: Int) -> String { + if rating > Double(index) + 0.75 { + return "star.fill" + } else if rating > Double(index) + 0.25 { + return "star.lefthalf.fill" + } else { + return "star" + } + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Search/SearchScreen.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Search/SearchScreen.swift new file mode 100644 index 0000000..8db8e8e --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Features/Search/SearchScreen.swift @@ -0,0 +1,107 @@ +// 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. + +import FirebaseDataConnect +import FriendlyFlixSDK +import SwiftUI + +struct SearchedView: View { + @Environment(\.isSearching) private var isSearching + var namespace: Namespace.ID + var filteredMovies = [Movie]() + var connector = DataConnect.friendlyFlixConnector + + private var topMovies: [Movie] { + connector.listMoviesQuery + .ref { optionalVars in + optionalVars.limit = 5 + optionalVars.orderByRating = .DESC + } + .data?.movies.map(Movie.init) ?? [] + } + + var body: some View { + if !isSearching { + MovieListSection(namespace: namespace, title: "Top Movies", movies: topMovies) + } else { + ForEach(filteredMovies) { movie in + NavigationLink(value: movie) { + MovieListRowView( + title: movie.title, + subtitle: movie.description, + imageUrl: movie.imageUrl + ) + .matchedTransitionSource(id: movie.id, in: namespace) + } + .buttonStyle(.noHighlight) + } + } + } +} + +struct SearchScreen: View { + @State private var searchText: String = "" + @State private var isStatusBarHidden = false + @Namespace private var namespace + + var connector = DataConnect.friendlyFlixConnector + + private var filteredMovies: [Movie] { + connector.listMoviesByPartialTitleQuery + .ref(searchTerm: searchText.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)) + .data?.movies.map(Movie.init) ?? [] + } +} + +extension SearchScreen { + var body: some View { + NavigationStack { + ScrollView { + SearchedView(namespace: namespace, filteredMovies: filteredMovies) + .searchable(text: $searchText) + .textInputAutocapitalization(.never) + } + .padding() + .navigationTitle("Search") + .navigationDestination(for: Movie.self) { movie in + MovieCardView(showDetails: true, movie: movie) + .navigationTransition(.zoom(sourceID: movie.id, in: namespace)) + .task { + // NavigationStack requires `.statusBarHidden` to be applied to the navigationstack + // itself, not on any children. + // See https://danielsaidi.com/blog/2023/03/14/handling-status-bar-color-scheme-and-visibility-in-swiftui + isStatusBarHidden = true + } + } + .navigationDestination(for: [Movie].self) { movies in + MovieListScreen(namespace: namespace, movies: movies) + } + .navigationDestination(for: SectionedMovie.self) { sectionedMovie in + MovieCardView(showDetails: true, movie: sectionedMovie.movie) + .navigationTransition(.zoom(sourceID: sectionedMovie.id, in: namespace)) + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + AuthenticationToolbarButton() + } + } + } + .statusBarHidden(isStatusBarHidden) + } +} + +#Preview { + SearchScreen() + .environment(AuthenticationService()) +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Model/Mockable.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Model/Mockable.swift new file mode 100644 index 0000000..c5bca77 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Model/Mockable.swift @@ -0,0 +1,30 @@ +// 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. + +public protocol Mockable { + associatedtype MockType + + static var mock: MockType { get } + static var mockList: [MockType] { get } +} + +public extension Mockable { + static var mock: MockType { + mockList[0] + } + + static var mockList: [MockType] { + [] + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Model/Movie+DataConnect.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Model/Movie+DataConnect.swift new file mode 100644 index 0000000..798faf5 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Model/Movie+DataConnect.swift @@ -0,0 +1,45 @@ +// 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. + +import FirebaseDataConnect +import FriendlyFlixSDK + +extension Movie { + init(from: ListMoviesQuery.Data.Movie) { + id = from.id + title = from.title + description = from.description ?? "" + releaseYear = from.releaseYear + rating = from.rating + imageUrl = from.imageUrl + } + + init(from: ListMoviesByPartialTitleQuery.Data.Movie) { + id = from.id + title = from.title + description = from.description ?? "" + releaseYear = from.releaseYear + rating = from.rating + imageUrl = from.imageUrl + } + + init(from: GetUserFavoriteMoviesQuery.Data.User.FavoriteMovieFavoriteMovies) { + id = from.movie.id + title = from.movie.title + description = from.movie.description ?? "" + releaseYear = from.movie.releaseYear + rating = from.movie.rating + imageUrl = from.movie.imageUrl + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Model/Movie.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Model/Movie.swift new file mode 100644 index 0000000..d8fb712 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Model/Movie.swift @@ -0,0 +1,143 @@ +// 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. + +import Foundation +import SwiftUI + +struct Review: Identifiable, Hashable { + let id: UUID + var reviewText: String + var rating: Int + var userName: String +} + +struct MovieActor: Identifiable, Hashable { + var id: UUID + var name: String + var imageUrl: String +} + +struct MovieDetails: Identifiable, Hashable { + let id: UUID + let title: String + let description: String + let releaseYear: Int? + var rating: Double + let imageUrl: String + + let mainActors: [MovieActor] + let supportingActors: [MovieActor] + let reviews: [Review] + + init(id: UUID = UUID(), + title: String, + description: String, + releaseYear: Int?, + rating: Double, + imageUrl: String, + mainActors: [MovieActor], + supportingActors: [MovieActor], + reviews: [Review]) { + self.id = id + self.title = title + self.description = description + self.releaseYear = releaseYear + self.rating = rating + self.imageUrl = imageUrl + self.mainActors = mainActors + self.supportingActors = supportingActors + self.reviews = reviews + } +} + +struct Movie: Identifiable, Hashable { + let id: UUID + let title: String + let description: String + let releaseYear: Int? + var rating: Double? + let imageUrl: String + + init(id: UUID = UUID(), + title: String, + description: String, + releaseYear: Int?, + rating: Double? = nil, + imageUrl: String) { + self.id = id + self.title = title + self.description = description + self.releaseYear = releaseYear + self.rating = rating + self.imageUrl = imageUrl + } +} + +extension Movie: Mockable { + static let mockList: [Movie] = [ + .init( + title: "The Hitchhiker's Guide to the Galaxy", + description: + "Mere seconds before the Earth is to be demolished by an alien construction crew, Arthur Dent is swept off the planet by his friend Ford Prefect, a researcher penning a new edition of \"The Hitchhiker's Guide to the Galaxy.\"", + releaseYear: 2005, + imageUrl: "https://image.tmdb.org/t/p/w1280/yr9A3KGQlxBh3yW0cmglsr8aMIz.jpg" + ), + .init( + title: "Interstellar", + description: + "The adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.", + releaseYear: 2005, + imageUrl: "https://image.tmdb.org/t/p/w1280/gEU2QniE6E77NI6lCU6MxlNBvIx.jpg" + ), + .init( + title: "The Matrix", + description: + "Set in the 22nd century, The Matrix tells the story of a computer hacker who joins a group of underground insurgents fighting the vast and powerful computers who now rule the earth.", + releaseYear: 1999, + imageUrl: "https://image.tmdb.org/t/p/w1280/f89U3ADr1oiB1s9GkdPOEpXUk5H.jpg" + ), + .init( + title: "Titanic", + description: + "101-year-old Rose DeWitt Bukater tells the story of her life aboard the Titanic, 84 years later. A young Rose boards the ship with her mother and fiancé. Meanwhile, Jack Dawson and Fabrizio De Rossi win third-class tickets aboard the ship. Rose tells the whole story from Titanic's departure through to its death—on its first and last voyage—on April 15, 1912.", + releaseYear: 1998, + imageUrl: "https://image.tmdb.org/t/p/w1280/tuvoTDlqaLm7hFUROR6u0OUtwCW.jpg" + ), + .init( + title: "Slow Horses", + description: + "Follow a dysfunctional team of MI5 agents—and their obnoxious boss, the notorious Jackson Lamb—as they navigate the espionage world's smoke and mirrors to defend England from sinister forces.", + releaseYear: 2022, + imageUrl: "https://image.tmdb.org/t/p/w1280/dnpatlJrEPiDSn5fzgzvxtiSnMo.jpg" + ), + .init( + title: "Tomorrow Never Dies", + description: + "A deranged media mogul is staging international incidents to pit the world's superpowers against each other. Now James Bond must take on this evil mastermind in an adrenaline-charged battle to end his reign of terror and prevent global pandemonium.", + releaseYear: 1997, + imageUrl: "https://image.tmdb.org/t/p/w1280/bkEJA84af63IpaOPP4CbbgTiTlL.jpg" + ), + .init( + title: "The man from U.N.C.L.E.", + description: + "At the height of the Cold War, a mysterious criminal organization plans to use nuclear weapons and technology to upset the fragile balance of power between the United States and Soviet Union. CIA agent Napoleon Solo and KGB agent Illya Kuryakin are forced to put aside their hostilities and work together to stop the evildoers in their tracks. The duo's only lead is the daughter of a missing German scientist, whom they must find soon to prevent a global catastrophe.", + releaseYear: 2015, + imageUrl: "https://image.tmdb.org/t/p/w1280/nFiu4lLhkyf1amaGaN6pNoUn5Ly.jpg" + ), + ] + + static let featured = [Movie](mockList.filter { $0.title.contains("The") }) + static let topMovies = [Movie](mockList.prefix(3)) + static let watchList = [Movie](mockList.suffix(5)) +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/ButtonStyle+NoHighlight.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/ButtonStyle+NoHighlight.swift new file mode 100644 index 0000000..dbb0386 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/ButtonStyle+NoHighlight.swift @@ -0,0 +1,25 @@ +// 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. + +import SwiftUI + +struct NoHighlightButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + } +} + +extension ButtonStyle where Self == NoHighlightButtonStyle { + static var noHighlight: NoHighlightButtonStyle { NoHighlightButtonStyle() } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/Color+Hex.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/Color+Hex.swift new file mode 100644 index 0000000..36e3804 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/Color+Hex.swift @@ -0,0 +1,44 @@ +// 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. + +import SwiftUI + +extension Color { + init(hex: String, opacity: Double = 1.0) { + var hexFormatted: String = hex.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + .uppercased() + + if hexFormatted.hasPrefix("#") { + hexFormatted = String(hexFormatted.dropFirst()) + } + + assert(hexFormatted.count == 6 || hexFormatted.count == 8, "Invalid hex code") + + var rgbValue: UInt64 = 0 + Scanner(string: hexFormatted).scanHexInt64(&rgbValue) + + var alpha: Double = opacity + if hexFormatted.count == 8 { + alpha = Double((rgbValue & 0xFF00_0000) >> 24) / 255.0 + } + + let red = Double((rgbValue & 0x00FF_0000) >> 16) / 255.0 + let green = Double((rgbValue & 0x0000_FF00) >> 8) / 255.0 + let blue = Double(rgbValue & 0x0000_00FF) / 255.0 + + self + .init(.sRGB, red: red, green: green, blue: blue, + opacity: alpha) // Use alpha instead of opacity + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/String+Placeholder.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/String+Placeholder.swift new file mode 100644 index 0000000..9daeba9 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/String+Placeholder.swift @@ -0,0 +1,21 @@ +// 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. + +import Foundation + +extension String { + static func placeholder(length: Int) -> String { + String(Array(repeating: "X", count: length)) + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/String+StringInterpolation.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/String+StringInterpolation.swift new file mode 100644 index 0000000..0bcf514 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/String+StringInterpolation.swift @@ -0,0 +1,26 @@ +// 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. + +import Foundation + +extension String.StringInterpolation { + mutating func appendInterpolation(format value: Int, using style: NumberFormatter.Style) { + let formatter = NumberFormatter() + formatter.numberStyle = style + + if let result = formatter.string(from: value as NSNumber) { + appendLiteral(result) + } + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/View+Extension.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/View+Extension.swift new file mode 100644 index 0000000..69356d8 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Utilities/View+Extension.swift @@ -0,0 +1,43 @@ +// 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. + +import SwiftUI + +extension View { + @ViewBuilder + func redacted(if condition: @autoclosure () -> Bool) -> some View { + redacted(reason: condition() ? .placeholder : []) + } +} + +extension View { + @ViewBuilder + func navigationLink(value: any Hashable, hideChevron: Bool = false) -> some View { + if hideChevron { + // If hideChevron is true, apply the overlay trick to hide the chevron + // Put the NavigationLink into an overlay, and set its opacity to zero. By using this trick, + // we can hide the chevron. + // Source: https://www.reddit.com/r/SwiftUI/comments/13rhg02/how_can_i_use_navigationlink_inside_list_without/jlkqbkz/ + overlay { + NavigationLink(value: value) { + EmptyView() + } + .opacity(0) + } + } else { + // If hideChevron is false, return the original view without modification + self + } + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/CardView.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/CardView.swift new file mode 100644 index 0000000..a670842 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/CardView.swift @@ -0,0 +1,222 @@ +// 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. + +import SwiftUI + +// Try for blendmode https://saeedrz.medium.com/unleashing-creativity-a-deep-dive-into-blendmode-in-swiftui-2edc3f204fa8 + +struct CardView: View { + @Environment(\.dismiss) var dismiss + + var showDetails = true + + var hero: some View { + heroView() + } + + var heroView: () -> Hero + var heroTitle: () -> Title + var details: () -> Details + + public init(showDetails: Bool = false, @ViewBuilder heroView: @escaping () -> Hero, + @ViewBuilder heroTitle: @escaping () -> Title, + @ViewBuilder details: @escaping () -> Details) { + self.showDetails = showDetails + self.heroView = heroView + self.heroTitle = heroTitle + self.details = details + } + + var cardView: some View { + VStack(spacing: 0) { + ZStack(alignment: .topLeading) { + hero + heroTitle() + } + .clipShape( + UnevenRoundedRectangle( + cornerRadii: .init( + topLeading: cornerRadius, + bottomLeading: showDetails ? 0 : 16, + bottomTrailing: showDetails ? 0 : 16, + topTrailing: cornerRadius + ), + style: .continuous + ) + ) + .padding(showDetails ? 0 : 16) + .shadow(radius: showDetails ? 0 : 8) + + if showDetails { + details() + .background(Color(UIColor.systemBackground)) + } + } + } + + @State var scaleFactor: CGFloat = 1 + @State var cornerRadius: CGFloat = 16 + + var body: some View { + if showDetails { + ScrollView { + cardView + .scaleEffect(scaleFactor) + .navigationBarBackButtonHidden(true) + .statusBarHidden() + } + .ignoresSafeArea() + + // Start: drag down to pop back + .background(Color(UIColor.secondarySystemBackground)) + .scrollIndicators(scaleFactor < 1 ? .hidden : .automatic, axes: .vertical) + .onScrollGeometryChange(for: CGFloat.self) { geometry in + geometry.contentOffset.y + } action: { oldValue, newValue in + if newValue >= 0 { + scaleFactor = 1 + cornerRadius = 16 + } else { + scaleFactor = 1 - (0.1 * (newValue / -50)) + cornerRadius = 55 - (35 / 50 * -newValue) + } + } + .onScrollGeometryChange(for: Bool.self) { geometry in + geometry.contentOffset.y < -50 + } action: { oldValue, newValue in + if newValue { + dismiss() + } + } + // End: drag down to pop back + } else { + cardView + .foregroundColor(.primary) + } + } +} + +#Preview { + NavigationStack { + CardView(showDetails: true) { + GradientView(configuration: GradienConfiguration.sample) + .frame(height: 450) + } heroTitle: { + VStack(alignment: .leading) { + Spacer() + Text("Here be titles") + .font(.title) + Text("And subtitles") + .font(.title3) + } + .foregroundColor(Color(UIColor.systemGroupedBackground)) + .padding() + } details: { + VStack { + Text( + """ + Amet culpa excepteur sit ad tempor minim aute anim nisi voluptate do. Exercitation nisi adipisicing esse officia sit ullamco. + Tempor ullamco irure proident cupidatat non Lorem ut voluptate est ad in deserunt esse velit exercitation. Tempor voluptate ex aute id. + Fugiat in minim labore minim duis et duis eiusmod ullamco eiusmod minim deserunt voluptate. + """ + ) + .font(.body) + .padding() + } + } + } +} + +#Preview { + ScrollView { + CardView(showDetails: false) { + GradientView(configuration: GradienConfiguration.sample) + .frame(height: 450) + } heroTitle: { + VStack(alignment: .leading) { + Spacer() + Text("Here be titles") + .font(.title) + Text("And subtitles") + .font(.title3) + } + .foregroundColor(Color(UIColor.systemGroupedBackground)) + .padding() + } details: { + VStack { + Text( + """ + Amet culpa excepteur sit ad tempor minim aute anim nisi voluptate do. Exercitation nisi adipisicing esse officia sit ullamco. + Tempor ullamco irure proident cupidatat non Lorem ut voluptate est ad in deserunt esse velit exercitation. Tempor voluptate ex aute id. + Fugiat in minim labore minim duis et duis eiusmod ullamco eiusmod minim deserunt voluptate. + """ + ) + .font(.body) + .padding() + } + } + CardView(showDetails: false) { + GradientView(configuration: GradienConfiguration.samples[1]) + .frame(height: 450) + } heroTitle: { + VStack(alignment: .leading) { + Spacer() + Text("Here be titles") + .font(.title) + Text("And subtitles") + .font(.title3) + } + .foregroundColor(Color(UIColor.systemGroupedBackground)) + .padding() + } details: { + VStack { + Text( + """ + Amet culpa excepteur sit ad tempor minim aute anim nisi voluptate do. Exercitation nisi adipisicing esse officia sit ullamco. + Tempor ullamco irure proident cupidatat non Lorem ut voluptate est ad in deserunt esse velit exercitation. Tempor voluptate ex aute id. + Fugiat in minim labore minim duis et duis eiusmod ullamco eiusmod minim deserunt voluptate. + """ + ) + .font(.body) + .padding() + } + } + CardView(showDetails: false) { + GradientView(configuration: GradienConfiguration.samples[2]) + .frame(height: 450) + } heroTitle: { + VStack(alignment: .leading) { + Spacer() + Text("Here be titles") + .font(.title) + Text("And subtitles") + .font(.title3) + } + .foregroundColor(Color(UIColor.systemGroupedBackground)) + .padding() + } details: { + VStack { + Text( + """ + Amet culpa excepteur sit ad tempor minim aute anim nisi voluptate do. Exercitation nisi adipisicing esse officia sit ullamco. + Tempor ullamco irure proident cupidatat non Lorem ut voluptate est ad in deserunt esse velit exercitation. Tempor voluptate ex aute id. + Fugiat in minim labore minim duis et duis eiusmod ullamco eiusmod minim deserunt voluptate. + """ + ) + .font(.body) + .padding() + } + } + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/GradienConfiguration.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/GradienConfiguration.swift new file mode 100644 index 0000000..b0c025f --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/GradienConfiguration.swift @@ -0,0 +1,150 @@ +// 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. + +import SwiftUI + +let neonSunsetBlurb = + """ + Immerse yourself in the captivating allure of Neon Sunset, a mesmerizing gradient that seamlessly blends the warmth of daylight with the electric energy of the night. This vibrant spectrum captures the fleeting magic of twilight, where the sun dips below the horizon, painting the sky with a kaleidoscope of fiery oranges, deep pinks, and electrifying purples. + + Neon Sunset is not merely a color scheme; it's an experience. It evokes a sense of wonder, igniting the imagination and transporting you to a world where boundaries blur and possibilities abound. Whether you're designing a website, creating artwork, or simply seeking a visual feast for the eyes, Neon Sunset will infuse your projects with an undeniable vibrancy and modern flair. + + Embrace the dynamism of this gradient, as it effortlessly transitions from warm to cool tones. Let the radiant oranges and yellows awaken your creativity, while the deep purples and pinks add a touch of mystery and intrigue. Neon Sunset is a versatile tool that can be adapted to suit a wide range of styles and moods, making it an essential addition to any designer's toolkit. + + Unleash the power of Neon Sunset and watch your creations come alive with a captivating energy that is both bold and sophisticated. This gradient is more than just a visual element; it's a statement. It embodies a spirit of innovation, a celebration of individuality, and a passion for pushing boundaries. + + So, step into the world of Neon Sunset and let your imagination soar. Embrace the vibrant hues, the seamless transitions, and the electrifying energy. Discover a gradient that will elevate your designs, captivate your audience, and leave a lasting impression. + """ + +let prismaticDawnBlurb = + """ + Prismatic Dawn is an enchanting gradient that captures the ephemeral beauty of sunrise, where the first rays of light kiss the horizon and transform the world into a canvas of vibrant hues. This captivating spectrum evokes a sense of wonder, joy, and limitless possibilities. + + Immerse yourself in the mesmerizing blend of soft pastels and bold primary colors. Let the gentle pinks, blues, and purples soothe your senses, while the fiery oranges and yellows ignite your creativity. Prismatic Dawn is a celebration of diversity and harmony, where each color plays a vital role in creating a symphony of light and energy. + + Embrace the fluidity of this gradient, as it seamlessly transitions from delicate shades to vivid bursts of brilliance. Whether you're designing a website, crafting artwork, or simply seeking visual inspiration, Prismatic Dawn will infuse your projects with an undeniable charm and optimism. + + Unleash the power of Prismatic Dawn and watch your creations radiate with a captivating energy that is both playful and sophisticated. This gradient is more than just a visual element; it's an invitation to embrace the magic of color and the boundless potential of creativity. + + Step into the world of Prismatic Dawn and let your imagination dance with light. Explore the harmonious blend of hues, the gentle transitions, and the vibrant energy. Discover a gradient that will elevate your designs, inspire your audience, and leave a lasting impression. + """ + +let serenityDuskBlurb = + """ + Serenity Dusk is a captivating gradient that captures the peaceful transition between day and night. This mesmerizing blend of soft blues and warm pinks evokes a sense of tranquility, introspection, and gentle beauty. + + Immerse yourself in the calming embrace of this gradient, as it seamlessly blends cool and warm tones. Let the soothing blues wash over you, while the delicate pinks awaken a sense of hope and optimism. Serenity Dusk is a celebration of balance and harmony, where opposing forces come together to create a breathtaking visual experience. + + Embrace the subtlety of this gradient, as it gently shifts from one hue to another. Whether you're designing a website, crafting artwork, or simply seeking a moment of visual respite, Serenity Dusk will infuse your projects with a sense of peace and tranquility. + + Unleash the power of Serenity Dusk and watch your creations radiate with a captivating energy that is both calming and uplifting. This gradient is more than just a visual element; it's an invitation to embrace the beauty of stillness and the transformative power of color. + + Step into the world of Serenity Dusk and let your senses be enveloped by its gentle embrace. Explore the harmonious blend of hues, the soft transitions, and the subtle vibrancy. Discover a gradient that will elevate your designs, inspire your audience, and leave a lasting impression of serenity and beauty. + """ + +struct GradienConfiguration: Identifiable, Hashable { + let id = UUID().uuidString + let name: String + let tagLine: String + let subLine: String + let description: String + let points: [SIMD2] + let colors: [Color] +} + +extension GradienConfiguration { + var dimension: Int { + Int(Double(points.count).squareRoot()) + } + + static let samples: [Self] = [ + .init( + name: "Prismatic Dawn", + tagLine: "Where Light Dances with Color", + subLine: "Vivid, eye-catching, modern design.", + description: neonSunsetBlurb, + points: [ + [0.0, 0.0], [0.5, 0.0], [1.0, 0.0], + [0.0, 0.5], [0.6, 0.5], [1.0, 0.5], + [0.0, 1.0], [0.5, 1.0], [1.0, 1.0], + ], + colors: [ + .red, .purple, .indigo, + .orange, .white, .blue, + .yellow, .green, .mint, + ] + ), + .init( + name: "Neon Sunset", + tagLine: "Where the Day Meets the Night", + subLine: "Playful, vibrant, and energetic.", + description: prismaticDawnBlurb, + points: [ + [0.0, 0.0], [1.0, 0.0], + [0.0, 1.0], [1.0, 1.0], + ], + colors: [ + .red, .indigo, + .yellow, .blue, + ] + ), + .init( + name: "Serenity Dusk", + tagLine: "Where tranquility meets vibrancy", + subLine: "Embrace the calming hues of twilight.", + description: serenityDuskBlurb, + points: [ + [0.0, 0.0], [1.0, 0.0], + [0.0, 1.0], [1.0, 1.0], + ], + colors: [ + Color(hex: "#5a82e5"), Color(hex: "#ce6a7e"), + Color(hex: "#5084e9"), Color(hex: "#d66871"), + ] + ), + ] + + static let sample = Self.samples[0] +} + +struct GradientView: View { + var configuration: GradienConfiguration + @State private var isAnimating = false + var body: some View { + VStack { + if #available(iOS 18, macOS 15, tvOS 18, watchOS 11, *) { + MeshGradient( + width: configuration.dimension, + height: configuration.dimension, + points: configuration.points, + colors: configuration.colors, + smoothsColors: true, + colorSpace: .perceptual + ) + } else { + EllipticalGradient(colors: configuration.colors) + } + } + .ignoresSafeArea() + .onAppear { + withAnimation(.easeInOut(duration: 1).repeatForever(autoreverses: true)) { + isAnimating.toggle() + } + } + } +} + +#Preview { + GradientView(configuration: GradienConfiguration.sample) +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/MovieListRowView.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/MovieListRowView.swift new file mode 100644 index 0000000..93e115e --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/MovieListRowView.swift @@ -0,0 +1,76 @@ +// 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. + +import NukeUI +import SwiftUI + +struct MovieListRowView: View { + var title: String + var subtitle: String + var imageUrl: String + + var body: some View { + HStack(alignment: .top) { + if let imageUrl = URL(string: imageUrl) { + LazyImage(url: imageUrl) { state in + if let image = state.image { + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 150, height: 75) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + } else if state.error != nil { + Color.red + .frame(width: 150, height: 75) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .redacted(if: true) + } else { + Image(systemName: "photo.artframe") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 150, height: 75) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .redacted(reason: .placeholder) + } + } + .frame(width: 150, height: 75) + } + VStack(alignment: .leading, spacing: 4) { + Text(title) + .lineLimit(2) + .font(.headline) + Text(subtitle) + .lineLimit(2) + .font(.subheadline) + } + Spacer() + } + .contentShape(Rectangle()) // ensure entire frame is clickable + } +} + +#Preview { + let movie = Movie.mock + MovieListRowView(title: movie.title, subtitle: movie.description, imageUrl: movie.imageUrl) +} + +#Preview { + NavigationStack { + List(Movie.mockList) { movie in + MovieListRowView(title: movie.title, subtitle: movie.description, imageUrl: movie.imageUrl) + } + .listStyle(.plain) + .navigationTitle("Movies") + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/MovieListSection.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/MovieListSection.swift new file mode 100644 index 0000000..a4b4921 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/MovieListSection.swift @@ -0,0 +1,76 @@ +// 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. + +import SwiftUI + +struct SectionedMovie: Identifiable, Hashable { + var id = UUID() + var movie: Movie + + static func == (lhs: SectionedMovie, rhs: SectionedMovie) -> Bool { + lhs.id == rhs.id && lhs.movie.id == rhs.movie.id // Assuming MovieRepresentable has an id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +struct MovieListSection: View { + // pass namespace from parent view, see https://forums.developer.apple.com/forums/thread/651996 + var namespace: Namespace.ID + var title: String + var movies: [Movie] + + private var sectionedMovies: [SectionedMovie] { + movies.map { movie in + SectionedMovie(movie: movie) + } + } + + var body: some View { + DetailsSection { + NavigationLink(value: movies) { + Text(title) + } + .buttonStyle(.noHighlight) + } content: { + ScrollView(.horizontal) { + LazyHStack { + ForEach(sectionedMovies) { sectionedMovie in + NavigationLink(value: sectionedMovie) { + MovieTileView( + title: sectionedMovie.movie.title, + imageUrl: sectionedMovie.movie.imageUrl, + averageRating: sectionedMovie.movie.rating ?? 0, + userRating: 10 + ) + .frame(maxWidth: 150, maxHeight: 300) + .matchedTransitionSource(id: sectionedMovie.id, in: namespace) + } + .buttonStyle(.noHighlight) + } + } + } + .scrollIndicators(.never) + } + } +} + +#Preview { + @Previewable @Namespace var namespace + NavigationStack { + MovieListSection(namespace: namespace, title: "Top Movies", movies: Movie.topMovies) + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/MovieTileView.swift b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/MovieTileView.swift new file mode 100644 index 0000000..2e0b845 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlix/FriendlyFlix/Views/MovieTileView.swift @@ -0,0 +1,83 @@ +// 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. + +import NukeUI +import SwiftUI + +struct MovieTileView: View { + var title: String = "The Matrix" + var imageUrl: String + var averageRating: Double = 5.7 + var userRating: Double = 9 + + let gradient = LinearGradient( + colors: [.blue, .green], + startPoint: .leading, + endPoint: .trailing + ) + + private let star = Image(systemName: "star.fill") + + var body: some View { + VStack(alignment: .leading) { + if let imageUrl = URL(string: imageUrl) { + LazyImage(url: imageUrl) { state in + if let image = state.image { + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 150, height: 200) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + } else if state.error != nil { + Color.red + .frame(width: 150, height: 200) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .redacted(if: true) + } else { + Image(systemName: "photo.artframe") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 150, height: 200) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .redacted(reason: .placeholder) + } + } + .frame(width: 150, height: 200) + } + Text(title) + .lineLimit(1) + .font(.headline) + HStack { + Text(star) + .foregroundColor(.yellow) + Text(" ") + Text("\(averageRating, specifier: "%.1f")") + + Text(" ") + Text(star) + .foregroundColor(.blue) + Text(" ") + Text("\(userRating, specifier: "%.1f")") + } + } + } +} + +#Preview { + ScrollView(.horizontal) { + LazyHStack { + ForEach(Movie.featured) { movie in + MovieTileView(title: movie.title, + imageUrl: movie.imageUrl, + averageRating: 8, + userRating: 10) + .frame(width: 200, height: 300) + } + } + } +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlixSDK/Package.swift b/Examples/FriendlyFlix/app/FriendlyFlixSDK/Package.swift new file mode 100644 index 0000000..bd7f410 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlixSDK/Package.swift @@ -0,0 +1,47 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +// 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. + +import PackageDescription + +let package = Package( + name: "FriendlyFlixSDK", + platforms: [ + .iOS(.v17), + .macOS(.v14), + .watchOS(.v10), + .tvOS(.v17), + ], + products: [ + .library( + name: "FriendlyFlixSDK", + targets: ["FriendlyFlixSDK"] + ), + ], + dependencies: [ + .package(url: "https://github.com/firebase/data-connect-ios-sdk", from: "11.3.0-beta"), + + ], + targets: [ + .target( + name: "FriendlyFlixSDK", + dependencies: [ + .product(name: "FirebaseDataConnect", package: "data-connect-ios-sdk"), + ], + path: "Sources" + ), + ] +) diff --git a/Examples/FriendlyFlix/app/FriendlyFlixSDK/README.md b/Examples/FriendlyFlix/app/FriendlyFlixSDK/README.md new file mode 100644 index 0000000..fdce190 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlixSDK/README.md @@ -0,0 +1,459 @@ +This Swift package contains the generated Swift code for the connector `friendly-flix`. + +You can use this package by adding it as a local Swift package dependency in your project. + +# Accessing the connector + +Add the necessary imports + +``` +import FirebaseDataConnect +import FriendlyFlixSDK + +``` + +The connector can be accessed using the following code: + +``` +let connector = DataConnect.friendlyFlixConnector + +``` + + +## Connecting to the local Emulator +By default, the connector will connect to the production service. + +To connect to the emulator, you can use the following code, which can be called from the `init` function of your SwiftUI app + +``` +connector.useEmulator() +``` + +# Queries + +## ListMoviesQuery +### Variables + + +#### Optional +```swift + +let orderByRating: OrderDirection = ... +let orderByReleaseYear: OrderDirection = ... +let limit: Int = ... +``` + + + +### Using the Query Reference +``` +struct MyView: View { + var listMoviesQueryRef = DataConnect.friendlyFlixConnector.listMoviesQuery.ref(...) + + var body: some View { + VStack { + if let data = listMoviesQueryRef.data { + // use data in View + } + else { + Text("Loading...") + } + } + .task { + do { + let _ = try await listMoviesQueryRef.execute() + } catch { + } + } + } +} +``` + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.listMoviesQuery.execute(...) +``` + + +## GetMovieByIdQuery +### Variables +#### Required +```swift + +let id: UUID = ... +``` + + + + +### Using the Query Reference +``` +struct MyView: View { + var getMovieByIdQueryRef = DataConnect.friendlyFlixConnector.getMovieByIdQuery.ref(...) + + var body: some View { + VStack { + if let data = getMovieByIdQueryRef.data { + // use data in View + } + else { + Text("Loading...") + } + } + .task { + do { + let _ = try await getMovieByIdQueryRef.execute() + } catch { + } + } + } +} +``` + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.getMovieByIdQuery.execute(...) +``` + + +## GetActorByIdQuery +### Variables +#### Required +```swift + +let id: UUID = ... +``` + + + + +### Using the Query Reference +``` +struct MyView: View { + var getActorByIdQueryRef = DataConnect.friendlyFlixConnector.getActorByIdQuery.ref(...) + + var body: some View { + VStack { + if let data = getActorByIdQueryRef.data { + // use data in View + } + else { + Text("Loading...") + } + } + .task { + do { + let _ = try await getActorByIdQueryRef.execute() + } catch { + } + } + } +} +``` + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.getActorByIdQuery.execute(...) +``` + + +## GetCurrentUserQuery + + +### Using the Query Reference +``` +struct MyView: View { + var getCurrentUserQueryRef = DataConnect.friendlyFlixConnector.getCurrentUserQuery.ref(...) + + var body: some View { + VStack { + if let data = getCurrentUserQueryRef.data { + // use data in View + } + else { + Text("Loading...") + } + } + .task { + do { + let _ = try await getCurrentUserQueryRef.execute() + } catch { + } + } + } +} +``` + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.getCurrentUserQuery.execute(...) +``` + + +## GetIfFavoritedMovieQuery +### Variables +#### Required +```swift + +let movieId: UUID = ... +``` + + + + +### Using the Query Reference +``` +struct MyView: View { + var getIfFavoritedMovieQueryRef = DataConnect.friendlyFlixConnector.getIfFavoritedMovieQuery.ref(...) + + var body: some View { + VStack { + if let data = getIfFavoritedMovieQueryRef.data { + // use data in View + } + else { + Text("Loading...") + } + } + .task { + do { + let _ = try await getIfFavoritedMovieQueryRef.execute() + } catch { + } + } + } +} +``` + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.getIfFavoritedMovieQuery.execute(...) +``` + + +## SearchAllQuery +### Variables +#### Required +```swift + +let minYear: Int = ... +let maxYear: Int = ... +let minRating: Double = ... +let maxRating: Double = ... +let genre: String = ... +``` + + +#### Optional +```swift + +let input: String = ... +``` + + + +### Using the Query Reference +``` +struct MyView: View { + var searchAllQueryRef = DataConnect.friendlyFlixConnector.searchAllQuery.ref(...) + + var body: some View { + VStack { + if let data = searchAllQueryRef.data { + // use data in View + } + else { + Text("Loading...") + } + } + .task { + do { + let _ = try await searchAllQueryRef.execute() + } catch { + } + } + } +} +``` + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.searchAllQuery.execute(...) +``` + + +## ListMoviesByPartialTitleQuery +### Variables +#### Required +```swift + +let searchTerm: String = ... +``` + + + + +### Using the Query Reference +``` +struct MyView: View { + var listMoviesByPartialTitleQueryRef = DataConnect.friendlyFlixConnector.listMoviesByPartialTitleQuery.ref(...) + + var body: some View { + VStack { + if let data = listMoviesByPartialTitleQueryRef.data { + // use data in View + } + else { + Text("Loading...") + } + } + .task { + do { + let _ = try await listMoviesByPartialTitleQueryRef.execute() + } catch { + } + } + } +} +``` + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.listMoviesByPartialTitleQuery.execute(...) +``` + + +## GetUserFavoriteMoviesQuery + + +### Using the Query Reference +``` +struct MyView: View { + var getUserFavoriteMoviesQueryRef = DataConnect.friendlyFlixConnector.getUserFavoriteMoviesQuery.ref(...) + + var body: some View { + VStack { + if let data = getUserFavoriteMoviesQueryRef.data { + // use data in View + } + else { + Text("Loading...") + } + } + .task { + do { + let _ = try await getUserFavoriteMoviesQueryRef.execute() + } catch { + } + } + } +} +``` + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.getUserFavoriteMoviesQuery.execute(...) +``` + + +# Mutations +## UpsertUserMutation + +### Variables + +#### Required +```swift + +let username: String = ... +``` + + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.upsertUserMutation.execute(...) +``` + +## AddFavoritedMovieMutation + +### Variables + +#### Required +```swift + +let movieId: UUID = ... +``` + + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.addFavoritedMovieMutation.execute(...) +``` + +## DeleteFavoritedMovieMutation + +### Variables + +#### Required +```swift + +let movieId: UUID = ... +``` + + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.deleteFavoritedMovieMutation.execute(...) +``` + +## AddReviewMutation + +### Variables + +#### Required +```swift + +let movieId: UUID = ... +let rating: Int = ... +let reviewText: String = ... +``` + + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.addReviewMutation.execute(...) +``` + +## UpdateReviewMutation + +### Variables + +#### Required +```swift + +let movieId: UUID = ... +let rating: Int = ... +let reviewText: String = ... +``` + + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.updateReviewMutation.execute(...) +``` + +## DeleteReviewMutation + +### Variables + +#### Required +```swift + +let movieId: UUID = ... +``` + + +### One-shot execute +``` +DataConnect.friendlyFlixConnector.deleteReviewMutation.execute(...) +``` + diff --git a/Examples/FriendlyFlix/app/FriendlyFlixSDK/Sources/FriendlyFlixClient.swift b/Examples/FriendlyFlix/app/FriendlyFlixSDK/Sources/FriendlyFlixClient.swift new file mode 100644 index 0000000..7c7a7e4 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlixSDK/Sources/FriendlyFlixClient.swift @@ -0,0 +1,80 @@ +// 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. + +import Foundation + +import FirebaseCore +import FirebaseDataConnect + +public extension DataConnect { + static let friendlyFlixConnector: FriendlyFlixConnector = { + let dc = DataConnect.dataConnect( + connectorConfig: FriendlyFlixConnector.connectorConfig, + callerSDKType: .generated + ) + return FriendlyFlixConnector(dataConnect: dc) + }() +} + +public class FriendlyFlixConnector { + let dataConnect: DataConnect + + public static let connectorConfig = ConnectorConfig( + serviceId: "dataconnect", + location: "us-central1", + connector: "friendly-flix" + ) + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + + // init operations + upsertUserMutation = UpsertUserMutation(dataConnect: dataConnect) + addFavoritedMovieMutation = AddFavoritedMovieMutation(dataConnect: dataConnect) + deleteFavoritedMovieMutation = DeleteFavoritedMovieMutation(dataConnect: dataConnect) + addReviewMutation = AddReviewMutation(dataConnect: dataConnect) + updateReviewMutation = UpdateReviewMutation(dataConnect: dataConnect) + deleteReviewMutation = DeleteReviewMutation(dataConnect: dataConnect) + listMoviesQuery = ListMoviesQuery(dataConnect: dataConnect) + getMovieByIdQuery = GetMovieByIdQuery(dataConnect: dataConnect) + getActorByIdQuery = GetActorByIdQuery(dataConnect: dataConnect) + getCurrentUserQuery = GetCurrentUserQuery(dataConnect: dataConnect) + getIfFavoritedMovieQuery = GetIfFavoritedMovieQuery(dataConnect: dataConnect) + searchAllQuery = SearchAllQuery(dataConnect: dataConnect) + listMoviesByPartialTitleQuery = ListMoviesByPartialTitleQuery(dataConnect: dataConnect) + getUserFavoriteMoviesQuery = GetUserFavoriteMoviesQuery(dataConnect: dataConnect) + } + + public func useEmulator(host: String = DataConnect.EmulatorDefaults.host, + port: Int = DataConnect.EmulatorDefaults.port) { + dataConnect.useEmulator(host: host, port: port) + } + + // MARK: Operations + + public let upsertUserMutation: UpsertUserMutation + public let addFavoritedMovieMutation: AddFavoritedMovieMutation + public let deleteFavoritedMovieMutation: DeleteFavoritedMovieMutation + public let addReviewMutation: AddReviewMutation + public let updateReviewMutation: UpdateReviewMutation + public let deleteReviewMutation: DeleteReviewMutation + public let listMoviesQuery: ListMoviesQuery + public let getMovieByIdQuery: GetMovieByIdQuery + public let getActorByIdQuery: GetActorByIdQuery + public let getCurrentUserQuery: GetCurrentUserQuery + public let getIfFavoritedMovieQuery: GetIfFavoritedMovieQuery + public let searchAllQuery: SearchAllQuery + public let listMoviesByPartialTitleQuery: ListMoviesByPartialTitleQuery + public let getUserFavoriteMoviesQuery: GetUserFavoriteMoviesQuery +} diff --git a/Examples/FriendlyFlix/app/FriendlyFlixSDK/Sources/FriendlyFlixKeys.swift b/Examples/FriendlyFlix/app/FriendlyFlixSDK/Sources/FriendlyFlixKeys.swift new file mode 100644 index 0000000..c364d01 --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlixSDK/Sources/FriendlyFlixKeys.swift @@ -0,0 +1,353 @@ +// 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. + +import Foundation + +import FirebaseDataConnect + +public struct ActorKey { + public private(set) var id: UUID + + enum CodingKeys: String, CodingKey { + case id + } +} + +extension ActorKey: Codable { + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(id, forKey: .id, container: &container) + } +} + +extension ActorKey: Equatable { + public static func == (lhs: ActorKey, rhs: ActorKey) -> Bool { + if lhs.id != rhs.id { + return false + } + + return true + } +} + +extension ActorKey: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension ActorKey: Sendable {} + +public struct FavoriteMovieKey { + public private(set) var userId: String + + public private(set) var movieId: UUID + + enum CodingKeys: String, CodingKey { + case userId + + case movieId + } +} + +extension FavoriteMovieKey: Codable { + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + userId = try codecHelper.decode(String.self, forKey: .userId, container: &container) + + movieId = try codecHelper.decode(UUID.self, forKey: .movieId, container: &container) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(userId, forKey: .userId, container: &container) + + try codecHelper.encode(movieId, forKey: .movieId, container: &container) + } +} + +extension FavoriteMovieKey: Equatable { + public static func == (lhs: FavoriteMovieKey, rhs: FavoriteMovieKey) -> Bool { + if lhs.userId != rhs.userId { + return false + } + + if lhs.movieId != rhs.movieId { + return false + } + + return true + } +} + +extension FavoriteMovieKey: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(userId) + + hasher.combine(movieId) + } +} + +extension FavoriteMovieKey: Sendable {} + +public struct MovieActorKey { + public private(set) var movieId: UUID + + public private(set) var actorId: UUID + + enum CodingKeys: String, CodingKey { + case movieId + + case actorId + } +} + +extension MovieActorKey: Codable { + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + movieId = try codecHelper.decode(UUID.self, forKey: .movieId, container: &container) + + actorId = try codecHelper.decode(UUID.self, forKey: .actorId, container: &container) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(movieId, forKey: .movieId, container: &container) + + try codecHelper.encode(actorId, forKey: .actorId, container: &container) + } +} + +extension MovieActorKey: Equatable { + public static func == (lhs: MovieActorKey, rhs: MovieActorKey) -> Bool { + if lhs.movieId != rhs.movieId { + return false + } + + if lhs.actorId != rhs.actorId { + return false + } + + return true + } +} + +extension MovieActorKey: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(movieId) + + hasher.combine(actorId) + } +} + +extension MovieActorKey: Sendable {} + +public struct MovieMetadataKey { + public private(set) var id: UUID + + enum CodingKeys: String, CodingKey { + case id + } +} + +extension MovieMetadataKey: Codable { + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(id, forKey: .id, container: &container) + } +} + +extension MovieMetadataKey: Equatable { + public static func == (lhs: MovieMetadataKey, rhs: MovieMetadataKey) -> Bool { + if lhs.id != rhs.id { + return false + } + + return true + } +} + +extension MovieMetadataKey: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension MovieMetadataKey: Sendable {} + +public struct MovieKey { + public private(set) var id: UUID + + enum CodingKeys: String, CodingKey { + case id + } +} + +extension MovieKey: Codable { + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(id, forKey: .id, container: &container) + } +} + +extension MovieKey: Equatable { + public static func == (lhs: MovieKey, rhs: MovieKey) -> Bool { + if lhs.id != rhs.id { + return false + } + + return true + } +} + +extension MovieKey: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension MovieKey: Sendable {} + +public struct ReviewKey { + public private(set) var userId: String + + public private(set) var movieId: UUID + + enum CodingKeys: String, CodingKey { + case userId + + case movieId + } +} + +extension ReviewKey: Codable { + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + userId = try codecHelper.decode(String.self, forKey: .userId, container: &container) + + movieId = try codecHelper.decode(UUID.self, forKey: .movieId, container: &container) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(userId, forKey: .userId, container: &container) + + try codecHelper.encode(movieId, forKey: .movieId, container: &container) + } +} + +extension ReviewKey: Equatable { + public static func == (lhs: ReviewKey, rhs: ReviewKey) -> Bool { + if lhs.userId != rhs.userId { + return false + } + + if lhs.movieId != rhs.movieId { + return false + } + + return true + } +} + +extension ReviewKey: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(userId) + + hasher.combine(movieId) + } +} + +extension ReviewKey: Sendable {} + +public struct UserKey { + public private(set) var id: String + + enum CodingKeys: String, CodingKey { + case id + } +} + +extension UserKey: Codable { + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(String.self, forKey: .id, container: &container) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(id, forKey: .id, container: &container) + } +} + +extension UserKey: Equatable { + public static func == (lhs: UserKey, rhs: UserKey) -> Bool { + if lhs.id != rhs.id { + return false + } + + return true + } +} + +extension UserKey: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension UserKey: Sendable {} diff --git a/Examples/FriendlyFlix/app/FriendlyFlixSDK/Sources/FriendlyFlixOperations.swift b/Examples/FriendlyFlix/app/FriendlyFlixSDK/Sources/FriendlyFlixOperations.swift new file mode 100644 index 0000000..0cacb1f --- /dev/null +++ b/Examples/FriendlyFlix/app/FriendlyFlixSDK/Sources/FriendlyFlixOperations.swift @@ -0,0 +1,2677 @@ +// 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. + +import Foundation + +import FirebaseCore +import FirebaseDataConnect + +// MARK: Common Enums + +public enum OrderDirection: String, Codable, Sendable { + case ASC + case DESC +} + +// End enum definitions + +public class UpsertUserMutation { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "UpsertUser" + + public typealias Ref = MutationRef + + public struct Variables: OperationVariable { + public var + username: String + + public init(username: String) { + self.username = username + } + + public static func == (lhs: Variables, rhs: Variables) -> Bool { + return lhs.username == rhs.username + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(username) + } + + enum CodingKeys: String, CodingKey { + case username + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(username, forKey: .username, container: &container) + } + } + + public struct Data: Decodable, Sendable { + public var + user_upsert: UserKey + } + + public func ref(username: String) -> MutationRef< + UpsertUserMutation.Data, + UpsertUserMutation.Variables + > { + var variables = UpsertUserMutation.Variables(username: username) + + let ref = dataConnect.mutation( + name: "UpsertUser", + variables: variables, + resultsDataType: UpsertUserMutation.Data.self + ) + return ref as MutationRef + } + + @MainActor + public func execute(username: String) async throws -> OperationResult { + var variables = UpsertUserMutation.Variables(username: username) + + let ref = dataConnect.mutation( + name: "UpsertUser", + variables: variables, + resultsDataType: UpsertUserMutation.Data.self + ) + + return try await ref.execute() + } +} + +public class AddFavoritedMovieMutation { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "AddFavoritedMovie" + + public typealias Ref = MutationRef< + AddFavoritedMovieMutation.Data, + AddFavoritedMovieMutation.Variables + > + + public struct Variables: OperationVariable { + public var + movieId: UUID + + public init(movieId: UUID) { + self.movieId = movieId + } + + public static func == (lhs: Variables, rhs: Variables) -> Bool { + return lhs.movieId == rhs.movieId + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(movieId) + } + + enum CodingKeys: String, CodingKey { + case movieId + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(movieId, forKey: .movieId, container: &container) + } + } + + public struct Data: Decodable, Sendable { + public var + favorite_movie_upsert: FavoriteMovieKey + } + + public func ref(movieId: UUID) -> MutationRef< + AddFavoritedMovieMutation.Data, + AddFavoritedMovieMutation.Variables + > { + var variables = AddFavoritedMovieMutation.Variables(movieId: movieId) + + let ref = dataConnect.mutation( + name: "AddFavoritedMovie", + variables: variables, + resultsDataType: AddFavoritedMovieMutation.Data.self + ) + return ref as MutationRef< + AddFavoritedMovieMutation.Data, + AddFavoritedMovieMutation.Variables + > + } + + @MainActor + public func execute(movieId: UUID) async throws + -> OperationResult { + var variables = AddFavoritedMovieMutation.Variables(movieId: movieId) + + let ref = dataConnect.mutation( + name: "AddFavoritedMovie", + variables: variables, + resultsDataType: AddFavoritedMovieMutation.Data.self + ) + + return try await ref.execute() + } +} + +public class DeleteFavoritedMovieMutation { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "DeleteFavoritedMovie" + + public typealias Ref = MutationRef< + DeleteFavoritedMovieMutation.Data, + DeleteFavoritedMovieMutation.Variables + > + + public struct Variables: OperationVariable { + public var + movieId: UUID + + public init(movieId: UUID) { + self.movieId = movieId + } + + public static func == (lhs: Variables, rhs: Variables) -> Bool { + return lhs.movieId == rhs.movieId + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(movieId) + } + + enum CodingKeys: String, CodingKey { + case movieId + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(movieId, forKey: .movieId, container: &container) + } + } + + public struct Data: Decodable, Sendable { + public var + favorite_movie_delete: FavoriteMovieKey? + } + + public func ref(movieId: UUID) + -> MutationRef { + var variables = DeleteFavoritedMovieMutation.Variables(movieId: movieId) + + let ref = dataConnect.mutation( + name: "DeleteFavoritedMovie", + variables: variables, + resultsDataType: DeleteFavoritedMovieMutation.Data.self + ) + return ref as MutationRef< + DeleteFavoritedMovieMutation.Data, + DeleteFavoritedMovieMutation.Variables + > + } + + @MainActor + public func execute(movieId: UUID) async throws + -> OperationResult { + var variables = DeleteFavoritedMovieMutation.Variables(movieId: movieId) + + let ref = dataConnect.mutation( + name: "DeleteFavoritedMovie", + variables: variables, + resultsDataType: DeleteFavoritedMovieMutation.Data.self + ) + + return try await ref.execute() + } +} + +public class AddReviewMutation { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "AddReview" + + public typealias Ref = MutationRef + + public struct Variables: OperationVariable { + public var + movieId: UUID + + public var + rating: Int + + public var + reviewText: String + + public init(movieId: UUID, + + rating: Int, + + reviewText: String) { + self.movieId = movieId + self.rating = rating + self.reviewText = reviewText + } + + public static func == (lhs: Variables, rhs: Variables) -> Bool { + return lhs.movieId == rhs.movieId && + lhs.rating == rhs.rating && + lhs.reviewText == rhs.reviewText + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(movieId) + + hasher.combine(rating) + + hasher.combine(reviewText) + } + + enum CodingKeys: String, CodingKey { + case movieId + + case rating + + case reviewText + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(movieId, forKey: .movieId, container: &container) + + try codecHelper.encode(rating, forKey: .rating, container: &container) + + try codecHelper.encode(reviewText, forKey: .reviewText, container: &container) + } + } + + public struct Data: Decodable, Sendable { + public var + review_insert: ReviewKey + } + + public func ref(movieId: UUID, + + rating: Int, + + reviewText: String) + -> MutationRef { + var variables = AddReviewMutation.Variables( + movieId: movieId, + rating: rating, + reviewText: reviewText + ) + + let ref = dataConnect.mutation( + name: "AddReview", + variables: variables, + resultsDataType: AddReviewMutation.Data.self + ) + return ref as MutationRef + } + + @MainActor + public func execute(movieId: UUID, + + rating: Int, + + reviewText: String) async throws -> OperationResult { + var variables = AddReviewMutation.Variables( + movieId: movieId, + rating: rating, + reviewText: reviewText + ) + + let ref = dataConnect.mutation( + name: "AddReview", + variables: variables, + resultsDataType: AddReviewMutation.Data.self + ) + + return try await ref.execute() + } +} + +public class UpdateReviewMutation { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "UpdateReview" + + public typealias Ref = MutationRef + + public struct Variables: OperationVariable { + public var + movieId: UUID + + public var + rating: Int + + public var + reviewText: String + + public init(movieId: UUID, + + rating: Int, + + reviewText: String) { + self.movieId = movieId + self.rating = rating + self.reviewText = reviewText + } + + public static func == (lhs: Variables, rhs: Variables) -> Bool { + return lhs.movieId == rhs.movieId && + lhs.rating == rhs.rating && + lhs.reviewText == rhs.reviewText + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(movieId) + + hasher.combine(rating) + + hasher.combine(reviewText) + } + + enum CodingKeys: String, CodingKey { + case movieId + + case rating + + case reviewText + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(movieId, forKey: .movieId, container: &container) + + try codecHelper.encode(rating, forKey: .rating, container: &container) + + try codecHelper.encode(reviewText, forKey: .reviewText, container: &container) + } + } + + public struct Data: Decodable, Sendable { + public var + review_update: ReviewKey? + } + + public func ref(movieId: UUID, + + rating: Int, + + reviewText: String) + -> MutationRef { + var variables = UpdateReviewMutation.Variables( + movieId: movieId, + rating: rating, + reviewText: reviewText + ) + + let ref = dataConnect.mutation( + name: "UpdateReview", + variables: variables, + resultsDataType: UpdateReviewMutation.Data.self + ) + return ref as MutationRef + } + + @MainActor + public func execute(movieId: UUID, + + rating: Int, + + reviewText: String) async throws + -> OperationResult { + var variables = UpdateReviewMutation.Variables( + movieId: movieId, + rating: rating, + reviewText: reviewText + ) + + let ref = dataConnect.mutation( + name: "UpdateReview", + variables: variables, + resultsDataType: UpdateReviewMutation.Data.self + ) + + return try await ref.execute() + } +} + +public class DeleteReviewMutation { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "DeleteReview" + + public typealias Ref = MutationRef + + public struct Variables: OperationVariable { + public var + movieId: UUID + + public init(movieId: UUID) { + self.movieId = movieId + } + + public static func == (lhs: Variables, rhs: Variables) -> Bool { + return lhs.movieId == rhs.movieId + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(movieId) + } + + enum CodingKeys: String, CodingKey { + case movieId + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(movieId, forKey: .movieId, container: &container) + } + } + + public struct Data: Decodable, Sendable { + public var + review_delete: ReviewKey? + } + + public func ref(movieId: UUID) -> MutationRef< + DeleteReviewMutation.Data, + DeleteReviewMutation.Variables + > { + var variables = DeleteReviewMutation.Variables(movieId: movieId) + + let ref = dataConnect.mutation( + name: "DeleteReview", + variables: variables, + resultsDataType: DeleteReviewMutation.Data.self + ) + return ref as MutationRef + } + + @MainActor + public func execute(movieId: UUID) async throws -> OperationResult { + var variables = DeleteReviewMutation.Variables(movieId: movieId) + + let ref = dataConnect.mutation( + name: "DeleteReview", + variables: variables, + resultsDataType: DeleteReviewMutation.Data.self + ) + + return try await ref.execute() + } +} + +public class ListMoviesQuery { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "ListMovies" + + public typealias Ref = QueryRefObservation + + public struct Variables: OperationVariable { + @OptionalVariable + public var + orderByRating: OrderDirection? + + @OptionalVariable + public var + orderByReleaseYear: OrderDirection? + + @OptionalVariable + public var + limit: Int? + + public init(_ optionalVars: ((inout Variables) -> Void)? = nil) { + if let optionalVars { + optionalVars(&self) + } + } + + public static func == (lhs: Variables, rhs: Variables) -> Bool { + return lhs.orderByRating == rhs.orderByRating && + lhs.orderByReleaseYear == rhs.orderByReleaseYear && + lhs.limit == rhs.limit + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(orderByRating) + + hasher.combine(orderByReleaseYear) + + hasher.combine(limit) + } + + enum CodingKeys: String, CodingKey { + case orderByRating + + case orderByReleaseYear + + case limit + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + if $orderByRating.isSet { + try codecHelper.encode(orderByRating, forKey: .orderByRating, container: &container) + } + + if $orderByReleaseYear.isSet { + try codecHelper.encode( + orderByReleaseYear, + forKey: .orderByReleaseYear, + container: &container + ) + } + + if $limit.isSet { + try codecHelper.encode(limit, forKey: .limit, container: &container) + } + } + } + + public struct Data: Decodable, Sendable { + public struct Movie: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + title: String + + public var + imageUrl: String + + public var + releaseYear: Int? + + public var + genre: String? + + public var + rating: Double? + + public var + tags: [String]? + + public var + description: String? + + public var movieKey: MovieKey { + return MovieKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Movie, rhs: Movie) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case title + + case imageUrl + + case releaseYear + + case genre + + case rating + + case tags + + case description + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + title = try codecHelper.decode(String.self, forKey: .title, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + + releaseYear = try codecHelper.decode(Int?.self, forKey: .releaseYear, container: &container) + + genre = try codecHelper.decode(String?.self, forKey: .genre, container: &container) + + rating = try codecHelper.decode(Double?.self, forKey: .rating, container: &container) + + tags = try codecHelper.decode([String].self, forKey: .tags, container: &container) + + description = try codecHelper.decode( + String?.self, + forKey: .description, + container: &container + ) + } + } + + public var + movies: [Movie] + } + + public func ref(_ optionalVars: ((inout ListMoviesQuery.Variables) -> Void)? = nil) + -> QueryRefObservation< + ListMoviesQuery.Data, + ListMoviesQuery.Variables + > { + var variables = ListMoviesQuery.Variables() + + if let optionalVars { + optionalVars(&variables) + } + + let ref = dataConnect.query( + name: "ListMovies", + variables: variables, + resultsDataType: ListMoviesQuery.Data.self, + publisher: .observableMacro + ) + return ref as! QueryRefObservation + } + + @MainActor + public func execute(_ optionalVars: (@MainActor (inout ListMoviesQuery.Variables) -> Void)? = + nil) async throws -> OperationResult { + var variables = ListMoviesQuery.Variables() + + if let optionalVars { + optionalVars(&variables) + } + + let ref = dataConnect.query( + name: "ListMovies", + variables: variables, + resultsDataType: ListMoviesQuery.Data.self, + publisher: .observableMacro + ) + + let refCast = ref as! QueryRefObservation + return try await refCast.execute() + } +} + +public class GetMovieByIdQuery { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "GetMovieById" + + public typealias Ref = QueryRefObservation + + public struct Variables: OperationVariable { + public var + id: UUID + + public init(id: UUID) { + self.id = id + } + + public static func == (lhs: Variables, rhs: Variables) -> Bool { + return lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + enum CodingKeys: String, CodingKey { + case id + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(id, forKey: .id, container: &container) + } + } + + public struct Data: Decodable, Sendable { + public struct Movie: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + title: String + + public var + imageUrl: String + + public var + releaseYear: Int? + + public var + genre: String? + + public var + rating: Double? + + public var + description: String? + + public var + tags: [String]? + + public struct MovieMetadataMetadata: Decodable, Sendable { + public var + director: String? + + enum CodingKeys: String, CodingKey { + case director + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + director = try codecHelper.decode(String?.self, forKey: .director, container: &container) + } + } + + public var + metadata: [MovieMetadataMetadata] + + public struct ActorMainActors: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + name: String + + public var + imageUrl: String + + public var actorMainActorsKey: ActorKey { + return ActorKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: ActorMainActors, rhs: ActorMainActors) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case name + + case imageUrl + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + name = try codecHelper.decode(String.self, forKey: .name, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + } + } + + public var + mainActors: [ActorMainActors] + + public struct ActorSupportingActors: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + name: String + + public var + imageUrl: String + + public var actorSupportingActorsKey: ActorKey { + return ActorKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: ActorSupportingActors, rhs: ActorSupportingActors) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case name + + case imageUrl + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + name = try codecHelper.decode(String.self, forKey: .name, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + } + } + + public var + supportingActors: [ActorSupportingActors] + + public struct ReviewReviews: Decodable, Sendable { + public var + id: UUID + + public var + reviewText: String? + + public var + reviewDate: LocalDate + + public var + rating: Int? + + public struct User: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: String + + public var + username: String + + public var userKey: UserKey { + return UserKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: User, rhs: User) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case username + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(String.self, forKey: .id, container: &container) + + username = try codecHelper.decode(String.self, forKey: .username, container: &container) + } + } + + public var + user: User + + enum CodingKeys: String, CodingKey { + case id + + case reviewText + + case reviewDate + + case rating + + case user + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + reviewText = try codecHelper.decode( + String?.self, + forKey: .reviewText, + container: &container + ) + + reviewDate = try codecHelper.decode( + LocalDate.self, + forKey: .reviewDate, + container: &container + ) + + rating = try codecHelper.decode(Int?.self, forKey: .rating, container: &container) + + user = try codecHelper.decode(User.self, forKey: .user, container: &container) + } + } + + public var + reviews: [ReviewReviews] + + public var movieKey: MovieKey { + return MovieKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Movie, rhs: Movie) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case title + + case imageUrl + + case releaseYear + + case genre + + case rating + + case description + + case tags + + case metadata + + case mainActors + + case supportingActors + + case reviews + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + title = try codecHelper.decode(String.self, forKey: .title, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + + releaseYear = try codecHelper.decode(Int?.self, forKey: .releaseYear, container: &container) + + genre = try codecHelper.decode(String?.self, forKey: .genre, container: &container) + + rating = try codecHelper.decode(Double?.self, forKey: .rating, container: &container) + + description = try codecHelper.decode( + String?.self, + forKey: .description, + container: &container + ) + + tags = try codecHelper.decode([String].self, forKey: .tags, container: &container) + + metadata = try codecHelper.decode( + [MovieMetadataMetadata].self, + forKey: .metadata, + container: &container + ) + + mainActors = try codecHelper.decode( + [ActorMainActors].self, + forKey: .mainActors, + container: &container + ) + + supportingActors = try codecHelper.decode( + [ActorSupportingActors].self, + forKey: .supportingActors, + container: &container + ) + + reviews = try codecHelper.decode( + [ReviewReviews].self, + forKey: .reviews, + container: &container + ) + } + } + + public var + movie: Movie? + } + + public func ref(id: UUID) -> QueryRefObservation< + GetMovieByIdQuery.Data, + GetMovieByIdQuery.Variables + > { + var variables = GetMovieByIdQuery.Variables(id: id) + + let ref = dataConnect.query( + name: "GetMovieById", + variables: variables, + resultsDataType: GetMovieByIdQuery.Data.self, + publisher: .observableMacro + ) + return ref as! QueryRefObservation + } + + @MainActor + public func execute(id: UUID) async throws -> OperationResult { + var variables = GetMovieByIdQuery.Variables(id: id) + + let ref = dataConnect.query( + name: "GetMovieById", + variables: variables, + resultsDataType: GetMovieByIdQuery.Data.self, + publisher: .observableMacro + ) + + let refCast = ref as! QueryRefObservation< + GetMovieByIdQuery.Data, + GetMovieByIdQuery.Variables + > + return try await refCast.execute() + } +} + +public class GetActorByIdQuery { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "GetActorById" + + public typealias Ref = QueryRefObservation + + public struct Variables: OperationVariable { + public var + id: UUID + + public init(id: UUID) { + self.id = id + } + + public static func == (lhs: Variables, rhs: Variables) -> Bool { + return lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + enum CodingKeys: String, CodingKey { + case id + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(id, forKey: .id, container: &container) + } + } + + public struct Data: Decodable, Sendable { + public struct Actor: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + name: String + + public var + imageUrl: String + + public struct MovieMainActors: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + title: String + + public var + genre: String? + + public var + tags: [String]? + + public var + imageUrl: String + + public var movieMainActorsKey: MovieKey { + return MovieKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: MovieMainActors, rhs: MovieMainActors) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case title + + case genre + + case tags + + case imageUrl + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + title = try codecHelper.decode(String.self, forKey: .title, container: &container) + + genre = try codecHelper.decode(String?.self, forKey: .genre, container: &container) + + tags = try codecHelper.decode([String].self, forKey: .tags, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + } + } + + public var + mainActors: [MovieMainActors] + + public struct MovieSupportingActors: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + title: String + + public var + genre: String? + + public var + tags: [String]? + + public var + imageUrl: String + + public var movieSupportingActorsKey: MovieKey { + return MovieKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: MovieSupportingActors, rhs: MovieSupportingActors) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case title + + case genre + + case tags + + case imageUrl + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + title = try codecHelper.decode(String.self, forKey: .title, container: &container) + + genre = try codecHelper.decode(String?.self, forKey: .genre, container: &container) + + tags = try codecHelper.decode([String].self, forKey: .tags, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + } + } + + public var + supportingActors: [MovieSupportingActors] + + public var actorKey: ActorKey { + return ActorKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Actor, rhs: Actor) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case name + + case imageUrl + + case mainActors + + case supportingActors + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + name = try codecHelper.decode(String.self, forKey: .name, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + + mainActors = try codecHelper.decode( + [MovieMainActors].self, + forKey: .mainActors, + container: &container + ) + + supportingActors = try codecHelper.decode( + [MovieSupportingActors].self, + forKey: .supportingActors, + container: &container + ) + } + } + + public var + actor: Actor? + } + + public func ref(id: UUID) -> QueryRefObservation< + GetActorByIdQuery.Data, + GetActorByIdQuery.Variables + > { + var variables = GetActorByIdQuery.Variables(id: id) + + let ref = dataConnect.query( + name: "GetActorById", + variables: variables, + resultsDataType: GetActorByIdQuery.Data.self, + publisher: .observableMacro + ) + return ref as! QueryRefObservation + } + + @MainActor + public func execute(id: UUID) async throws -> OperationResult { + var variables = GetActorByIdQuery.Variables(id: id) + + let ref = dataConnect.query( + name: "GetActorById", + variables: variables, + resultsDataType: GetActorByIdQuery.Data.self, + publisher: .observableMacro + ) + + let refCast = ref as! QueryRefObservation< + GetActorByIdQuery.Data, + GetActorByIdQuery.Variables + > + return try await refCast.execute() + } +} + +public class GetCurrentUserQuery { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "GetCurrentUser" + + public typealias Ref = QueryRefObservation< + GetCurrentUserQuery.Data, + GetCurrentUserQuery.Variables + > + + public struct Variables: OperationVariable {} + + public struct Data: Decodable, Sendable { + public struct User: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: String + + public var + username: String + + public struct ReviewReviews: Decodable, Sendable { + public var + id: UUID + + public var + rating: Int? + + public var + reviewDate: LocalDate + + public var + reviewText: String? + + public struct Movie: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + title: String + + public var movieKey: MovieKey { + return MovieKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Movie, rhs: Movie) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case title + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + title = try codecHelper.decode(String.self, forKey: .title, container: &container) + } + } + + public var + movie: Movie + + enum CodingKeys: String, CodingKey { + case id + + case rating + + case reviewDate + + case reviewText + + case movie + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + rating = try codecHelper.decode(Int?.self, forKey: .rating, container: &container) + + reviewDate = try codecHelper.decode( + LocalDate.self, + forKey: .reviewDate, + container: &container + ) + + reviewText = try codecHelper.decode( + String?.self, + forKey: .reviewText, + container: &container + ) + + movie = try codecHelper.decode(Movie.self, forKey: .movie, container: &container) + } + } + + public var + reviews: [ReviewReviews] + + public struct FavoriteMovieFavoriteMovies: Decodable, Sendable { + public struct Movie: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + title: String + + public var + genre: String? + + public var + imageUrl: String + + public var + releaseYear: Int? + + public var + rating: Double? + + public var + description: String? + + public var + tags: [String]? + + public struct MovieMetadataMetadata: Decodable, Sendable { + public var + director: String? + + enum CodingKeys: String, CodingKey { + case director + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + director = try codecHelper.decode( + String?.self, + forKey: .director, + container: &container + ) + } + } + + public var + metadata: [MovieMetadataMetadata] + + public var movieKey: MovieKey { + return MovieKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Movie, rhs: Movie) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case title + + case genre + + case imageUrl + + case releaseYear + + case rating + + case description + + case tags + + case metadata + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + title = try codecHelper.decode(String.self, forKey: .title, container: &container) + + genre = try codecHelper.decode(String?.self, forKey: .genre, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + + releaseYear = try codecHelper.decode( + Int?.self, + forKey: .releaseYear, + container: &container + ) + + rating = try codecHelper.decode(Double?.self, forKey: .rating, container: &container) + + description = try codecHelper.decode( + String?.self, + forKey: .description, + container: &container + ) + + tags = try codecHelper.decode([String].self, forKey: .tags, container: &container) + + metadata = try codecHelper.decode( + [MovieMetadataMetadata].self, + forKey: .metadata, + container: &container + ) + } + } + + public var + movie: Movie + + enum CodingKeys: String, CodingKey { + case movie + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + movie = try codecHelper.decode(Movie.self, forKey: .movie, container: &container) + } + } + + public var + favoriteMovies: [FavoriteMovieFavoriteMovies] + + public var userKey: UserKey { + return UserKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: User, rhs: User) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case username + + case reviews + + case favoriteMovies + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(String.self, forKey: .id, container: &container) + + username = try codecHelper.decode(String.self, forKey: .username, container: &container) + + reviews = try codecHelper.decode( + [ReviewReviews].self, + forKey: .reviews, + container: &container + ) + + favoriteMovies = try codecHelper.decode( + [FavoriteMovieFavoriteMovies].self, + forKey: .favoriteMovies, + container: &container + ) + } + } + + public var + user: User? + } + + public func ref( + ) -> QueryRefObservation { + var variables = GetCurrentUserQuery.Variables() + + let ref = dataConnect.query( + name: "GetCurrentUser", + variables: variables, + resultsDataType: GetCurrentUserQuery.Data.self, + publisher: .observableMacro + ) + return ref as! QueryRefObservation + } + + @MainActor + public func execute( + ) async throws -> OperationResult { + var variables = GetCurrentUserQuery.Variables() + + let ref = dataConnect.query( + name: "GetCurrentUser", + variables: variables, + resultsDataType: GetCurrentUserQuery.Data.self, + publisher: .observableMacro + ) + + let refCast = ref as! QueryRefObservation< + GetCurrentUserQuery.Data, + GetCurrentUserQuery.Variables + > + return try await refCast.execute() + } +} + +public class GetIfFavoritedMovieQuery { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "GetIfFavoritedMovie" + + public typealias Ref = QueryRefObservation< + GetIfFavoritedMovieQuery.Data, + GetIfFavoritedMovieQuery.Variables + > + + public struct Variables: OperationVariable { + public var + movieId: UUID + + public init(movieId: UUID) { + self.movieId = movieId + } + + public static func == (lhs: Variables, rhs: Variables) -> Bool { + return lhs.movieId == rhs.movieId + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(movieId) + } + + enum CodingKeys: String, CodingKey { + case movieId + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(movieId, forKey: .movieId, container: &container) + } + } + + public struct Data: Decodable, Sendable { + public struct FavoriteMovie: Decodable, Sendable { + public var + movieId: UUID + + enum CodingKeys: String, CodingKey { + case movieId + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + movieId = try codecHelper.decode(UUID.self, forKey: .movieId, container: &container) + } + } + + public var + favorite_movie: FavoriteMovie? + } + + public func ref(movieId: UUID) + -> QueryRefObservation { + var variables = GetIfFavoritedMovieQuery.Variables(movieId: movieId) + + let ref = dataConnect.query( + name: "GetIfFavoritedMovie", + variables: variables, + resultsDataType: GetIfFavoritedMovieQuery.Data.self, + publisher: .observableMacro + ) + return ref as! QueryRefObservation< + GetIfFavoritedMovieQuery.Data, + GetIfFavoritedMovieQuery.Variables + > + } + + @MainActor + public func execute(movieId: UUID) async throws + -> OperationResult { + var variables = GetIfFavoritedMovieQuery.Variables(movieId: movieId) + + let ref = dataConnect.query( + name: "GetIfFavoritedMovie", + variables: variables, + resultsDataType: GetIfFavoritedMovieQuery.Data.self, + publisher: .observableMacro + ) + + let refCast = ref as! QueryRefObservation< + GetIfFavoritedMovieQuery.Data, + GetIfFavoritedMovieQuery.Variables + > + return try await refCast.execute() + } +} + +public class SearchAllQuery { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "SearchAll" + + public typealias Ref = QueryRefObservation + + public struct Variables: OperationVariable { + @OptionalVariable + public var + input: String? + + public var + minYear: Int + + public var + maxYear: Int + + public var + minRating: Double + + public var + maxRating: Double + + public var + genre: String + + public init(minYear: Int, + + maxYear: Int, + + minRating: Double, + + maxRating: Double, + + genre: String, + + _ optionalVars: ((inout Variables) -> Void)? = nil) { + self.minYear = minYear + self.maxYear = maxYear + self.minRating = minRating + self.maxRating = maxRating + self.genre = genre + + if let optionalVars { + optionalVars(&self) + } + } + + public static func == (lhs: Variables, rhs: Variables) -> Bool { + return lhs.input == rhs.input && + lhs.minYear == rhs.minYear && + lhs.maxYear == rhs.maxYear && + lhs.minRating == rhs.minRating && + lhs.maxRating == rhs.maxRating && + lhs.genre == rhs.genre + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(input) + + hasher.combine(minYear) + + hasher.combine(maxYear) + + hasher.combine(minRating) + + hasher.combine(maxRating) + + hasher.combine(genre) + } + + enum CodingKeys: String, CodingKey { + case input + + case minYear + + case maxYear + + case minRating + + case maxRating + + case genre + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + if $input.isSet { + try codecHelper.encode(input, forKey: .input, container: &container) + } + + try codecHelper.encode(minYear, forKey: .minYear, container: &container) + + try codecHelper.encode(maxYear, forKey: .maxYear, container: &container) + + try codecHelper.encode(minRating, forKey: .minRating, container: &container) + + try codecHelper.encode(maxRating, forKey: .maxRating, container: &container) + + try codecHelper.encode(genre, forKey: .genre, container: &container) + } + } + + public struct Data: Decodable, Sendable { + public struct MovieMoviesMatchingTitle: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + title: String + + public var + genre: String? + + public var + rating: Double? + + public var + imageUrl: String + + public var movieMoviesMatchingTitleKey: MovieKey { + return MovieKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: MovieMoviesMatchingTitle, rhs: MovieMoviesMatchingTitle) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case title + + case genre + + case rating + + case imageUrl + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + title = try codecHelper.decode(String.self, forKey: .title, container: &container) + + genre = try codecHelper.decode(String?.self, forKey: .genre, container: &container) + + rating = try codecHelper.decode(Double?.self, forKey: .rating, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + } + } + + public var + moviesMatchingTitle: [MovieMoviesMatchingTitle] + + public struct MovieMoviesMatchingDescription: Decodable, Sendable, Hashable, Equatable, + Identifiable { + public var + id: UUID + + public var + title: String + + public var + genre: String? + + public var + rating: Double? + + public var + imageUrl: String + + public var movieMoviesMatchingDescriptionKey: MovieKey { + return MovieKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: MovieMoviesMatchingDescription, + rhs: MovieMoviesMatchingDescription) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case title + + case genre + + case rating + + case imageUrl + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + title = try codecHelper.decode(String.self, forKey: .title, container: &container) + + genre = try codecHelper.decode(String?.self, forKey: .genre, container: &container) + + rating = try codecHelper.decode(Double?.self, forKey: .rating, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + } + } + + public var + moviesMatchingDescription: [MovieMoviesMatchingDescription] + + public struct ActorActorsMatchingName: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + name: String + + public var + imageUrl: String + + public var actorActorsMatchingNameKey: ActorKey { + return ActorKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: ActorActorsMatchingName, rhs: ActorActorsMatchingName) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case name + + case imageUrl + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + name = try codecHelper.decode(String.self, forKey: .name, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + } + } + + public var + actorsMatchingName: [ActorActorsMatchingName] + + public struct ReviewReviewsMatchingText: Decodable, Sendable { + public var + id: UUID + + public var + rating: Int? + + public var + reviewText: String? + + public var + reviewDate: LocalDate + + public struct Movie: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + title: String + + public var movieKey: MovieKey { + return MovieKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Movie, rhs: Movie) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case title + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + title = try codecHelper.decode(String.self, forKey: .title, container: &container) + } + } + + public var + movie: Movie + + public struct User: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: String + + public var + username: String + + public var userKey: UserKey { + return UserKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: User, rhs: User) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case username + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(String.self, forKey: .id, container: &container) + + username = try codecHelper.decode(String.self, forKey: .username, container: &container) + } + } + + public var + user: User + + enum CodingKeys: String, CodingKey { + case id + + case rating + + case reviewText + + case reviewDate + + case movie + + case user + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + rating = try codecHelper.decode(Int?.self, forKey: .rating, container: &container) + + reviewText = try codecHelper.decode( + String?.self, + forKey: .reviewText, + container: &container + ) + + reviewDate = try codecHelper.decode( + LocalDate.self, + forKey: .reviewDate, + container: &container + ) + + movie = try codecHelper.decode(Movie.self, forKey: .movie, container: &container) + + user = try codecHelper.decode(User.self, forKey: .user, container: &container) + } + } + + public var + reviewsMatchingText: [ReviewReviewsMatchingText] + } + + public func ref(minYear: Int, + + maxYear: Int, + + minRating: Double, + + maxRating: Double, + + genre: String, + + _ optionalVars: ((inout SearchAllQuery.Variables) -> Void)? = nil) + -> QueryRefObservation< + SearchAllQuery.Data, + SearchAllQuery.Variables + > { + var variables = SearchAllQuery.Variables( + minYear: minYear, + maxYear: maxYear, + minRating: minRating, + maxRating: maxRating, + genre: genre + ) + + if let optionalVars { + optionalVars(&variables) + } + + let ref = dataConnect.query( + name: "SearchAll", + variables: variables, + resultsDataType: SearchAllQuery.Data.self, + publisher: .observableMacro + ) + return ref as! QueryRefObservation + } + + @MainActor + public func execute(minYear: Int, + + maxYear: Int, + + minRating: Double, + + maxRating: Double, + + genre: String, + + _ optionalVars: (@MainActor (inout SearchAllQuery.Variables) -> Void)? = + nil) async throws -> OperationResult { + var variables = SearchAllQuery.Variables( + minYear: minYear, + maxYear: maxYear, + minRating: minRating, + maxRating: maxRating, + genre: genre + ) + + if let optionalVars { + optionalVars(&variables) + } + + let ref = dataConnect.query( + name: "SearchAll", + variables: variables, + resultsDataType: SearchAllQuery.Data.self, + publisher: .observableMacro + ) + + let refCast = ref as! QueryRefObservation + return try await refCast.execute() + } +} + +public class ListMoviesByPartialTitleQuery { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "ListMoviesByPartialTitle" + + public typealias Ref = QueryRefObservation< + ListMoviesByPartialTitleQuery.Data, + ListMoviesByPartialTitleQuery.Variables + > + + public struct Variables: OperationVariable { + public var + searchTerm: String + + public init(searchTerm: String) { + self.searchTerm = searchTerm + } + + public static func == (lhs: Variables, rhs: Variables) -> Bool { + return lhs.searchTerm == rhs.searchTerm + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(searchTerm) + } + + enum CodingKeys: String, CodingKey { + case searchTerm + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + try codecHelper.encode(searchTerm, forKey: .searchTerm, container: &container) + } + } + + public struct Data: Decodable, Sendable { + public struct Movie: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + title: String + + public var + imageUrl: String + + public var + releaseYear: Int? + + public var + genre: String? + + public var + rating: Double? + + public var + description: String? + + public var movieKey: MovieKey { + return MovieKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Movie, rhs: Movie) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case title + + case imageUrl + + case releaseYear + + case genre + + case rating + + case description + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + title = try codecHelper.decode(String.self, forKey: .title, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + + releaseYear = try codecHelper.decode(Int?.self, forKey: .releaseYear, container: &container) + + genre = try codecHelper.decode(String?.self, forKey: .genre, container: &container) + + rating = try codecHelper.decode(Double?.self, forKey: .rating, container: &container) + + description = try codecHelper.decode( + String?.self, + forKey: .description, + container: &container + ) + } + } + + public var + movies: [Movie] + } + + public func ref(searchTerm: String) + -> QueryRefObservation { + var variables = ListMoviesByPartialTitleQuery.Variables(searchTerm: searchTerm) + + let ref = dataConnect.query( + name: "ListMoviesByPartialTitle", + variables: variables, + resultsDataType: ListMoviesByPartialTitleQuery.Data.self, + publisher: .observableMacro + ) + return ref as! QueryRefObservation< + ListMoviesByPartialTitleQuery.Data, + ListMoviesByPartialTitleQuery.Variables + > + } + + @MainActor + public func execute(searchTerm: String) async throws + -> OperationResult { + var variables = ListMoviesByPartialTitleQuery.Variables(searchTerm: searchTerm) + + let ref = dataConnect.query( + name: "ListMoviesByPartialTitle", + variables: variables, + resultsDataType: ListMoviesByPartialTitleQuery.Data.self, + publisher: .observableMacro + ) + + let refCast = ref as! QueryRefObservation< + ListMoviesByPartialTitleQuery.Data, + ListMoviesByPartialTitleQuery.Variables + > + return try await refCast.execute() + } +} + +public class GetUserFavoriteMoviesQuery { + let dataConnect: DataConnect + + init(dataConnect: DataConnect) { + self.dataConnect = dataConnect + } + + public static let OperationName = "GetUserFavoriteMovies" + + public typealias Ref = QueryRefObservation< + GetUserFavoriteMoviesQuery.Data, + GetUserFavoriteMoviesQuery.Variables + > + + public struct Variables: OperationVariable {} + + public struct Data: Decodable, Sendable { + public struct User: Decodable, Sendable { + public struct FavoriteMovieFavoriteMovies: Decodable, Sendable { + public struct Movie: Decodable, Sendable, Hashable, Equatable, Identifiable { + public var + id: UUID + + public var + title: String + + public var + genre: String? + + public var + imageUrl: String + + public var + releaseYear: Int? + + public var + rating: Double? + + public var + description: String? + + public var movieKey: MovieKey { + return MovieKey( + id: id + ) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Movie, rhs: Movie) -> Bool { + return lhs.id == rhs.id + } + + enum CodingKeys: String, CodingKey { + case id + + case title + + case genre + + case imageUrl + + case releaseYear + + case rating + + case description + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + id = try codecHelper.decode(UUID.self, forKey: .id, container: &container) + + title = try codecHelper.decode(String.self, forKey: .title, container: &container) + + genre = try codecHelper.decode(String?.self, forKey: .genre, container: &container) + + imageUrl = try codecHelper.decode(String.self, forKey: .imageUrl, container: &container) + + releaseYear = try codecHelper.decode( + Int?.self, + forKey: .releaseYear, + container: &container + ) + + rating = try codecHelper.decode(Double?.self, forKey: .rating, container: &container) + + description = try codecHelper.decode( + String?.self, + forKey: .description, + container: &container + ) + } + } + + public var + movie: Movie + + enum CodingKeys: String, CodingKey { + case movie + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + movie = try codecHelper.decode(Movie.self, forKey: .movie, container: &container) + } + } + + public var + favoriteMovies: [FavoriteMovieFavoriteMovies] + + enum CodingKeys: String, CodingKey { + case favoriteMovies + } + + public init(from decoder: any Decoder) throws { + var container = try decoder.container(keyedBy: CodingKeys.self) + let codecHelper = CodecHelper() + + favoriteMovies = try codecHelper.decode( + [FavoriteMovieFavoriteMovies].self, + forKey: .favoriteMovies, + container: &container + ) + } + } + + public var + user: User? + } + + public func ref( + ) + -> QueryRefObservation { + var variables = GetUserFavoriteMoviesQuery.Variables() + + let ref = dataConnect.query( + name: "GetUserFavoriteMovies", + variables: variables, + resultsDataType: GetUserFavoriteMoviesQuery.Data.self, + publisher: .observableMacro + ) + return ref as! QueryRefObservation< + GetUserFavoriteMoviesQuery.Data, + GetUserFavoriteMoviesQuery.Variables + > + } + + @MainActor + public func execute( + ) async throws -> OperationResult { + var variables = GetUserFavoriteMoviesQuery.Variables() + + let ref = dataConnect.query( + name: "GetUserFavoriteMovies", + variables: variables, + resultsDataType: GetUserFavoriteMoviesQuery.Data.self, + publisher: .observableMacro + ) + + let refCast = ref as! QueryRefObservation< + GetUserFavoriteMoviesQuery.Data, + GetUserFavoriteMoviesQuery.Variables + > + return try await refCast.execute() + } +} diff --git a/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/implicit.gql b/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/implicit.gql new file mode 100644 index 0000000..c7944ef --- /dev/null +++ b/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/implicit.gql @@ -0,0 +1,40 @@ +extend type FavoriteMovie { + """ + ✨ Implicit foreign key field based on `FavoriteMovie`.`user`. It must match the value of `User`.`id`. See `@ref` for how to customize it. + """ + userId: String! @fdc_generated(from: "FavoriteMovie.user", purpose: IMPLICIT_REF_FIELD) + """ + ✨ Implicit foreign key field based on `FavoriteMovie`.`movie`. It must match the value of `Movie`.`id`. See `@ref` for how to customize it. + """ + movieId: UUID! @fdc_generated(from: "FavoriteMovie.movie", purpose: IMPLICIT_REF_FIELD) +} +extend type MovieActor { + """ + ✨ Implicit foreign key field based on `MovieActor`.`movie`. It must match the value of `Movie`.`id`. See `@ref` for how to customize it. + """ + movieId: UUID! @fdc_generated(from: "MovieActor.movie", purpose: IMPLICIT_REF_FIELD) + """ + ✨ Implicit foreign key field based on `MovieActor`.`actor`. It must match the value of `Actor`.`id`. See `@ref` for how to customize it. + """ + actorId: UUID! @fdc_generated(from: "MovieActor.actor", purpose: IMPLICIT_REF_FIELD) +} +extend type MovieMetadata { + """ + ✨ Implicit primary key field. It's a UUID column default to a generated new value. See `@table` for how to customize it. + """ + id: UUID! @default(expr: "uuidV4()") @fdc_generated(from: "MovieMetadata", purpose: IMPLICIT_KEY_FIELD) + """ + ✨ Implicit foreign key field based on `MovieMetadata`.`movie`. It must match the value of `Movie`.`id`. See `@ref` for how to customize it. + """ + movieId: UUID! @fdc_generated(from: "MovieMetadata.movie", purpose: IMPLICIT_REF_FIELD) +} +extend type Review { + """ + ✨ Implicit foreign key field based on `Review`.`movie`. It must match the value of `Movie`.`id`. See `@ref` for how to customize it. + """ + movieId: UUID! @fdc_generated(from: "Review.movie", purpose: IMPLICIT_REF_FIELD) + """ + ✨ Implicit foreign key field based on `Review`.`user`. It must match the value of `User`.`id`. See `@ref` for how to customize it. + """ + userId: String! @fdc_generated(from: "Review.user", purpose: IMPLICIT_REF_FIELD) +} diff --git a/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/input.gql b/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/input.gql new file mode 100644 index 0000000..c904887 --- /dev/null +++ b/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/input.gql @@ -0,0 +1,1080 @@ +""" +✨ `Actor_KeyOutput` returns the primary key fields of table type `Actor`. + +It has the same format as `Actor_Key`, but is only used as mutation return value. +""" +scalar Actor_KeyOutput +""" +✨ `FavoriteMovie_KeyOutput` returns the primary key fields of table type `FavoriteMovie`. + +It has the same format as `FavoriteMovie_Key`, but is only used as mutation return value. +""" +scalar FavoriteMovie_KeyOutput +""" +✨ `Movie_KeyOutput` returns the primary key fields of table type `Movie`. + +It has the same format as `Movie_Key`, but is only used as mutation return value. +""" +scalar Movie_KeyOutput +""" +✨ `MovieActor_KeyOutput` returns the primary key fields of table type `MovieActor`. + +It has the same format as `MovieActor_Key`, but is only used as mutation return value. +""" +scalar MovieActor_KeyOutput +""" +✨ `MovieMetadata_KeyOutput` returns the primary key fields of table type `MovieMetadata`. + +It has the same format as `MovieMetadata_Key`, but is only used as mutation return value. +""" +scalar MovieMetadata_KeyOutput +""" +✨ `Review_KeyOutput` returns the primary key fields of table type `Review`. + +It has the same format as `Review_Key`, but is only used as mutation return value. +""" +scalar Review_KeyOutput +""" +✨ `User_KeyOutput` returns the primary key fields of table type `User`. + +It has the same format as `User_Key`, but is only used as mutation return value. +""" +scalar User_KeyOutput +""" +✨ Generated data input type for table 'Actor'. It includes all necessary fields for creating or upserting rows into table. +""" +input Actor_Data { + """ + ✨ Generated from Field `Actor`.`id` of type `UUID!` + """ + id: UUID + """ + ✨ `_expr` server value variant of `id` (✨ Generated from Field `Actor`.`id` of type `UUID!`) + """ + id_expr: UUID_Expr + """ + ✨ Generated from Field `Actor`.`imageUrl` of type `String!` + """ + imageUrl: String + """ + ✨ `_expr` server value variant of `imageUrl` (✨ Generated from Field `Actor`.`imageUrl` of type `String!`) + """ + imageUrl_expr: String_Expr + """ + ✨ Generated from Field `Actor`.`name` of type `String!` + """ + name: String + """ + ✨ `_expr` server value variant of `name` (✨ Generated from Field `Actor`.`name` of type `String!`) + """ + name_expr: String_Expr +} +""" +✨ Generated filter input type for table 'Actor'. This input allows filtering objects using various conditions. Use `_or`, `_and`, and `_not` to compose complex filters. +""" +input Actor_Filter { + """ + Apply multiple filter conditions using `AND` logic. + """ + _and: [Actor_Filter!] + """ + Negate the result of the provided filter condition. + """ + _not: Actor_Filter + """ + Apply multiple filter conditions using `OR` logic. + """ + _or: [Actor_Filter!] + """ + ✨ Generated from Field `Actor`.`id` of type `UUID!` + """ + id: UUID_Filter + """ + ✨ Generated from Field `Actor`.`imageUrl` of type `String!` + """ + imageUrl: String_Filter + """ + ✨ Generated from Field `Actor`.`name` of type `String!` + """ + name: String_Filter + """ + ✨ Generated from Field `Actor`.`movieActors_on_actor` of type `[MovieActor!]!` + """ + movieActors_on_actor: MovieActor_ListFilter + """ + ✨ Generated from Field `Actor`.`movies_via_MovieActor` of type `[Movie!]!` + """ + movies_via_MovieActor: Movie_ListFilter +} +""" +✨ Generated first-row input type for table 'Actor'. This input selects the first row matching the filter criteria, ordered according to the specified conditions. +""" +input Actor_FirstRow { + """ + Order the result by the specified fields. + """ + orderBy: [Actor_Order!] + """ + Filters rows based on the specified conditions. + """ + where: Actor_Filter +} +""" +✨ Generated key input type for table 'Actor'. It represents the primary key fields used to uniquely identify a row in the table. +""" +input Actor_Key { + """ + ✨ Generated from Field `Actor`.`id` of type `UUID!` + """ + id: UUID + """ + ✨ `_expr` server value variant of `id` (✨ Generated from Field `Actor`.`id` of type `UUID!`) + """ + id_expr: UUID_Expr +} +""" +✨ Generated list filter input type for table 'Actor'. This input applies filtering logic based on the count or existence of related objects that matches certain criteria. +""" +input Actor_ListFilter { + """ + The desired number of objects that match the condition (defaults to at least one). + """ + count: Int_Filter = {gt:0} + """ + Condition of the related objects to filter for. + """ + exist: Actor_Filter +} +""" +✨ Generated order input type for table 'Actor'. This input defines the sorting order of rows in query results based on one or more fields. +""" +input Actor_Order { + """ + ✨ Generated from Field `Actor`.`id` of type `UUID!` + """ + id: OrderDirection + """ + ✨ Generated from Field `Actor`.`imageUrl` of type `String!` + """ + imageUrl: OrderDirection + """ + ✨ Generated from Field `Actor`.`name` of type `String!` + """ + name: OrderDirection +} +""" +✨ Generated data input type for table 'FavoriteMovie'. It includes all necessary fields for creating or upserting rows into table. +""" +input FavoriteMovie_Data { + """ + ✨ Generated from Field `FavoriteMovie`.`userId` of type `String!` + """ + userId: String + """ + ✨ `_expr` server value variant of `userId` (✨ Generated from Field `FavoriteMovie`.`userId` of type `String!`) + """ + userId_expr: String_Expr + """ + ✨ Generated from Field `FavoriteMovie`.`movieId` of type `UUID!` + """ + movieId: UUID + """ + ✨ `_expr` server value variant of `movieId` (✨ Generated from Field `FavoriteMovie`.`movieId` of type `UUID!`) + """ + movieId_expr: UUID_Expr + """ + ✨ Generated from Field `FavoriteMovie`.`movie` of type `Movie!` + """ + movie: Movie_Key + """ + ✨ Generated from Field `FavoriteMovie`.`user` of type `User!` + """ + user: User_Key +} +""" +✨ Generated filter input type for table 'FavoriteMovie'. This input allows filtering objects using various conditions. Use `_or`, `_and`, and `_not` to compose complex filters. +""" +input FavoriteMovie_Filter { + """ + Apply multiple filter conditions using `AND` logic. + """ + _and: [FavoriteMovie_Filter!] + """ + Negate the result of the provided filter condition. + """ + _not: FavoriteMovie_Filter + """ + Apply multiple filter conditions using `OR` logic. + """ + _or: [FavoriteMovie_Filter!] + """ + ✨ Generated from Field `FavoriteMovie`.`userId` of type `String!` + """ + userId: String_Filter + """ + ✨ Generated from Field `FavoriteMovie`.`movieId` of type `UUID!` + """ + movieId: UUID_Filter + """ + ✨ Generated from Field `FavoriteMovie`.`movie` of type `Movie!` + """ + movie: Movie_Filter + """ + ✨ Generated from Field `FavoriteMovie`.`user` of type `User!` + """ + user: User_Filter +} +""" +✨ Generated first-row input type for table 'FavoriteMovie'. This input selects the first row matching the filter criteria, ordered according to the specified conditions. +""" +input FavoriteMovie_FirstRow { + """ + Order the result by the specified fields. + """ + orderBy: [FavoriteMovie_Order!] + """ + Filters rows based on the specified conditions. + """ + where: FavoriteMovie_Filter +} +""" +✨ Generated key input type for table 'FavoriteMovie'. It represents the primary key fields used to uniquely identify a row in the table. +""" +input FavoriteMovie_Key { + """ + ✨ Generated from Field `FavoriteMovie`.`userId` of type `String!` + """ + userId: String + """ + ✨ `_expr` server value variant of `userId` (✨ Generated from Field `FavoriteMovie`.`userId` of type `String!`) + """ + userId_expr: String_Expr + """ + ✨ Generated from Field `FavoriteMovie`.`movieId` of type `UUID!` + """ + movieId: UUID + """ + ✨ `_expr` server value variant of `movieId` (✨ Generated from Field `FavoriteMovie`.`movieId` of type `UUID!`) + """ + movieId_expr: UUID_Expr +} +""" +✨ Generated list filter input type for table 'FavoriteMovie'. This input applies filtering logic based on the count or existence of related objects that matches certain criteria. +""" +input FavoriteMovie_ListFilter { + """ + The desired number of objects that match the condition (defaults to at least one). + """ + count: Int_Filter = {gt:0} + """ + Condition of the related objects to filter for. + """ + exist: FavoriteMovie_Filter +} +""" +✨ Generated order input type for table 'FavoriteMovie'. This input defines the sorting order of rows in query results based on one or more fields. +""" +input FavoriteMovie_Order { + """ + ✨ Generated from Field `FavoriteMovie`.`userId` of type `String!` + """ + userId: OrderDirection + """ + ✨ Generated from Field `FavoriteMovie`.`movieId` of type `UUID!` + """ + movieId: OrderDirection + """ + ✨ Generated from Field `FavoriteMovie`.`movie` of type `Movie!` + """ + movie: Movie_Order + """ + ✨ Generated from Field `FavoriteMovie`.`user` of type `User!` + """ + user: User_Order +} +""" +✨ Generated data input type for table 'Movie'. It includes all necessary fields for creating or upserting rows into table. +""" +input Movie_Data { + """ + ✨ Generated from Field `Movie`.`id` of type `UUID!` + """ + id: UUID + """ + ✨ `_expr` server value variant of `id` (✨ Generated from Field `Movie`.`id` of type `UUID!`) + """ + id_expr: UUID_Expr + """ + ✨ Generated from Field `Movie`.`description` of type `String` + """ + description: String + """ + ✨ `_expr` server value variant of `description` (✨ Generated from Field `Movie`.`description` of type `String`) + """ + description_expr: String_Expr + """ + ✨ Generated from Field `Movie`.`genre` of type `String` + """ + genre: String + """ + ✨ `_expr` server value variant of `genre` (✨ Generated from Field `Movie`.`genre` of type `String`) + """ + genre_expr: String_Expr + """ + ✨ Generated from Field `Movie`.`imageUrl` of type `String!` + """ + imageUrl: String + """ + ✨ `_expr` server value variant of `imageUrl` (✨ Generated from Field `Movie`.`imageUrl` of type `String!`) + """ + imageUrl_expr: String_Expr + """ + ✨ Generated from Field `Movie`.`rating` of type `Float` + """ + rating: Float + """ + ✨ Generated from Field `Movie`.`releaseYear` of type `Int` + """ + releaseYear: Int + """ + ✨ Generated from Field `Movie`.`tags` of type `[String]` + """ + tags: [String!] + """ + ✨ Generated from Field `Movie`.`title` of type `String!` + """ + title: String + """ + ✨ `_expr` server value variant of `title` (✨ Generated from Field `Movie`.`title` of type `String!`) + """ + title_expr: String_Expr +} +""" +✨ Generated filter input type for table 'Movie'. This input allows filtering objects using various conditions. Use `_or`, `_and`, and `_not` to compose complex filters. +""" +input Movie_Filter { + """ + Apply multiple filter conditions using `AND` logic. + """ + _and: [Movie_Filter!] + """ + Negate the result of the provided filter condition. + """ + _not: Movie_Filter + """ + Apply multiple filter conditions using `OR` logic. + """ + _or: [Movie_Filter!] + """ + ✨ Generated from Field `Movie`.`id` of type `UUID!` + """ + id: UUID_Filter + """ + ✨ Generated from Field `Movie`.`description` of type `String` + """ + description: String_Filter + """ + ✨ Generated from Field `Movie`.`genre` of type `String` + """ + genre: String_Filter + """ + ✨ Generated from Field `Movie`.`imageUrl` of type `String!` + """ + imageUrl: String_Filter + """ + ✨ Generated from Field `Movie`.`rating` of type `Float` + """ + rating: Float_Filter + """ + ✨ Generated from Field `Movie`.`releaseYear` of type `Int` + """ + releaseYear: Int_Filter + """ + ✨ Generated from Field `Movie`.`tags` of type `[String]` + """ + tags: String_ListFilter + """ + ✨ Generated from Field `Movie`.`title` of type `String!` + """ + title: String_Filter + """ + ✨ Generated from Field `Movie`.`favorite_movies_on_movie` of type `[FavoriteMovie!]!` + """ + favorite_movies_on_movie: FavoriteMovie_ListFilter + """ + ✨ Generated from Field `Movie`.`movieActors_on_movie` of type `[MovieActor!]!` + """ + movieActors_on_movie: MovieActor_ListFilter + """ + ✨ Generated from Field `Movie`.`movieMetadatas_on_movie` of type `[MovieMetadata!]!` + """ + movieMetadatas_on_movie: MovieMetadata_ListFilter + """ + ✨ Generated from Field `Movie`.`reviews_on_movie` of type `[Review!]!` + """ + reviews_on_movie: Review_ListFilter + """ + ✨ Generated from Field `Movie`.`actors_via_MovieActor` of type `[Actor!]!` + """ + actors_via_MovieActor: Actor_ListFilter + """ + ✨ Generated from Field `Movie`.`users_via_FavoriteMovie` of type `[User!]!` + """ + users_via_FavoriteMovie: User_ListFilter + """ + ✨ Generated from Field `Movie`.`users_via_Review` of type `[User!]!` + """ + users_via_Review: User_ListFilter +} +""" +✨ Generated first-row input type for table 'Movie'. This input selects the first row matching the filter criteria, ordered according to the specified conditions. +""" +input Movie_FirstRow { + """ + Order the result by the specified fields. + """ + orderBy: [Movie_Order!] + """ + Filters rows based on the specified conditions. + """ + where: Movie_Filter +} +""" +✨ Generated key input type for table 'Movie'. It represents the primary key fields used to uniquely identify a row in the table. +""" +input Movie_Key { + """ + ✨ Generated from Field `Movie`.`id` of type `UUID!` + """ + id: UUID + """ + ✨ `_expr` server value variant of `id` (✨ Generated from Field `Movie`.`id` of type `UUID!`) + """ + id_expr: UUID_Expr +} +""" +✨ Generated list filter input type for table 'Movie'. This input applies filtering logic based on the count or existence of related objects that matches certain criteria. +""" +input Movie_ListFilter { + """ + The desired number of objects that match the condition (defaults to at least one). + """ + count: Int_Filter = {gt:0} + """ + Condition of the related objects to filter for. + """ + exist: Movie_Filter +} +""" +✨ Generated order input type for table 'Movie'. This input defines the sorting order of rows in query results based on one or more fields. +""" +input Movie_Order { + """ + ✨ Generated from Field `Movie`.`id` of type `UUID!` + """ + id: OrderDirection + """ + ✨ Generated from Field `Movie`.`description` of type `String` + """ + description: OrderDirection + """ + ✨ Generated from Field `Movie`.`genre` of type `String` + """ + genre: OrderDirection + """ + ✨ Generated from Field `Movie`.`imageUrl` of type `String!` + """ + imageUrl: OrderDirection + """ + ✨ Generated from Field `Movie`.`rating` of type `Float` + """ + rating: OrderDirection + """ + ✨ Generated from Field `Movie`.`releaseYear` of type `Int` + """ + releaseYear: OrderDirection + """ + ✨ Generated from Field `Movie`.`title` of type `String!` + """ + title: OrderDirection +} +""" +✨ Generated data input type for table 'MovieActor'. It includes all necessary fields for creating or upserting rows into table. +""" +input MovieActor_Data { + """ + ✨ Generated from Field `MovieActor`.`movieId` of type `UUID!` + """ + movieId: UUID + """ + ✨ `_expr` server value variant of `movieId` (✨ Generated from Field `MovieActor`.`movieId` of type `UUID!`) + """ + movieId_expr: UUID_Expr + """ + ✨ Generated from Field `MovieActor`.`actorId` of type `UUID!` + """ + actorId: UUID + """ + ✨ `_expr` server value variant of `actorId` (✨ Generated from Field `MovieActor`.`actorId` of type `UUID!`) + """ + actorId_expr: UUID_Expr + """ + ✨ Generated from Field `MovieActor`.`actor` of type `Actor!` + """ + actor: Actor_Key + """ + ✨ Generated from Field `MovieActor`.`movie` of type `Movie!` + """ + movie: Movie_Key + """ + ✨ Generated from Field `MovieActor`.`role` of type `String!` + """ + role: String + """ + ✨ `_expr` server value variant of `role` (✨ Generated from Field `MovieActor`.`role` of type `String!`) + """ + role_expr: String_Expr +} +""" +✨ Generated filter input type for table 'MovieActor'. This input allows filtering objects using various conditions. Use `_or`, `_and`, and `_not` to compose complex filters. +""" +input MovieActor_Filter { + """ + Apply multiple filter conditions using `AND` logic. + """ + _and: [MovieActor_Filter!] + """ + Negate the result of the provided filter condition. + """ + _not: MovieActor_Filter + """ + Apply multiple filter conditions using `OR` logic. + """ + _or: [MovieActor_Filter!] + """ + ✨ Generated from Field `MovieActor`.`movieId` of type `UUID!` + """ + movieId: UUID_Filter + """ + ✨ Generated from Field `MovieActor`.`actorId` of type `UUID!` + """ + actorId: UUID_Filter + """ + ✨ Generated from Field `MovieActor`.`actor` of type `Actor!` + """ + actor: Actor_Filter + """ + ✨ Generated from Field `MovieActor`.`movie` of type `Movie!` + """ + movie: Movie_Filter + """ + ✨ Generated from Field `MovieActor`.`role` of type `String!` + """ + role: String_Filter +} +""" +✨ Generated first-row input type for table 'MovieActor'. This input selects the first row matching the filter criteria, ordered according to the specified conditions. +""" +input MovieActor_FirstRow { + """ + Order the result by the specified fields. + """ + orderBy: [MovieActor_Order!] + """ + Filters rows based on the specified conditions. + """ + where: MovieActor_Filter +} +""" +✨ Generated key input type for table 'MovieActor'. It represents the primary key fields used to uniquely identify a row in the table. +""" +input MovieActor_Key { + """ + ✨ Generated from Field `MovieActor`.`movieId` of type `UUID!` + """ + movieId: UUID + """ + ✨ `_expr` server value variant of `movieId` (✨ Generated from Field `MovieActor`.`movieId` of type `UUID!`) + """ + movieId_expr: UUID_Expr + """ + ✨ Generated from Field `MovieActor`.`actorId` of type `UUID!` + """ + actorId: UUID + """ + ✨ `_expr` server value variant of `actorId` (✨ Generated from Field `MovieActor`.`actorId` of type `UUID!`) + """ + actorId_expr: UUID_Expr +} +""" +✨ Generated list filter input type for table 'MovieActor'. This input applies filtering logic based on the count or existence of related objects that matches certain criteria. +""" +input MovieActor_ListFilter { + """ + The desired number of objects that match the condition (defaults to at least one). + """ + count: Int_Filter = {gt:0} + """ + Condition of the related objects to filter for. + """ + exist: MovieActor_Filter +} +""" +✨ Generated order input type for table 'MovieActor'. This input defines the sorting order of rows in query results based on one or more fields. +""" +input MovieActor_Order { + """ + ✨ Generated from Field `MovieActor`.`movieId` of type `UUID!` + """ + movieId: OrderDirection + """ + ✨ Generated from Field `MovieActor`.`actorId` of type `UUID!` + """ + actorId: OrderDirection + """ + ✨ Generated from Field `MovieActor`.`actor` of type `Actor!` + """ + actor: Actor_Order + """ + ✨ Generated from Field `MovieActor`.`movie` of type `Movie!` + """ + movie: Movie_Order + """ + ✨ Generated from Field `MovieActor`.`role` of type `String!` + """ + role: OrderDirection +} +""" +✨ Generated data input type for table 'MovieMetadata'. It includes all necessary fields for creating or upserting rows into table. +""" +input MovieMetadata_Data { + """ + ✨ Generated from Field `MovieMetadata`.`id` of type `UUID!` + """ + id: UUID + """ + ✨ `_expr` server value variant of `id` (✨ Generated from Field `MovieMetadata`.`id` of type `UUID!`) + """ + id_expr: UUID_Expr + """ + ✨ Generated from Field `MovieMetadata`.`movieId` of type `UUID!` + """ + movieId: UUID + """ + ✨ `_expr` server value variant of `movieId` (✨ Generated from Field `MovieMetadata`.`movieId` of type `UUID!`) + """ + movieId_expr: UUID_Expr + """ + ✨ Generated from Field `MovieMetadata`.`movie` of type `Movie!` + """ + movie: Movie_Key + """ + ✨ Generated from Field `MovieMetadata`.`director` of type `String` + """ + director: String + """ + ✨ `_expr` server value variant of `director` (✨ Generated from Field `MovieMetadata`.`director` of type `String`) + """ + director_expr: String_Expr +} +""" +✨ Generated filter input type for table 'MovieMetadata'. This input allows filtering objects using various conditions. Use `_or`, `_and`, and `_not` to compose complex filters. +""" +input MovieMetadata_Filter { + """ + Apply multiple filter conditions using `AND` logic. + """ + _and: [MovieMetadata_Filter!] + """ + Negate the result of the provided filter condition. + """ + _not: MovieMetadata_Filter + """ + Apply multiple filter conditions using `OR` logic. + """ + _or: [MovieMetadata_Filter!] + """ + ✨ Generated from Field `MovieMetadata`.`id` of type `UUID!` + """ + id: UUID_Filter + """ + ✨ Generated from Field `MovieMetadata`.`movieId` of type `UUID!` + """ + movieId: UUID_Filter + """ + ✨ Generated from Field `MovieMetadata`.`movie` of type `Movie!` + """ + movie: Movie_Filter + """ + ✨ Generated from Field `MovieMetadata`.`director` of type `String` + """ + director: String_Filter +} +""" +✨ Generated first-row input type for table 'MovieMetadata'. This input selects the first row matching the filter criteria, ordered according to the specified conditions. +""" +input MovieMetadata_FirstRow { + """ + Order the result by the specified fields. + """ + orderBy: [MovieMetadata_Order!] + """ + Filters rows based on the specified conditions. + """ + where: MovieMetadata_Filter +} +""" +✨ Generated key input type for table 'MovieMetadata'. It represents the primary key fields used to uniquely identify a row in the table. +""" +input MovieMetadata_Key { + """ + ✨ Generated from Field `MovieMetadata`.`id` of type `UUID!` + """ + id: UUID + """ + ✨ `_expr` server value variant of `id` (✨ Generated from Field `MovieMetadata`.`id` of type `UUID!`) + """ + id_expr: UUID_Expr +} +""" +✨ Generated list filter input type for table 'MovieMetadata'. This input applies filtering logic based on the count or existence of related objects that matches certain criteria. +""" +input MovieMetadata_ListFilter { + """ + The desired number of objects that match the condition (defaults to at least one). + """ + count: Int_Filter = {gt:0} + """ + Condition of the related objects to filter for. + """ + exist: MovieMetadata_Filter +} +""" +✨ Generated order input type for table 'MovieMetadata'. This input defines the sorting order of rows in query results based on one or more fields. +""" +input MovieMetadata_Order { + """ + ✨ Generated from Field `MovieMetadata`.`id` of type `UUID!` + """ + id: OrderDirection + """ + ✨ Generated from Field `MovieMetadata`.`movieId` of type `UUID!` + """ + movieId: OrderDirection + """ + ✨ Generated from Field `MovieMetadata`.`movie` of type `Movie!` + """ + movie: Movie_Order + """ + ✨ Generated from Field `MovieMetadata`.`director` of type `String` + """ + director: OrderDirection +} +""" +✨ Generated data input type for table 'Review'. It includes all necessary fields for creating or upserting rows into table. +""" +input Review_Data { + """ + ✨ Generated from Field `Review`.`movieId` of type `UUID!` + """ + movieId: UUID + """ + ✨ `_expr` server value variant of `movieId` (✨ Generated from Field `Review`.`movieId` of type `UUID!`) + """ + movieId_expr: UUID_Expr + """ + ✨ Generated from Field `Review`.`userId` of type `String!` + """ + userId: String + """ + ✨ `_expr` server value variant of `userId` (✨ Generated from Field `Review`.`userId` of type `String!`) + """ + userId_expr: String_Expr + """ + ✨ Generated from Field `Review`.`movie` of type `Movie!` + """ + movie: Movie_Key + """ + ✨ Generated from Field `Review`.`user` of type `User!` + """ + user: User_Key + """ + ✨ Generated from Field `Review`.`id` of type `UUID!` + """ + id: UUID + """ + ✨ `_expr` server value variant of `id` (✨ Generated from Field `Review`.`id` of type `UUID!`) + """ + id_expr: UUID_Expr + """ + ✨ Generated from Field `Review`.`rating` of type `Int` + """ + rating: Int + """ + ✨ Generated from Field `Review`.`reviewDate` of type `Date!` + """ + reviewDate: Date + """ + ✨ `_date` server value variant of `reviewDate` (✨ Generated from Field `Review`.`reviewDate` of type `Date!`) + """ + reviewDate_date: Date_Relative + """ + ✨ `_expr` server value variant of `reviewDate` (✨ Generated from Field `Review`.`reviewDate` of type `Date!`) + """ + reviewDate_expr: Date_Expr + """ + ✨ Generated from Field `Review`.`reviewText` of type `String` + """ + reviewText: String + """ + ✨ `_expr` server value variant of `reviewText` (✨ Generated from Field `Review`.`reviewText` of type `String`) + """ + reviewText_expr: String_Expr +} +""" +✨ Generated filter input type for table 'Review'. This input allows filtering objects using various conditions. Use `_or`, `_and`, and `_not` to compose complex filters. +""" +input Review_Filter { + """ + Apply multiple filter conditions using `AND` logic. + """ + _and: [Review_Filter!] + """ + Negate the result of the provided filter condition. + """ + _not: Review_Filter + """ + Apply multiple filter conditions using `OR` logic. + """ + _or: [Review_Filter!] + """ + ✨ Generated from Field `Review`.`movieId` of type `UUID!` + """ + movieId: UUID_Filter + """ + ✨ Generated from Field `Review`.`userId` of type `String!` + """ + userId: String_Filter + """ + ✨ Generated from Field `Review`.`movie` of type `Movie!` + """ + movie: Movie_Filter + """ + ✨ Generated from Field `Review`.`user` of type `User!` + """ + user: User_Filter + """ + ✨ Generated from Field `Review`.`id` of type `UUID!` + """ + id: UUID_Filter + """ + ✨ Generated from Field `Review`.`rating` of type `Int` + """ + rating: Int_Filter + """ + ✨ Generated from Field `Review`.`reviewDate` of type `Date!` + """ + reviewDate: Date_Filter + """ + ✨ Generated from Field `Review`.`reviewText` of type `String` + """ + reviewText: String_Filter +} +""" +✨ Generated first-row input type for table 'Review'. This input selects the first row matching the filter criteria, ordered according to the specified conditions. +""" +input Review_FirstRow { + """ + Order the result by the specified fields. + """ + orderBy: [Review_Order!] + """ + Filters rows based on the specified conditions. + """ + where: Review_Filter +} +""" +✨ Generated key input type for table 'Review'. It represents the primary key fields used to uniquely identify a row in the table. +""" +input Review_Key { + """ + ✨ Generated from Field `Review`.`movieId` of type `UUID!` + """ + movieId: UUID + """ + ✨ `_expr` server value variant of `movieId` (✨ Generated from Field `Review`.`movieId` of type `UUID!`) + """ + movieId_expr: UUID_Expr + """ + ✨ Generated from Field `Review`.`userId` of type `String!` + """ + userId: String + """ + ✨ `_expr` server value variant of `userId` (✨ Generated from Field `Review`.`userId` of type `String!`) + """ + userId_expr: String_Expr +} +""" +✨ Generated list filter input type for table 'Review'. This input applies filtering logic based on the count or existence of related objects that matches certain criteria. +""" +input Review_ListFilter { + """ + The desired number of objects that match the condition (defaults to at least one). + """ + count: Int_Filter = {gt:0} + """ + Condition of the related objects to filter for. + """ + exist: Review_Filter +} +""" +✨ Generated order input type for table 'Review'. This input defines the sorting order of rows in query results based on one or more fields. +""" +input Review_Order { + """ + ✨ Generated from Field `Review`.`movieId` of type `UUID!` + """ + movieId: OrderDirection + """ + ✨ Generated from Field `Review`.`userId` of type `String!` + """ + userId: OrderDirection + """ + ✨ Generated from Field `Review`.`movie` of type `Movie!` + """ + movie: Movie_Order + """ + ✨ Generated from Field `Review`.`user` of type `User!` + """ + user: User_Order + """ + ✨ Generated from Field `Review`.`id` of type `UUID!` + """ + id: OrderDirection + """ + ✨ Generated from Field `Review`.`rating` of type `Int` + """ + rating: OrderDirection + """ + ✨ Generated from Field `Review`.`reviewDate` of type `Date!` + """ + reviewDate: OrderDirection + """ + ✨ Generated from Field `Review`.`reviewText` of type `String` + """ + reviewText: OrderDirection +} +""" +✨ Generated data input type for table 'User'. It includes all necessary fields for creating or upserting rows into table. +""" +input User_Data { + """ + ✨ Generated from Field `User`.`id` of type `String!` + """ + id: String + """ + ✨ `_expr` server value variant of `id` (✨ Generated from Field `User`.`id` of type `String!`) + """ + id_expr: String_Expr + """ + ✨ Generated from Field `User`.`username` of type `String!` + """ + username: String + """ + ✨ `_expr` server value variant of `username` (✨ Generated from Field `User`.`username` of type `String!`) + """ + username_expr: String_Expr +} +""" +✨ Generated filter input type for table 'User'. This input allows filtering objects using various conditions. Use `_or`, `_and`, and `_not` to compose complex filters. +""" +input User_Filter { + """ + Apply multiple filter conditions using `AND` logic. + """ + _and: [User_Filter!] + """ + Negate the result of the provided filter condition. + """ + _not: User_Filter + """ + Apply multiple filter conditions using `OR` logic. + """ + _or: [User_Filter!] + """ + ✨ Generated from Field `User`.`id` of type `String!` + """ + id: String_Filter + """ + ✨ Generated from Field `User`.`username` of type `String!` + """ + username: String_Filter + """ + ✨ Generated from Field `User`.`favorite_movies_on_user` of type `[FavoriteMovie!]!` + """ + favorite_movies_on_user: FavoriteMovie_ListFilter + """ + ✨ Generated from Field `User`.`reviews_on_user` of type `[Review!]!` + """ + reviews_on_user: Review_ListFilter + """ + ✨ Generated from Field `User`.`movies_via_FavoriteMovie` of type `[Movie!]!` + """ + movies_via_FavoriteMovie: Movie_ListFilter + """ + ✨ Generated from Field `User`.`movies_via_Review` of type `[Movie!]!` + """ + movies_via_Review: Movie_ListFilter +} +""" +✨ Generated first-row input type for table 'User'. This input selects the first row matching the filter criteria, ordered according to the specified conditions. +""" +input User_FirstRow { + """ + Order the result by the specified fields. + """ + orderBy: [User_Order!] + """ + Filters rows based on the specified conditions. + """ + where: User_Filter +} +""" +✨ Generated key input type for table 'User'. It represents the primary key fields used to uniquely identify a row in the table. +""" +input User_Key { + """ + ✨ Generated from Field `User`.`id` of type `String!` + """ + id: String + """ + ✨ `_expr` server value variant of `id` (✨ Generated from Field `User`.`id` of type `String!`) + """ + id_expr: String_Expr +} +""" +✨ Generated list filter input type for table 'User'. This input applies filtering logic based on the count or existence of related objects that matches certain criteria. +""" +input User_ListFilter { + """ + The desired number of objects that match the condition (defaults to at least one). + """ + count: Int_Filter = {gt:0} + """ + Condition of the related objects to filter for. + """ + exist: User_Filter +} +""" +✨ Generated order input type for table 'User'. This input defines the sorting order of rows in query results based on one or more fields. +""" +input User_Order { + """ + ✨ Generated from Field `User`.`id` of type `String!` + """ + id: OrderDirection + """ + ✨ Generated from Field `User`.`username` of type `String!` + """ + username: OrderDirection +} diff --git a/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/mutation.gql b/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/mutation.gql new file mode 100644 index 0000000..1dad217 --- /dev/null +++ b/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/mutation.gql @@ -0,0 +1,766 @@ +extend type Mutation { + """ + ✨ Insert a single `Actor` into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + actor_insert( + """ + Data object to insert into the table. + """ + data: Actor_Data! + ): Actor_KeyOutput! @fdc_generated(from: "Actor", purpose: INSERT_SINGLE) + """ + ✨ Insert a single `FavoriteMovie` into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + favorite_movie_insert( + """ + Data object to insert into the table. + """ + data: FavoriteMovie_Data! + ): FavoriteMovie_KeyOutput! @fdc_generated(from: "FavoriteMovie", purpose: INSERT_SINGLE) + """ + ✨ Insert a single `Movie` into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + movie_insert( + """ + Data object to insert into the table. + """ + data: Movie_Data! + ): Movie_KeyOutput! @fdc_generated(from: "Movie", purpose: INSERT_SINGLE) + """ + ✨ Insert a single `MovieActor` into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + movieActor_insert( + """ + Data object to insert into the table. + """ + data: MovieActor_Data! + ): MovieActor_KeyOutput! @fdc_generated(from: "MovieActor", purpose: INSERT_SINGLE) + """ + ✨ Insert a single `MovieMetadata` into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + movieMetadata_insert( + """ + Data object to insert into the table. + """ + data: MovieMetadata_Data! + ): MovieMetadata_KeyOutput! @fdc_generated(from: "MovieMetadata", purpose: INSERT_SINGLE) + """ + ✨ Insert a single `Review` into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + review_insert( + """ + Data object to insert into the table. + """ + data: Review_Data! + ): Review_KeyOutput! @fdc_generated(from: "Review", purpose: INSERT_SINGLE) + """ + ✨ Insert a single `User` into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + user_insert( + """ + Data object to insert into the table. + """ + data: User_Data! + ): User_KeyOutput! @fdc_generated(from: "User", purpose: INSERT_SINGLE) + """ + ✨ Insert `Actor` objects into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + actor_insertMany( + """ + List of data objects to insert into the table. + """ + data: [Actor_Data!]! + ): [Actor_KeyOutput!]! @fdc_generated(from: "Actor", purpose: INSERT_MULTIPLE) + """ + ✨ Insert `FavoriteMovie` objects into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + favorite_movie_insertMany( + """ + List of data objects to insert into the table. + """ + data: [FavoriteMovie_Data!]! + ): [FavoriteMovie_KeyOutput!]! @fdc_generated(from: "FavoriteMovie", purpose: INSERT_MULTIPLE) + """ + ✨ Insert `Movie` objects into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + movie_insertMany( + """ + List of data objects to insert into the table. + """ + data: [Movie_Data!]! + ): [Movie_KeyOutput!]! @fdc_generated(from: "Movie", purpose: INSERT_MULTIPLE) + """ + ✨ Insert `MovieActor` objects into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + movieActor_insertMany( + """ + List of data objects to insert into the table. + """ + data: [MovieActor_Data!]! + ): [MovieActor_KeyOutput!]! @fdc_generated(from: "MovieActor", purpose: INSERT_MULTIPLE) + """ + ✨ Insert `MovieMetadata` objects into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + movieMetadata_insertMany( + """ + List of data objects to insert into the table. + """ + data: [MovieMetadata_Data!]! + ): [MovieMetadata_KeyOutput!]! @fdc_generated(from: "MovieMetadata", purpose: INSERT_MULTIPLE) + """ + ✨ Insert `Review` objects into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + review_insertMany( + """ + List of data objects to insert into the table. + """ + data: [Review_Data!]! + ): [Review_KeyOutput!]! @fdc_generated(from: "Review", purpose: INSERT_MULTIPLE) + """ + ✨ Insert `User` objects into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + user_insertMany( + """ + List of data objects to insert into the table. + """ + data: [User_Data!]! + ): [User_KeyOutput!]! @fdc_generated(from: "User", purpose: INSERT_MULTIPLE) + """ + ✨ Insert or update a single `Actor` into the table, based on the primary key. Returns the key of the newly inserted `Actor`. + """ + actor_upsert( + """ + Data object to insert or update if it already exists. + """ + data: Actor_Data! + ): Actor_KeyOutput! @fdc_generated(from: "Actor", purpose: UPSERT_SINGLE) + """ + ✨ Insert or update a single `FavoriteMovie` into the table, based on the primary key. Returns the key of the newly inserted `FavoriteMovie`. + """ + favorite_movie_upsert( + """ + Data object to insert or update if it already exists. + """ + data: FavoriteMovie_Data! + ): FavoriteMovie_KeyOutput! @fdc_generated(from: "FavoriteMovie", 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 object to insert or update if it already exists. + """ + data: Movie_Data! + ): Movie_KeyOutput! @fdc_generated(from: "Movie", purpose: UPSERT_SINGLE) + """ + ✨ Insert or update a single `MovieActor` into the table, based on the primary key. Returns the key of the newly inserted `MovieActor`. + """ + movieActor_upsert( + """ + Data object to insert or update if it already exists. + """ + data: MovieActor_Data! + ): MovieActor_KeyOutput! @fdc_generated(from: "MovieActor", purpose: UPSERT_SINGLE) + """ + ✨ Insert or update a single `MovieMetadata` into the table, based on the primary key. Returns the key of the newly inserted `MovieMetadata`. + """ + movieMetadata_upsert( + """ + Data object to insert or update if it already exists. + """ + data: MovieMetadata_Data! + ): MovieMetadata_KeyOutput! @fdc_generated(from: "MovieMetadata", purpose: UPSERT_SINGLE) + """ + ✨ Insert or update a single `Review` into the table, based on the primary key. Returns the key of the newly inserted `Review`. + """ + review_upsert( + """ + Data object to insert or update if it already exists. + """ + data: Review_Data! + ): Review_KeyOutput! @fdc_generated(from: "Review", purpose: UPSERT_SINGLE) + """ + ✨ Insert or update a single `User` into the table, based on the primary key. Returns the key of the newly inserted `User`. + """ + user_upsert( + """ + Data object to insert or update if it already exists. + """ + data: User_Data! + ): User_KeyOutput! @fdc_generated(from: "User", purpose: UPSERT_SINGLE) + """ + ✨ Insert or update `Actor` objects into the table, based on the primary key. Returns the key of the newly inserted `Actor`. + """ + actor_upsertMany( + """ + List of data objects to insert or update if it already exists. + """ + data: [Actor_Data!]! + ): [Actor_KeyOutput!]! @fdc_generated(from: "Actor", purpose: UPSERT_MULTIPLE) + """ + ✨ Insert or update `FavoriteMovie` objects into the table, based on the primary key. Returns the key of the newly inserted `FavoriteMovie`. + """ + favorite_movie_upsertMany( + """ + List of data objects to insert or update if it already exists. + """ + data: [FavoriteMovie_Data!]! + ): [FavoriteMovie_KeyOutput!]! @fdc_generated(from: "FavoriteMovie", purpose: UPSERT_MULTIPLE) + """ + ✨ Insert or update `Movie` objects into the table, based on the primary key. Returns the key of the newly inserted `Movie`. + """ + movie_upsertMany( + """ + List of data objects to insert or update if it already exists. + """ + data: [Movie_Data!]! + ): [Movie_KeyOutput!]! @fdc_generated(from: "Movie", purpose: UPSERT_MULTIPLE) + """ + ✨ Insert or update `MovieActor` objects into the table, based on the primary key. Returns the key of the newly inserted `MovieActor`. + """ + movieActor_upsertMany( + """ + List of data objects to insert or update if it already exists. + """ + data: [MovieActor_Data!]! + ): [MovieActor_KeyOutput!]! @fdc_generated(from: "MovieActor", purpose: UPSERT_MULTIPLE) + """ + ✨ Insert or update `MovieMetadata` objects into the table, based on the primary key. Returns the key of the newly inserted `MovieMetadata`. + """ + movieMetadata_upsertMany( + """ + List of data objects to insert or update if it already exists. + """ + data: [MovieMetadata_Data!]! + ): [MovieMetadata_KeyOutput!]! @fdc_generated(from: "MovieMetadata", purpose: UPSERT_MULTIPLE) + """ + ✨ Insert or update `Review` objects into the table, based on the primary key. Returns the key of the newly inserted `Review`. + """ + review_upsertMany( + """ + List of data objects to insert or update if it already exists. + """ + data: [Review_Data!]! + ): [Review_KeyOutput!]! @fdc_generated(from: "Review", purpose: UPSERT_MULTIPLE) + """ + ✨ Insert or update `User` objects into the table, based on the primary key. Returns the key of the newly inserted `User`. + """ + user_upsertMany( + """ + List of data objects to insert or update if it already exists. + """ + data: [User_Data!]! + ): [User_KeyOutput!]! @fdc_generated(from: "User", purpose: UPSERT_MULTIPLE) + """ + ✨ Update a single `Actor` based on `id`, `key` or `first`, setting columns specified in `data`. Returns `null` if not found. + """ + actor_update( + """ + The unique ID of the object. + """ + id: UUID + + """ + The key used to identify the object. + """ + key: Actor_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: Actor_FirstRow + + """ + Data object containing fields to be updated. + """ + data: Actor_Data! + ): Actor_KeyOutput @fdc_generated(from: "Actor", purpose: UPDATE_SINGLE) + """ + ✨ Update a single `FavoriteMovie` based on `id`, `key` or `first`, setting columns specified in `data`. Returns `null` if not found. + """ + favorite_movie_update( + """ + The key used to identify the object. + """ + key: FavoriteMovie_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: FavoriteMovie_FirstRow + + """ + Data object containing fields to be updated. + """ + data: FavoriteMovie_Data! + ): FavoriteMovie_KeyOutput @fdc_generated(from: "FavoriteMovie", purpose: UPDATE_SINGLE) + """ + ✨ Update a single `Movie` based on `id`, `key` or `first`, setting columns specified in `data`. Returns `null` if not found. + """ + movie_update( + """ + The unique ID of the object. + """ + id: UUID + + """ + The key used to identify the object. + """ + key: Movie_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: Movie_FirstRow + + """ + Data object containing fields to be updated. + """ + data: Movie_Data! + ): Movie_KeyOutput @fdc_generated(from: "Movie", purpose: UPDATE_SINGLE) + """ + ✨ Update a single `MovieActor` based on `id`, `key` or `first`, setting columns specified in `data`. Returns `null` if not found. + """ + movieActor_update( + """ + The key used to identify the object. + """ + key: MovieActor_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: MovieActor_FirstRow + + """ + Data object containing fields to be updated. + """ + data: MovieActor_Data! + ): MovieActor_KeyOutput @fdc_generated(from: "MovieActor", purpose: UPDATE_SINGLE) + """ + ✨ Update a single `MovieMetadata` based on `id`, `key` or `first`, setting columns specified in `data`. Returns `null` if not found. + """ + movieMetadata_update( + """ + The unique ID of the object. + """ + id: UUID + + """ + The key used to identify the object. + """ + key: MovieMetadata_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: MovieMetadata_FirstRow + + """ + Data object containing fields to be updated. + """ + data: MovieMetadata_Data! + ): MovieMetadata_KeyOutput @fdc_generated(from: "MovieMetadata", purpose: UPDATE_SINGLE) + """ + ✨ Update a single `Review` based on `id`, `key` or `first`, setting columns specified in `data`. Returns `null` if not found. + """ + review_update( + """ + The key used to identify the object. + """ + key: Review_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: Review_FirstRow + + """ + Data object containing fields to be updated. + """ + data: Review_Data! + ): Review_KeyOutput @fdc_generated(from: "Review", purpose: UPDATE_SINGLE) + """ + ✨ Update a single `User` based on `id`, `key` or `first`, setting columns specified in `data`. Returns `null` if not found. + """ + user_update( + """ + The unique ID of the object. + """ + id: String + + """ + ✨ `_expr` server value variant of `id` (The unique ID of the object.) + """ + id_expr: String_Expr + + """ + The key used to identify the object. + """ + key: User_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: User_FirstRow + + """ + Data object containing fields to be updated. + """ + data: User_Data! + ): User_KeyOutput @fdc_generated(from: "User", purpose: UPDATE_SINGLE) + """ + ✨ Update `Actor` objects matching `where` conditions (or `all`, if true) according to `data`. Returns the number of rows updated. + """ + actor_updateMany( + """ + Filter condition to specify which rows to update. + """ + where: Actor_Filter + + """ + Set to true to update all rows. + """ + all: Boolean = false + + """ + Data object containing fields to update. + """ + data: Actor_Data! + ): Int! @fdc_generated(from: "Actor", purpose: UPDATE_MULTIPLE) + """ + ✨ Update `FavoriteMovie` objects matching `where` conditions (or `all`, if true) according to `data`. Returns the number of rows updated. + """ + favorite_movie_updateMany( + """ + Filter condition to specify which rows to update. + """ + where: FavoriteMovie_Filter + + """ + Set to true to update all rows. + """ + all: Boolean = false + + """ + Data object containing fields to update. + """ + data: FavoriteMovie_Data! + ): Int! @fdc_generated(from: "FavoriteMovie", purpose: UPDATE_MULTIPLE) + """ + ✨ Update `Movie` objects matching `where` conditions (or `all`, if true) according to `data`. Returns the number of rows updated. + """ + movie_updateMany( + """ + Filter condition to specify which rows to update. + """ + where: Movie_Filter + + """ + Set to true to update all rows. + """ + all: Boolean = false + + """ + Data object containing fields to update. + """ + data: Movie_Data! + ): Int! @fdc_generated(from: "Movie", purpose: UPDATE_MULTIPLE) + """ + ✨ Update `MovieActor` objects matching `where` conditions (or `all`, if true) according to `data`. Returns the number of rows updated. + """ + movieActor_updateMany( + """ + Filter condition to specify which rows to update. + """ + where: MovieActor_Filter + + """ + Set to true to update all rows. + """ + all: Boolean = false + + """ + Data object containing fields to update. + """ + data: MovieActor_Data! + ): Int! @fdc_generated(from: "MovieActor", purpose: UPDATE_MULTIPLE) + """ + ✨ Update `MovieMetadata` objects matching `where` conditions (or `all`, if true) according to `data`. Returns the number of rows updated. + """ + movieMetadata_updateMany( + """ + Filter condition to specify which rows to update. + """ + where: MovieMetadata_Filter + + """ + Set to true to update all rows. + """ + all: Boolean = false + + """ + Data object containing fields to update. + """ + data: MovieMetadata_Data! + ): Int! @fdc_generated(from: "MovieMetadata", purpose: UPDATE_MULTIPLE) + """ + ✨ Update `Review` objects matching `where` conditions (or `all`, if true) according to `data`. Returns the number of rows updated. + """ + review_updateMany( + """ + Filter condition to specify which rows to update. + """ + where: Review_Filter + + """ + Set to true to update all rows. + """ + all: Boolean = false + + """ + Data object containing fields to update. + """ + data: Review_Data! + ): Int! @fdc_generated(from: "Review", purpose: UPDATE_MULTIPLE) + """ + ✨ Update `User` objects matching `where` conditions (or `all`, if true) according to `data`. Returns the number of rows updated. + """ + user_updateMany( + """ + Filter condition to specify which rows to update. + """ + where: User_Filter + + """ + Set to true to update all rows. + """ + all: Boolean = false + + """ + Data object containing fields to update. + """ + data: User_Data! + ): Int! @fdc_generated(from: "User", purpose: UPDATE_MULTIPLE) + """ + ✨ Delete a single `Actor` based on `id`, `key` or `first` and return its key (or `null` if not found). + """ + actor_delete( + """ + The unique ID of the object. + """ + id: UUID + + """ + The key used to identify the object. + """ + key: Actor_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: Actor_FirstRow + ): Actor_KeyOutput @fdc_generated(from: "Actor", purpose: DELETE_SINGLE) + """ + ✨ Delete a single `FavoriteMovie` based on `id`, `key` or `first` and return its key (or `null` if not found). + """ + favorite_movie_delete( + """ + The key used to identify the object. + """ + key: FavoriteMovie_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: FavoriteMovie_FirstRow + ): FavoriteMovie_KeyOutput @fdc_generated(from: "FavoriteMovie", purpose: DELETE_SINGLE) + """ + ✨ Delete a single `Movie` based on `id`, `key` or `first` and return its key (or `null` if not found). + """ + movie_delete( + """ + The unique ID of the object. + """ + id: UUID + + """ + The key used to identify the object. + """ + key: Movie_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: Movie_FirstRow + ): Movie_KeyOutput @fdc_generated(from: "Movie", purpose: DELETE_SINGLE) + """ + ✨ Delete a single `MovieActor` based on `id`, `key` or `first` and return its key (or `null` if not found). + """ + movieActor_delete( + """ + The key used to identify the object. + """ + key: MovieActor_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: MovieActor_FirstRow + ): MovieActor_KeyOutput @fdc_generated(from: "MovieActor", purpose: DELETE_SINGLE) + """ + ✨ Delete a single `MovieMetadata` based on `id`, `key` or `first` and return its key (or `null` if not found). + """ + movieMetadata_delete( + """ + The unique ID of the object. + """ + id: UUID + + """ + The key used to identify the object. + """ + key: MovieMetadata_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: MovieMetadata_FirstRow + ): MovieMetadata_KeyOutput @fdc_generated(from: "MovieMetadata", purpose: DELETE_SINGLE) + """ + ✨ Delete a single `Review` based on `id`, `key` or `first` and return its key (or `null` if not found). + """ + review_delete( + """ + The key used to identify the object. + """ + key: Review_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: Review_FirstRow + ): Review_KeyOutput @fdc_generated(from: "Review", purpose: DELETE_SINGLE) + """ + ✨ Delete a single `User` based on `id`, `key` or `first` and return its key (or `null` if not found). + """ + user_delete( + """ + The unique ID of the object. + """ + id: String + + """ + ✨ `_expr` server value variant of `id` (The unique ID of the object.) + """ + id_expr: String_Expr + + """ + The key used to identify the object. + """ + key: User_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: User_FirstRow + ): User_KeyOutput @fdc_generated(from: "User", purpose: DELETE_SINGLE) + """ + ✨ Delete `Actor` objects matching `where` conditions (or `all`, if true). Returns the number of rows deleted. + """ + actor_deleteMany( + """ + Filter condition to specify which rows to delete. + """ + where: Actor_Filter + + """ + Set to true to delete all rows. + """ + all: Boolean = false + ): Int! @fdc_generated(from: "Actor", purpose: DELETE_MULTIPLE) + """ + ✨ Delete `FavoriteMovie` objects matching `where` conditions (or `all`, if true). Returns the number of rows deleted. + """ + favorite_movie_deleteMany( + """ + Filter condition to specify which rows to delete. + """ + where: FavoriteMovie_Filter + + """ + Set to true to delete all rows. + """ + all: Boolean = false + ): Int! @fdc_generated(from: "FavoriteMovie", purpose: DELETE_MULTIPLE) + """ + ✨ Delete `Movie` objects matching `where` conditions (or `all`, if true). Returns the number of rows deleted. + """ + movie_deleteMany( + """ + Filter condition to specify which rows to delete. + """ + where: Movie_Filter + + """ + Set to true to delete all rows. + """ + all: Boolean = false + ): Int! @fdc_generated(from: "Movie", purpose: DELETE_MULTIPLE) + """ + ✨ Delete `MovieActor` objects matching `where` conditions (or `all`, if true). Returns the number of rows deleted. + """ + movieActor_deleteMany( + """ + Filter condition to specify which rows to delete. + """ + where: MovieActor_Filter + + """ + Set to true to delete all rows. + """ + all: Boolean = false + ): Int! @fdc_generated(from: "MovieActor", purpose: DELETE_MULTIPLE) + """ + ✨ Delete `MovieMetadata` objects matching `where` conditions (or `all`, if true). Returns the number of rows deleted. + """ + movieMetadata_deleteMany( + """ + Filter condition to specify which rows to delete. + """ + where: MovieMetadata_Filter + + """ + Set to true to delete all rows. + """ + all: Boolean = false + ): Int! @fdc_generated(from: "MovieMetadata", purpose: DELETE_MULTIPLE) + """ + ✨ Delete `Review` objects matching `where` conditions (or `all`, if true). Returns the number of rows deleted. + """ + review_deleteMany( + """ + Filter condition to specify which rows to delete. + """ + where: Review_Filter + + """ + Set to true to delete all rows. + """ + all: Boolean = false + ): Int! @fdc_generated(from: "Review", purpose: DELETE_MULTIPLE) + """ + ✨ Delete `User` objects matching `where` conditions (or `all`, if true). Returns the number of rows deleted. + """ + user_deleteMany( + """ + Filter condition to specify which rows to delete. + """ + where: User_Filter + + """ + Set to true to delete all rows. + """ + all: Boolean = false + ): Int! @fdc_generated(from: "User", purpose: DELETE_MULTIPLE) +} diff --git a/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/query.gql b/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/query.gql new file mode 100644 index 0000000..cbbc257 --- /dev/null +++ b/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/query.gql @@ -0,0 +1,293 @@ +extend type Query { + """ + ✨ Look up a single `Actor` based on `id`, `key` or `first` and return selected fields (or `null` if not found). + """ + actor( + """ + The unique ID of the object. + """ + id: UUID + + """ + The key used to identify the object. + """ + key: Actor_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: Actor_FirstRow + ): Actor @fdc_generated(from: "Actor", purpose: QUERY_SINGLE) + """ + ✨ Look up a single `FavoriteMovie` based on `id`, `key` or `first` and return selected fields (or `null` if not found). + """ + favorite_movie( + """ + The key used to identify the object. + """ + key: FavoriteMovie_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: FavoriteMovie_FirstRow + ): FavoriteMovie @fdc_generated(from: "FavoriteMovie", purpose: QUERY_SINGLE) + """ + ✨ Look up a single `Movie` based on `id`, `key` or `first` and return selected fields (or `null` if not found). + """ + movie( + """ + The unique ID of the object. + """ + id: UUID + + """ + The key used to identify the object. + """ + key: Movie_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: Movie_FirstRow + ): Movie @fdc_generated(from: "Movie", purpose: QUERY_SINGLE) + """ + ✨ Look up a single `MovieActor` based on `id`, `key` or `first` and return selected fields (or `null` if not found). + """ + movieActor( + """ + The key used to identify the object. + """ + key: MovieActor_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: MovieActor_FirstRow + ): MovieActor @fdc_generated(from: "MovieActor", purpose: QUERY_SINGLE) + """ + ✨ Look up a single `MovieMetadata` based on `id`, `key` or `first` and return selected fields (or `null` if not found). + """ + movieMetadata( + """ + The unique ID of the object. + """ + id: UUID + + """ + The key used to identify the object. + """ + key: MovieMetadata_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: MovieMetadata_FirstRow + ): MovieMetadata @fdc_generated(from: "MovieMetadata", purpose: QUERY_SINGLE) + """ + ✨ Look up a single `Review` based on `id`, `key` or `first` and return selected fields (or `null` if not found). + """ + review( + """ + The key used to identify the object. + """ + key: Review_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: Review_FirstRow + ): Review @fdc_generated(from: "Review", purpose: QUERY_SINGLE) + """ + ✨ Look up a single `User` based on `id`, `key` or `first` and return selected fields (or `null` if not found). + """ + user( + """ + The unique ID of the object. + """ + id: String + + """ + ✨ `_expr` server value variant of `id` (The unique ID of the object.) + """ + id_expr: String_Expr + + """ + The key used to identify the object. + """ + key: User_Key + + """ + Fetch the first row based on the filters and ordering. + """ + first: User_FirstRow + ): User @fdc_generated(from: "User", purpose: QUERY_SINGLE) + """ + ✨ List `Actor` objects in the table, optionally filtered by `where` conditions. + """ + actors( + """ + Filter condition to narrow down the query results. + """ + where: Actor_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [Actor_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [Actor!]! @fdc_generated(from: "Actor", purpose: QUERY_MULTIPLE) + """ + ✨ List `FavoriteMovie` objects in the table, optionally filtered by `where` conditions. + """ + favorite_movies( + """ + Filter condition to narrow down the query results. + """ + where: FavoriteMovie_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [FavoriteMovie_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [FavoriteMovie!]! @fdc_generated(from: "FavoriteMovie", purpose: QUERY_MULTIPLE) + """ + ✨ List `Movie` objects in the table, optionally filtered by `where` conditions. + """ + movies( + """ + Filter condition to narrow down the query results. + """ + where: Movie_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [Movie_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [Movie!]! @fdc_generated(from: "Movie", purpose: QUERY_MULTIPLE) + """ + ✨ List `MovieActor` objects in the table, optionally filtered by `where` conditions. + """ + movieActors( + """ + Filter condition to narrow down the query results. + """ + where: MovieActor_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [MovieActor_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [MovieActor!]! @fdc_generated(from: "MovieActor", purpose: QUERY_MULTIPLE) + """ + ✨ List `MovieMetadata` objects in the table, optionally filtered by `where` conditions. + """ + movieMetadatas( + """ + Filter condition to narrow down the query results. + """ + where: MovieMetadata_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [MovieMetadata_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [MovieMetadata!]! @fdc_generated(from: "MovieMetadata", purpose: QUERY_MULTIPLE) + """ + ✨ List `Review` objects in the table, optionally filtered by `where` conditions. + """ + reviews( + """ + Filter condition to narrow down the query results. + """ + where: Review_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [Review_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [Review!]! @fdc_generated(from: "Review", purpose: QUERY_MULTIPLE) + """ + ✨ List `User` objects in the table, optionally filtered by `where` conditions. + """ + users( + """ + Filter condition to narrow down the query results. + """ + where: User_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [User_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [User!]! @fdc_generated(from: "User", purpose: QUERY_MULTIPLE) +} diff --git a/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/relation.gql b/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/relation.gql new file mode 100644 index 0000000..5b1d779 --- /dev/null +++ b/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/main/relation.gql @@ -0,0 +1,318 @@ +extend type Actor { + """ + ✨ List `MovieActor` objects in a one-to-many relationship (where `MovieActor`.`actor` is this object). + """ + movieActors_on_actor( + """ + Filter condition to narrow down the query results. + """ + where: MovieActor_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [MovieActor_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [MovieActor!]! @fdc_generated(from: "MovieActor.actor", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + """ + ✨ List `Movie` objects using `MovieActor` as the join table (a `MovieActor` object exists where its `actor` is this and its `movie` is that). + """ + movies_via_MovieActor( + """ + Filter condition to narrow down the query results. + """ + where: MovieActor_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [MovieActor_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [Movie!]! @fdc_generated(from: "MovieActor", purpose: QUERY_MULTIPLE_MANY_TO_MANY) +} +extend type Movie { + """ + ✨ List `FavoriteMovie` objects in a one-to-many relationship (where `FavoriteMovie`.`movie` is this object). + """ + favorite_movies_on_movie( + """ + Filter condition to narrow down the query results. + """ + where: FavoriteMovie_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [FavoriteMovie_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [FavoriteMovie!]! @fdc_generated(from: "FavoriteMovie.movie", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + """ + ✨ List `MovieActor` objects in a one-to-many relationship (where `MovieActor`.`movie` is this object). + """ + movieActors_on_movie( + """ + Filter condition to narrow down the query results. + """ + where: MovieActor_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [MovieActor_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [MovieActor!]! @fdc_generated(from: "MovieActor.movie", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + """ + ✨ List `MovieMetadata` objects in a one-to-many relationship (where `MovieMetadata`.`movie` is this object). + """ + movieMetadatas_on_movie( + """ + Filter condition to narrow down the query results. + """ + where: MovieMetadata_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [MovieMetadata_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [MovieMetadata!]! @fdc_generated(from: "MovieMetadata.movie", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + """ + ✨ List `Review` objects in a one-to-many relationship (where `Review`.`movie` is this object). + """ + reviews_on_movie( + """ + Filter condition to narrow down the query results. + """ + where: Review_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [Review_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [Review!]! @fdc_generated(from: "Review.movie", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + """ + ✨ List `Actor` objects using `MovieActor` as the join table (a `MovieActor` object exists where its `movie` is this and its `actor` is that). + """ + actors_via_MovieActor( + """ + Filter condition to narrow down the query results. + """ + where: MovieActor_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [MovieActor_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [Actor!]! @fdc_generated(from: "MovieActor", purpose: QUERY_MULTIPLE_MANY_TO_MANY) + """ + ✨ List `User` objects using `FavoriteMovie` as the join table (a `FavoriteMovie` object exists where its `movie` is this and its `user` is that). + """ + users_via_FavoriteMovie( + """ + Filter condition to narrow down the query results. + """ + where: FavoriteMovie_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [FavoriteMovie_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [User!]! @fdc_generated(from: "FavoriteMovie", purpose: QUERY_MULTIPLE_MANY_TO_MANY) + """ + ✨ List `User` objects using `Review` as the join table (a `Review` object exists where its `movie` is this and its `user` is that). + """ + users_via_Review( + """ + Filter condition to narrow down the query results. + """ + where: Review_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [Review_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [User!]! @fdc_generated(from: "Review", purpose: QUERY_MULTIPLE_MANY_TO_MANY) +} +extend type User { + """ + ✨ List `FavoriteMovie` objects in a one-to-many relationship (where `FavoriteMovie`.`user` is this object). + """ + favorite_movies_on_user( + """ + Filter condition to narrow down the query results. + """ + where: FavoriteMovie_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [FavoriteMovie_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [FavoriteMovie!]! @fdc_generated(from: "FavoriteMovie.user", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + """ + ✨ List `Review` objects in a one-to-many relationship (where `Review`.`user` is this object). + """ + reviews_on_user( + """ + Filter condition to narrow down the query results. + """ + where: Review_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [Review_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [Review!]! @fdc_generated(from: "Review.user", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + """ + ✨ List `Movie` objects using `FavoriteMovie` as the join table (a `FavoriteMovie` object exists where its `user` is this and its `movie` is that). + """ + movies_via_FavoriteMovie( + """ + Filter condition to narrow down the query results. + """ + where: FavoriteMovie_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [FavoriteMovie_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [Movie!]! @fdc_generated(from: "FavoriteMovie", purpose: QUERY_MULTIPLE_MANY_TO_MANY) + """ + ✨ List `Movie` objects using `Review` as the join table (a `Review` object exists where its `user` is this and its `movie` is that). + """ + movies_via_Review( + """ + Filter condition to narrow down the query results. + """ + where: Review_Filter + + """ + Order the query results by specific fields. + """ + orderBy: [Review_Order!] + + """ + Number of rows to skip before starting to return the results. + """ + offset: Int + + """ + Maximum number of rows to return (defaults to 100 rows). + """ + limit: Int = 100 + ): [Movie!]! @fdc_generated(from: "Review", purpose: QUERY_MULTIPLE_MANY_TO_MANY) +} diff --git a/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/prelude.gql b/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/prelude.gql new file mode 100644 index 0000000..d6124ac --- /dev/null +++ b/Examples/FriendlyFlix/dataconnect/.dataconnect/schema/prelude.gql @@ -0,0 +1,1890 @@ +"AccessLevel specifies coarse access policies for common situations." +enum AccessLevel { + """ + This operation is accessible to anyone, with or without authentication. + Equivalent to: `@auth(expr: "true")` + """ + PUBLIC + + """ + This operation can be executed only with a valid Firebase Auth ID token. + **Note:** This access level allows anonymous and unverified accounts, + which may present security and abuse risks. + Equivalent to: `@auth(expr: "auth.uid != nil")` + """ + USER_ANON + + """ + This operation is restricted to non-anonymous Firebase Auth accounts. + Equivalent to: `@auth(expr: "auth.uid != nil && auth.token.firebase.sign_in_provider != 'anonymous'")` + """ + USER + + """ + This operation is restricted to Firebase Auth accounts with verified email addresses. + Equivalent to: `@auth(expr: "auth.uid != nil && auth.token.email_verified")` + """ + USER_EMAIL_VERIFIED + + """ + This operation cannot be executed by anyone. The operation can only be performed + by using the Admin SDK from a privileged environment. + Equivalent to: `@auth(expr: "false")` + """ + NO_ACCESS +} + +""" +The `@auth` directive defines the authentication policy for a query or mutation. + +It must be added to any operation that you wish to be accessible from a client +application. If not specified, the operation defaults to `@auth(level: NO_ACCESS)`. + +Refer to [Data Connect Auth Guide](https://firebase.google.com/docs/data-connect/authorization-and-security) for the best practices. +""" +directive @auth( + """ + The minimal level of access required to perform this operation. + Exactly one of `level` and `expr` should be specified. + """ + level: AccessLevel @fdc_oneOf(required: true) + """ + A CEL expression that grants access to this operation if the expression + evaluates to `true`. + Exactly one of `level` and `expr` should be specified. + """ + expr: Boolean_Expr @fdc_oneOf(required: true) +) on QUERY | MUTATION + + +""" +Require that this mutation always run in a DB transaction. + +Mutations with `@transaction` are guaranteed to either fully succeed or fully +fail. If any of the fields within the transaction fails, the entire transaction +is rolled back. From a client standpoint, any failure behaves as if the entire +request had failed with a request error and execution had not begun. + +Mutations without `@transaction` would execute each root field one after +another in sequence. It surfaces any errors as partial [field errors](https://spec.graphql.org/October2021/#sec-Errors.Field-errors), +but not impacts the subsequent executions. + +The `@transaction` directive cannot be added to queries for now. +Currently, queries cannot fail partially, the response data is not guaranteed +to be a consistent snapshot. +""" +directive @transaction on MUTATION + +"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 +} + +""" +The pattern match condition on a string. Specify either like or regex. +https://www.postgresql.org/docs/current/functions-matching.html +""" +input String_Pattern { + "Match using the provided `LIKE` expression." + like: String + "Match using the provided POSIX regular expression." + regex: String + "When true, ignore case when matching." + ignoreCase: Boolean + "When true, invert the match result. Equivalent to `NOT LIKE` or `!~`." + invert: Boolean +} + +"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!] +} + +"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!] +} + +"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!] +} + +"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 +} + +"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!] +} + +"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 +} + +"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!] +} + +"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 +} + +"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!] +} + +"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!] +} + +"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!] +} + +"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!] +} + +"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!] +} + +""" +(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 + +type _Service { + "Full Service Definition Language of the Frebase Data Connect Schema, including normalized schema, predefined and generated types." + sdl( + """ + Whether or not to omit Data Connect builtin GraphQL preludes. + They are static GraphQL publically available in the docsite. + """ + omitBuiltin: Boolean = false + """ + Whether or not to omit GQL description in the SDL. + We generate description to document generated schema. + It may bloat the size of SDL. + """ + omitDescription: Boolean = false + ): String! + "Orignal Schema Sources in the service." + schema: String! + "Generated documentation from the schema of the Firebase Data Connect Service." + docs: [_Doc!]! +} + +type _Doc { + "Name of the Doc Page." + page: String! + "The markdown content of the doc page." + markdown: 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 + +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") +} + +""" +`UUID` is a string of hexadecimal digits representing an RFC4122-compliant UUID. + +UUIDs are always output as 32 lowercase hexadecimal digits without 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}` + +In the PostgreSQL table, it's stored as [`uuid`](https://www.postgresql.org/docs/current/datatype-uuid.html). +""" +scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") + +""" +`Int64` is a scalar that represents a 64-bit signed integer. + +In the PostgreSQL table, it's stored as [`bigint`](https://www.postgresql.org/docs/current/datatype-numeric.html). + +On the wire, it's encoded as string because 64-bit integer exceeds the range of JSON number. +""" +scalar Int64 + +""" +The `Any` scalar represents any valid [JSON value](https://www.json.org/json-en.html). +It can be an object, array, string, number, or boolean. + +Caution: JSON doesn't distinguish Int and Float. + +In the PostgreSQL table, it's stored as [`jsonb`](https://www.postgresql.org/docs/current/datatype-json.html). +""" +scalar Any @specifiedBy(url: "https://www.json.org/json-en.html") + +""" +The `Void` scalar type represents the absence of any value. It is typically used +in operations where no value is expected in return. +""" +scalar Void + +""" +The `True` scalar type only accepts the boolean value `true`. + +An optional field/argument typed as `True` may either be set +to `true` or omitted (not provided at all). The values `false` or `null` are not +accepted. +""" +scalar True + @fdc_forbiddenAsFieldType + @fdc_forbiddenAsVariableType + @fdc_example(value: true, description: "The only allowed value.") + +""" +A Common Expression Language (CEL) expression that returns a boolean at runtime. + +This expression can reference the `auth` variable, which is null when Firebase +Auth is not used. When Firebase Auth is used, the following fields are available: + + - `auth.uid`: The current user ID. + - `auth.token`: A map containing all token fields (e.g., 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**: Currently, only a limited set of expressions are supported. +""" +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**: Currently, only a limited set of expressions are supported. +""" +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 currently supported for each +type. +""" +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. + +In this example, we defined one table with a field named `myField`. + +```graphql +type TableName @table { + myField: String +} +``` +Data Connect adds an implicit `id` primary key column. So the above schema is equivalent to: + +```graphql +type TableName @table(key: "id") { + id: String @default(expr: "uuidV4()") + myField: String +} +``` + +Data Connect generates the following SQL table and CRUD operations to use it. + +```sql +CREATE TABLE "public"."table_name" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "my_field" text NULL, + PRIMARY KEY ("id") +) +``` + + * You can lookup a row: `query ($id: UUID!) { tableName(id: $id) { myField } } ` + * You can find rows using: `query tableNames(limit: 20) { myField }` + * You can insert a row: `mutation { tableName_insert(data: {myField: "foo"}) }` + * You can update a row: `mutation ($id: UUID!) { tableName_update(id: $id, data: {myField: "bar"}) }` + * You can delete a row: `mutation ($id: UUID!) { tableName_delete(id: $id) }` + +##### Customizations + +- `@table(singular)` and `@table(plural)` can customize the singular and plural name. +- `@table(name)` can customize the Postgres table name. +- `@table(key)` can customize the primary key field name and type. + +For example, the `User` table often has a `uid` as its primary key. + +```graphql +type User @table(key: "uid") { + uid: String! + name: String +} +``` + + * You can securely lookup a row: `query { user(key: {uid_expr: "auth.uid"}) { name } } ` + * You can securely insert a row: `mutation { user_insert(data: {uid_expr: "auth.uid" name: "Fred"}) }` + * You can securely update a row: `mutation { user_update(key: {uid_expr: "auth.uid"}, data: {name: "New Name"}) }` + * You can securely delete a row: `mutation { user_delete(key: {uid_expr: "auth.uid"}) }` + +`@table` type can be configured further with: + + - Custom SQL data types for columns. See `@col`. + - Add SQL indexes. See `@index`. + - Add SQL unique constraints. See `@unique`. + - Add foreign key constraints to define relations. See `@ref`. + +""" +directive @table( + """ + Configures the SQL database table name. Defaults to snake_case like `table_name`. + """ + name: String + """ + Configures the singular name. Defaults to the camelCase like `tableName`. + """ + singular: String + """ + Configures the plural name. Defaults to infer based on English plural pattern like `tableNames`. + """ + plural: String + """ + Defines the primary key of the table. Defaults to a single field named `id`. + If not present already, Data Connect adds an implicit field `id: UUID! @default(expr: "uuidV4()")`. + """ + key: [String!] +) on OBJECT + +""" +Defines a relational database Raw SQLview. + +Data Connect generates GraphQL queries with WHERE and ORDER BY clauses. +However, not all SQL features has native GraphQL equivalent. + +You can write **an arbitrary SQL SELECT statement**. Data Connect +would map Graphql fields on `@view` type to columns in your SELECT statement. + +* Scalar GQL fields (camelCase) should match a SQL column (snake_case) + in the SQL SELECT statement. +* Reference GQL field can point to another `@table` type. Similar to foreign key + defined with `@ref` on a `@table` type, a `@view` type establishes a relation + when `@ref(fields)` match `@ref(references)` on the target table. + +In this example, you can use `@view(sql)` to define an aggregation view on existing +table. + +```graphql +type User @table { + name: String + score: Int +} +type UserAggregation @view(sql: ''' + SELECT + COUNT(*) as count, + SUM(score) as sum, + AVG(score) as average, + PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY score) AS median, + (SELECT id FROM "user" LIMIT 1) as example_id + FROM "user" +''') { + count: Int + sum: Int + average: Float + median: Float + example: User + exampleId: UUID +} +``` + +###### Example: Query Raw SQL View + +```graphql +query { + userAggregations { + count sum average median + exampleId example { id } + } +} +``` + +##### One-to-One View + +An one-to-one companion `@view` can be handy if you want to argument a `@table` +with additional implied content. + +```graphql +type Restaurant @table { + name: String! +} +type Review @table { + restaurant: Restaurant! + rating: Int! +} +type RestaurantStats @view(sql: ''' + SELECT + restaurant_id, + COUNT(*) AS review_count, + AVG(rating) AS average_rating + FROM review + GROUP BY restaurant_id +''') { + restaurant: Restaurant @unique + reviewCount: Int + averageRating: Float +} +``` + +In this example, `@unique` convey the assumption that each `Restaurant` should +have only one `RestaurantStats` object. + +###### Example: Query One-to-One View + +```graphql +query ListRestaurants { + restaurants { + name + stats: restaurantStats_on_restaurant { + reviewCount + averageRating + } + } +} +``` + +###### Example: Filter based on One-to-One View + +```graphql +query BestRestaurants($minAvgRating: Float, $minReviewCount: Int) { + restaurants(where: { + restaurantStats_on_restaurant: { + averageRating: {ge: $minAvgRating} + reviewCount: {ge: $minReviewCount} + } + }) { name } +} +``` + +##### Customizations + +- One of `@view(sql)` or `@view(name)` should be defined. + `@view(name)` can refer to a persisted SQL view in the Postgres schema. +- `@view(singular)` and `@view(plural)` can customize the singular and plural name. + +`@view` type can be configured further: + + - `@unique` lets you define one-to-one relation. + - `@col` lets you customize SQL column mapping. For example, `@col(name: "column_in_select")`. + +##### Limitations + +Raw SQL view doesn't have a primary key, so it doesn't support lookup. Other +`@table` or `@view` cannot have `@ref` to a view either. + +View cannot be mutated. You can perform CRUD operations on the underlying +table to alter its content. + +**Important: Data Connect doesn't parse and validate SQL** + +- If the SQL view is invalid or undefined, related requests may fail. +- If the SQL view return incompatible types. Firebase Data Connect may surface + errors. +- If a field doesn't have a corresponding column in the SQL SELECT statement, + it will always be `null`. +- There is no way to ensure VIEW to TABLE `@ref` constraint. +- All fields must be nullable in case they aren't found in the SELECT statement + or in the referenced table. + +**Important: You should always test `@view`!** + +""" +directive @view( + """ + The SQL view name. If neither `name` nor `sql` are provided, defaults to the + snake_case of the singular type name. + `name` and `sql` cannot be specified at the same time. + """ + name: String @fdc_oneOf + """ + SQL `SELECT` statement used as the basis for this type. + SQL SELECT columns should use snake_case. GraphQL fields should use camelCase. + `name` and `sql` cannot be specified at the same time. + """ + sql: String @fdc_oneOf + """ + Configures the singular name. Defaults to the camelCase like `viewName`. + """ + singular: String + """ + Configures the plural name. Defaults to infer based on English plural pattern like `viewNames`. + """ + plural: String +) on OBJECT + +""" +Customizes a field that represents a SQL database table column. + +Data Connect maps scalar Fields on `@table` type to a SQL column of +corresponding data type. + +- scalar `UUID` maps to [`uuid`](https://www.postgresql.org/docs/current/datatype-uuid.html). +- scalar `String` maps to [`text`](https://www.postgresql.org/docs/current/datatype-character.html). +- scalar `Int` maps to [`int`](https://www.postgresql.org/docs/current/datatype-numeric.html). +- scalar `Int64` maps to [`bigint`](https://www.postgresql.org/docs/current/datatype-numeric.html). +- scalar `Float` maps to [`double precision`](https://www.postgresql.org/docs/current/datatype-numeric.html). +- scalar `Boolean` maps to [`boolean`](https://www.postgresql.org/docs/current/datatype-boolean.html). +- scalar `Date` maps to [`date`](https://www.postgresql.org/docs/current/datatype-datetime.html). +- scalar `Timestamp` maps to [`timestamptz`](https://www.postgresql.org/docs/current/datatype-datetime.html). +- scalar `Any` maps to [`jsonb`](https://www.postgresql.org/docs/current/datatype-json.html). +- scalar `Vector` maps to [`pgvector`](https://github.com/pgvector/pgvector). + +Array scalar fields are mapped to [Postgres arrays](https://www.postgresql.org/docs/current/arrays.html). + +###### Example: Serial Primary Key + +For example, you can define auto-increment primary key. + +```graphql +type Post @table { + id: Int! @col(name: "post_id", dataType: "serial") +} +``` + +Data Connect converts it to the following SQL table schema. + +```sql +CREATE TABLE "public"."post" ( + "post_id" serial NOT NULL, + PRIMARY KEY ("id") +) +``` + +###### Example: Vector + +```graphql +type Post @table { + content: String! @col(name: "post_content") + contentEmbedding: Vector! @col(size:768) +} +``` + +""" +directive @col( + """ + The SQL database column name. Defaults to snake_case of the field name. + """ + name: String + """ + Configures the custom SQL data type. + + Each GraphQL type can map to multiple SQL data types. + Refer to [Postgres supported data types](https://www.postgresql.org/docs/current/datatype.html). + + Incompatible SQL data type will lead to undefiend barehavior. + """ + dataType: String + """ + Required on `Vector` columns. It specifies the length of the Vector. + `textembedding-gecko@003` model generates `Vector` of `@col(size:768)`. + """ + size: Int +) on FIELD_DEFINITION + + +""" +Defines a foreign key reference to another table. + +For example, we can define a many-to-one relation. + +```graphql +type ManyTable @table { + refField: OneTable! +} +type OneTable @table { + someField: String! +} +``` +Data Connect adds implicit foreign key column and relation query field. So the +above schema is equivalent to the following schema. + +```graphql +type ManyTable @table { + id: UUID! @default(expr: "uuidV4()") + refField: OneTable! @ref(fields: "refFieldId", references: "id") + refFieldId: UUID! +} +type OneTable @table { + id: UUID! @default(expr: "uuidV4()") + someField: UUID! + # Generated Fields: + # manyTables_on_refField: [ManyTable!]! +} +``` +Data Connect generates the necessary foreign key constraint. + +```graphql +CREATE TABLE "public"."many_table" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "ref_field_id" uuid NOT NULL, + PRIMARY KEY ("id"), + CONSTRAINT "many_table_ref_field_id_fkey" FOREIGN KEY ("ref_field_id") REFERENCES "public"."one_table" ("id") ON DELETE CASCADE +) +``` + +###### Example: Traverse the Reference Field + +```graphql +query ($id: UUID!) { + manyTable(id: $id) { + refField { id } + } +} +``` + +###### Example: Reverse Traverse the Reference field + +```graphql +query ($id: UUID!) { + oneTable(id: $id) { + manyTables_on_refField { id } + } +} +``` + +##### Optional Many-to-One Relation + +An optional foreign key reference will be set to null if the referenced row is deleted. + +In this example, if a `User` is deleted, the `assignee` and `reporter` +references will be set to null. + +```graphql +type Bug @table { + title: String! + assignee: User + reproter: User +} + +type User @table { name: String! } +``` + +##### Required Many-to-One Relation + +A required foreign key reference will cascade delete if the referenced row is +deleted. + +In this example, if a `Post` is deleted, associated comments will also be +deleted. + +```graphql +type Comment @table { + post: Post! + content: String! +} + +type Post @table { title: String! } +``` + +##### Many To Many Relation + +You can define a many-to-many relation with a join table. + +```graphql +type Membership @table(key: ["group", "user"]) { + group: Group! + user: User! + role: String! @default(value: "member") +} + +type Group @table { name: String! } +type User @table { name: String! } +``` + +When Data Connect sees a table with two reference field as its primary key, it +knows this is a join table, so expands the many-to-many query field. + +```graphql +type Group @table { + name: String! + # Generated Fields: + # users_via_Membership: [User!]! + # memberships_on_group: [Membership!]! +} +type User @table { + name: String! + # Generated Fields: + # groups_via_Membership: [Group!]! + # memberships_on_user: [Membership!]! +} +``` + +###### Example: Traverse the Many-To-Many Relation + +```graphql +query ($id: UUID!) { + group(id: $id) { + users: users_via_Membership { + name + } + } +} +``` + +###### Example: Traverse to the Join Table + +```graphql +query ($id: UUID!) { + group(id: $id) { + memberships: memberships_on_group { + user { name } + role + } + } +} +``` + +##### One To One Relation + +You can even define a one-to-one relation with the help of `@unique` or `@table(key)`. + +```graphql +type User @table { + name: String +} +type Account @table { + user: User! @unique +} +# Alternatively, use primary key constraint. +# type Account @table(key: "user") { +# user: User! +# } +``` + +###### Example: Transerse the Reference Field + +```graphql +query ($id: UUID!) { + account(id: $id) { + user { id } + } +} +``` + +###### Example: Reverse Traverse the Reference field + +```graphql +query ($id: UUID!) { + user(id: $id) { + account_on_user { id } + } +} +``` + +##### Customizations + +- `@ref(constraintName)` can customize the SQL foreign key constraint name (`table_name_ref_field_fkey` above). +- `@ref(fields)` can customize the foreign key field names. +- `@ref(references)` can customize the constraint to reference other columns. + By default, `@ref(references)` is the primary key of the `@ref` table. + Other fields with `@unique` may also be referred in the foreign key constraint. + +""" +directive @ref( + "The SQL database foreign key constraint name. Defaults to snake_case `{table_name}_{field_name}_fkey`." + constraintName: String + """ + Foreign key fields. Defaults to `{tableName}{PrimaryIdName}`. + """ + fields: [String!] + "The fields that the foreign key references in the other table. Defaults to its primary key." + references: [String!] +) on FIELD_DEFINITION + +"Defines the orderBy direction in a query." +enum OrderDirection { +"Results are ordered in ascending order." + ASC +"Results are ordered in descending order." + DESC +} + +""" +Specifies the default value for a column field. + +For example: + +```graphql +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + number: Int! @col(dataType: "serial") + createdAt: Date! @default(expr: "request.time") + role: String! @default(value: "Member") + credit: Int! @default(value: 100) +} +``` + +The supported arguments vary based on the field type. +""" +directive @default( + "A constant value validated against the field's GraphQL type during compilation." + value: Any @fdc_oneOf(required: true) + "A CEL expression whose return value must match the field's 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. + See [PostgreSQL defaults](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-DEFAULT) + for more details. + """ + sql: Any_SQL @fdc_oneOf(required: true) +) on FIELD_DEFINITION + +""" +Defines a database index to optimize query performance. + +```graphql +type User @table @index(fields: ["name", "phoneNumber"], order: [ASC, DESC]) { + name: String @index + phoneNumber: Int64 @index + tags: [String] @index # GIN Index +} +``` + +##### Single Field Index + +You can put `@index` on a `@col` field to create a SQL index. + +`@index(order)` matters little for single field indexes, as they can be scanned +in both directions. + +##### Composite Index + +You can put `@index(fields: [...])` on `@table` type to define composite indexes. + +`@index(order: [...])` can customize the index order to satisfy particular +filter and order requirement. + +""" +directive @index( + """ + Configure the SQL database index id. + + If not overridden, Data Connect generates the index name: + - `{table_name}_{first_field}_{second_field}_aa_idx` + - `{table_name}_{field_name}_idx` + """ + name: String + """ + Only allowed and required when used on a `@table` type. + Specifies the fields to create the index on. + """ + fields: [String!] + """ + Only allowed for `BTREE` `@index` on `@table` type. + Specifies the order for each indexed column. Defaults to all `ASC`. + """ + order: [IndexFieldOrder!] + """ + Customize the index type. + + For most index, it defaults to `BTREE`. + For array fields, only allowed `IndexType` is `GIN`. + For `Vector` fields, defaults to `HNSW`, may configure to `IVFFLAT`. + """ + type: IndexType + """ + Only allowed when used on vector field. + Defines the vector similarity method. Defaults to `INNER_PRODUCT`. + """ + vector_method: VectorSimilarityMethod +) repeatable on FIELD_DEFINITION | OBJECT + +"Specifies the sorting order for database indexes." +enum IndexFieldOrder { + "Sorts the field in ascending order (from lowest to highest)." + ASC + "Sorts the field in descending order (from highest to lowest)." + DESC +} + +"Defines the type of index to be used in the database." +enum IndexType { + "A general-purpose index type commonly used for sorting and searching." + BTREE + "Generalized Inverted Index, optimized for indexing composite values such as arrays." + GIN + "Hierarchical Navigable Small World graph, used for nearest-neighbor searches on vector fields." + HNSW + "Inverted File Index, optimized for approximate nearest-neighbor searches in vector databases." + IVFFLAT +} + +""" +Defines unique constraints on `@table`. + +For example, + +```graphql +type User @table { + phoneNumber: Int64 @unique +} +type UserProfile @table { + user: User! @unique + address: String @unique +} +``` + +- `@unique` on a `@col` field adds a single-column unique constraint. +- `@unique` on a `@table` type adds a composite unique constraint. +- `@unique` on a `@ref` defines a one-to-one relation. It adds unique constraint + on `@ref(fields)`. + +`@unique` ensures those fields can uniquely identify a row, so other `@table` +type may define `@ref(references)` to refer to fields that has a unique constraint. + +""" +directive @unique( + """ + Configures the SQL database unique constraint name. + + If not overridden, Data Connect generates the unique constraint name: + - `table_name_first_field_second_field_uidx` + - `table_name_only_field_name_uidx` + """ + indexName: String + """ + Only allowed and required when used on OBJECT, + this specifies the fields to create a unique constraint on. + """ + fields: [String!] +) repeatable on FIELD_DEFINITION | OBJECT + +""" +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. + +In the PostgreSQL table, it's stored as [`date`](https://www.postgresql.org/docs/current/datatype-datetime.html). +""" +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. + +In the PostgreSQL table, it's stored as [`timestamptz`](https://www.postgresql.org/docs/current/datatype-datetime.html). +""" +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. +""" +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. +""" +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 { + "Match if the field `IS NULL`." + isNull: Boolean + "Match if the field is exactly equal to the provided value." + eq: Date @fdc_oneOf(group: "eq") + "Match if the field equals the provided CEL expression." + eq_expr: Date_Expr @fdc_oneOf(group: "eq") + "Match if the field equals the provided relative date." + eq_date: Date_Relative @fdc_oneOf(group: "eq") + "Match if the field is not equal to the provided value." + ne: Date @fdc_oneOf(group: "ne") + "Match if the field is not equal to the provided CEL expression." + ne_expr: Date_Expr @fdc_oneOf(group: "ne") + "Match if the field is not equal to the provided relative date." + ne_date: Date_Relative @fdc_oneOf(group: "ne") + "Match if the field value is among the provided list of values." + in: [Date!] + "Match if the field value is not among the provided list of values." + nin: [Date!] + "Match if the field value is greater than the provided value." + gt: Date @fdc_oneOf(group: "gt") + "Match if the field value is greater than the provided CEL expression." + gt_expr: Date_Expr @fdc_oneOf(group: "gt") + "Match if the field value is greater than the provided relative date." + gt_date: Date_Relative @fdc_oneOf(group: "gt") + "Match if the field value is greater than or equal to the provided value." + ge: Date @fdc_oneOf(group: "ge") + "Match if the field value is greater than or equal to the provided CEL expression." + ge_expr: Date_Expr @fdc_oneOf(group: "ge") + "Match if the field value is greater than or equal to the provided relative date." + ge_date: Date_Relative @fdc_oneOf(group: "ge") + "Match if the field value is less than the provided value." + lt: Date @fdc_oneOf(group: "lt") + "Match if the field value is less than the provided CEL expression." + lt_expr: Date_Expr @fdc_oneOf(group: "lt") + "Match if the field value is less than the provided relative date." + lt_date: Date_Relative @fdc_oneOf(group: "lt") + "Match if the field value is less than or equal to the provided value." + le: Date @fdc_oneOf(group: "le") + "Match if the field value is less than or equal to the provided CEL expression." + le_expr: Date_Expr @fdc_oneOf(group: "le") + "Match if the field value is less than or equal to the provided relative date." + le_date: Date_Relative @fdc_oneOf(group: "le") +} + +"Conditions on a`Date` list." +input Date_ListFilter { + "Match if the list contains the provided date." + includes: Date @fdc_oneOf(group: "includes") + "Match if the list contains the provided date CEL expression." + includes_expr: Date_Expr @fdc_oneOf(group: "includes") + "Match if the list contains the provided relative date." + includes_date: Date_Relative @fdc_oneOf(group: "includes") + "Match if the list does not contain the provided date." + excludes: Date @fdc_oneOf(group: "excludes") + "Match if the list does not contain the provided date CEL expression." + excludes_expr: Date_Expr @fdc_oneOf(group: "excludes") + "Match if the list does not contain the provided relative date." + excludes_date: Date_Relative @fdc_oneOf(group: "excludes") + "Match if the list contains all the provided dates." + includesAll: [Date!] + "Match if the list contains none of the provided dates." + excludesAll: [Date!] +} + +"Conditions on a `Timestamp` value." +input Timestamp_Filter { + "Match if the field `IS NULL`." + isNull: Boolean + "Match if the field is exactly equal to the provided value." + eq: Timestamp @fdc_oneOf(group: "eq") + "Match if the field equals the provided CEL expression." + eq_expr: Timestamp_Expr @fdc_oneOf(group: "eq") + "Match if the field equals the provided relative time." + eq_time: Timestamp_Relative @fdc_oneOf(group: "eq") + "Match if the field is not equal to the provided value." + ne: Timestamp @fdc_oneOf(group: "ne") + "Match if the field is not equal to the provided CEL expression." + ne_expr: Timestamp_Expr @fdc_oneOf(group: "ne") + "Match if the field is not equal to the provided relative time." + ne_time: Timestamp_Relative @fdc_oneOf(group: "ne") + "Match if the field value is among the provided list of values." + in: [Timestamp!] + "Match if the field value is not among the provided list of values." + nin: [Timestamp!] + "Match if the field value is greater than the provided value." + gt: Timestamp @fdc_oneOf(group: "gt") + "Match if the field value is greater than the provided CEL expression." + gt_expr: Timestamp_Expr @fdc_oneOf(group: "gt") + "Match if the field value is greater than the provided relative time." + gt_time: Timestamp_Relative @fdc_oneOf(group: "gt") + "Match if the field value is greater than or equal to the provided value." + ge: Timestamp @fdc_oneOf(group: "ge") + "Match if the field value is greater than or equal to the provided CEL expression." + ge_expr: Timestamp_Expr @fdc_oneOf(group: "ge") + "Match if the field value is greater than or equal to the provided relative time." + ge_time: Timestamp_Relative @fdc_oneOf(group: "ge") + "Match if the field value is less than the provided value." + lt: Timestamp @fdc_oneOf(group: "lt") + "Match if the field value is less than the provided CEL expression." + lt_expr: Timestamp_Expr @fdc_oneOf(group: "lt") + "Match if the field value is less than the provided relative time." + lt_time: Timestamp_Relative @fdc_oneOf(group: "lt") + "Match if the field value is less than or equal to the provided value." + le: Timestamp @fdc_oneOf(group: "le") + "Match if the field value is less than or equal to the provided CEL expression." + le_expr: Timestamp_Expr @fdc_oneOf(group: "le") + "Match if the field value is less than or equal to the provided relative time." + le_time: Timestamp_Relative @fdc_oneOf(group: "le") +} + +"Conditions on a `Timestamp` list." +input Timestamp_ListFilter { + "Match if the list contains the provided timestamp." + includes: Timestamp @fdc_oneOf(group: "includes") + "Match if the list contains the provided timestamp CEL expression." + includes_expr: Timestamp_Expr @fdc_oneOf(group: "includes") + "Match if the list contains the provided relative timestamp." + includes_time: Timestamp_Relative @fdc_oneOf(group: "includes") + "Match if the list does not contain the provided timestamp." + excludes: Timestamp @fdc_oneOf(group: "excludes") + "Match if the list does not contain the provided timestamp CEL expression." + excludes_expr: Timestamp_Expr @fdc_oneOf(group: "excludes") + "Match if the list does not contain the provided relative timestamp." + excludes_time: Timestamp_Relative @fdc_oneOf(group: "excludes") + "Match if the list contains all the provided timestamps." + includesAll: [Timestamp!] + "Match if the list contains none of the provided timestamps." + excludesAll: [Timestamp!] +} + +"Update input of a `Date` value." +input Date_Update { + "Set the field to the provided date." + set: Date @fdc_oneOf(group: "set") + "Set the field to the provided date CEL expression." + set_expr: Date_Expr @fdc_oneOf(group: "set") + "Set the field to the provided relative date." + set_date: Date_Relative @fdc_oneOf(group: "set") +} + +"Update input of a `Date` list value." +input Date_ListUpdate { + "Replace the current list with the provided list of `Date` values." + set: [Date!] + "Append the provided `Date` values to the existing list." + append: [Date!] + "Prepend the provided `Date` values to the existing list." + prepend: [Date!] + "Remove the date value at the specified index." + delete: Int + "The index of the list to perform updates." + i: Int + "Update the date value at the specified index." + update: Date +} + +"Update input of a `Timestamp` value." +input Timestamp_Update { + "Set the field to the provided timestamp." + set: Timestamp @fdc_oneOf(group: "set") + "Set the field to the provided timestamp CEL expression." + set_expr: Timestamp_Expr @fdc_oneOf(group: "set") + "Set the field to the provided relative timestamp." + set_time: Timestamp_Relative @fdc_oneOf(group: "set") +} + +"Update input of an `Timestamp` list value." +input Timestamp_ListUpdate { + "Replace the current list with the provided list of `Timestamp` values." + set: [Timestamp!] + "Append the provided `Timestamp` values to the existing list." + append: [Timestamp!] + "Prepend the provided `Timestamp` values to the existing list." + prepend: [Timestamp!] + "Remove the timestamp value at the specified index." + delete: Int + "The index of the list to perform updates." + i: Int + "Update the timestamp value at the specified index." + update: Timestamp +} + + +"A runtime-calculated `Timestamp` value relative to `now` or `at`." +input Timestamp_Relative @fdc_forbiddenAsVariableType @fdc_forbiddenAsFieldType { + "Match for the current time." + now: True @fdc_oneOf(group: "from", required: true) + "A specific timestamp for matching." + at: Timestamp @fdc_oneOf(group: "from", required: true) + "Add the provided duration to the base timestamp." + add: Timestamp_Duration + "Subtract the provided duration from the base timestamp." + sub: Timestamp_Duration + "Truncate the timestamp to the provided interval." + truncateTo: Timestamp_Interval +} + +input Timestamp_Duration @fdc_forbiddenAsVariableType @fdc_forbiddenAsFieldType { + "The number of milliseconds for the duration." + milliseconds: Int! = 0 + "The number of seconds for the duration." + seconds: Int! = 0 + "The number of minutes for the duration." + minutes: Int! = 0 + "The number of hours for the duration." + hours: Int! = 0 + "The number of days for the duration." + days: Int! = 0 + "The number of weeks for the duration." + weeks: Int! = 0 + "The number of months for the duration." + months: Int! = 0 + "The number of years for the duration." + years: Int! = 0 +} + +enum Timestamp_Interval @fdc_forbiddenAsFieldType { + "Represents a time interval of one second." + SECOND + "Represents a time interval of one minute." + MINUTE + "Represents a time interval of one hour." + HOUR + "Represents a time interval of one day." + DAY + "Represents a time interval of one week." + WEEK + "Represents a time interval of one month." + MONTH + "Represents a time interval of one year." + YEAR +} + +"A runtime-calculated Date value relative to `today` or `on`." +input Date_Relative @fdc_forbiddenAsVariableType @fdc_forbiddenAsFieldType { + "Match for today’s date." + today: True @fdc_oneOf(group: "from", required: true) + "A specific date for matching." + on: Date @fdc_oneOf(group: "from", required: true) + "Add the provided duration to the base date." + add: Date_Duration + "Subtract the provided duration from the base date." + sub: Date_Duration + "Truncate the date to the provided interval." + truncateTo: Date_Interval +} + +input Date_Duration @fdc_forbiddenAsVariableType @fdc_forbiddenAsFieldType { + "The number of days for the duration." + days: Int! = 0 + "The number of weeks for the duration." + weeks: Int! = 0 + "The number of months for the duration." + months: Int! = 0 + "The number of years for the duration." + years: Int! = 0 +} + +enum Date_Interval @fdc_forbiddenAsFieldType { + "Represents a time interval of one week." + WEEK + "Represents a time interval of one month." + MONTH + "Represents a time interval of one year." + YEAR +} + +"Update input of a `String` value." +input String_Update { + "Set the field to a provided value." + set: String @fdc_oneOf(group: "set") + "Set the field to a provided server value expression." + set_expr: String_Expr @fdc_oneOf(group: "set") +} + +"Update input of a `String` list value." +input String_ListUpdate { + "Set the list with the provided values." + set: [String!] + "Append the provided values to the existing list." + append: [String!] + "Prepend the provided values to the existing list." + prepend: [String!] +} + +"Update input of a `UUID` value." +input UUID_Update { + "Set the field to a provided UUID." + set: UUID @fdc_oneOf(group: "set") + "Set the field to a provided UUID expression." + set_expr: UUID_Expr @fdc_oneOf(group: "set") +} + +"Update input of an `ID` list value." +input UUID_ListUpdate { + "Set the list with the provided list of UUIDs." + set: [UUID!] + "Append the provided UUIDs to the existing list." + append: [UUID!] + "Prepend the provided UUIDs to the existing list." + prepend: [UUID!] +} + +"Update input of an `Int` value." +input Int_Update { + "Set the field to a provided value." + set: Int + "Increment the field by a provided value." + inc: Int + "Decrement the field by a provided value." + dec: Int +} + +"Update input of an `Int` list value." +input Int_ListUpdate { + "Set the list with the provided values." + set: [Int!] + "Append the provided list of values to the existing list." + append: [Int!] + "Prepend the provided list of values to the existing list." + prepend: [Int!] +} + +"Update input of an `Int64` value." +input Int64_Update { + "Set the field to a provided value." + set: Int64 + "Increment the field by a provided value." + inc: Int64 + "Decrement the field by a provided value." + dec: Int64 +} + +"Update input of an `Int64` list value." +input Int64_ListUpdate { + "Replace the list with the provided values." + set: [Int64!] + "Append the provided list of values to the existing list." + append: [Int64!] + "Prepend the provided list of values to the existing list." + prepend: [Int64!] +} + +"Update input of a `Float` value." +input Float_Update { + "Set the field to a provided value." + set: Float + "Increment the field by a provided value." + inc: Float + "Decrement the field by a provided value." + dec: Float +} + +"Update input of a `Float` list value." +input Float_ListUpdate { + "Set the list with the provided values." + set: [Float!] + "Append the provided list of values to the existing list." + append: [Float!] + "Prepend the provided list of values to the existing list." + prepend: [Float!] +} + +"Update input of a `Boolean` value." +input Boolean_Update { + "Set the field to a provided value." + set: Boolean +} + +"Update input of a `Boolean` list value." +input Boolean_ListUpdate { + "Set the list with the provided values." + set: [Boolean!] + "Append the provided list of values to the existing list." + append: [Boolean!] + "Prepend the provided list of values to the existing list." + prepend: [Boolean!] +} + +"Update input of an `Any` value." +input Any_Update { + "Set the field to a provided value." + set: Any +} + +"Update input of an `Any` list value." +input Any_ListUpdate { + "Set the list with the provided values." + set: [Any!] + "Append the provided list of values to the existing list." + append: [Any!] + "Prepend the provided list of values to the existing list." + prepend: [Any!] +} + +type Query { + """ + _service provides customized introspection on Firebase Data Connect Sevice. + """ + _service: _Service! +} + +""" +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] + +In the PostgreSQL table, it's stored as [`pgvector`](https://github.com/pgvector/pgvector). + +See `Vector_Embed` for how to generate text embeddings in query and mutations. +""" +scalar Vector + +""" +Defines the similarity function to use when comparing vectors in queries. + +Defaults to `INNER_PRODUCT`. + +View [all vector functions](https://github.com/pgvector/pgvector?tab=readme-ov-file#vector-functions). +""" +enum VectorSimilarityMethod { + "Measures the Euclidean (L2) distance between two vectors." + L2 + "Measures the cosine similarity between two vectors." + COSINE + "Measures the inner product(dot product) between two vectors." + INNER_PRODUCT +} + +"Conditions on a Vector value." +input Vector_Filter { + "Match if the field is exactly equal to the provided vector." + eq: Vector + "Match if the field is not equal to the provided vector." + ne: Vector + "Match if the field value is among the provided list of vectors." + in: [Vector!] + "Match if the field value is not among the provided list of vectors." + nin: [Vector!] + "Match if the field is `NULL`." + isNull: Boolean +} + +input Vector_ListFilter { + "Match if the list includes the supplied vector." + includes: Vector + "Match if the list does not include the supplied vector." + excludes: Vector + "Match if the list contains all the provided vectors." + includesAll: [Vector!] + "Match if the list contains none of the provided vectors." + excludesAll: [Vector!] +} + +"Update input of a Vector value." +input Vector_Update { + "Set the field to the provided vector value." + set: Vector @fdc_oneOf(group: "set") + "Set the field to the vector embedding result from a text input." + set_embed: Vector_Embed @fdc_oneOf(group: "set") +} + + +"Update input of a Vector list value." +input Vector_ListUpdate { + "Replace the current list with the provided list of Vector values." + set: [Vector] + "Append the provided Vector values to the existing list." + append: [Vector] + "Prepend the provided Vector values to the existing list." + prepend: [Vector] + "Delete the vector at the specified index." + delete: Int + "The index of the vector to be updated." + i: Int + "Update the vector at the specified index." + update: Vector +} + +""" +Create a vector embedding of text using the given model on Vertex AI. + +Cloud SQL for Postgresql natively integrates with [Vertex AI Text embeddings API](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api) +to effectively generate text embeddings. + +If you uses [`Vector`](scalar.md#Vector) in your schema, Firebase Data Connect automatically installs +[`pgvector`](https://github.com/pgvector/pgvector) and [`google_ml_integration`](https://cloud.google.com/sql/docs/postgres/integrate-cloud-sql-with-vertex-ai) +Postgres extensions in your Cloud SQL database. + +Given a Post table with a `Vector` embedding field. + +```graphql +type Post @table { + content: String! + contentEmbedding: Vector @col(size:768) +} +``` + +NOTE: All natively supported `Vector_Embed_Model` generates vector of length `768`. + +###### Example: Insert embedding + +```graphql +mutation CreatePost($content: String!) { + post_insert(data: { + content: $content, + contentEmbedding_embed: {model: "textembedding-gecko@003", text: $content}, + }) +} +``` + +###### Example: Vector similarity Search + +```graphql +query SearchPost($query: String!) { + posts_contentEmbedding_similarity(compare_embed: {model: "textembedding-gecko@003", text: $query}) { + id + content + } +} +``` +""" +input Vector_Embed @fdc_forbiddenAsVariableType { + """ + The model to use for vector embedding. + Recommend the latest stable model: `textembedding-gecko@003`. + """ + model: Vector_Embed_Model! + "The text to generate the vector embedding from." + text: String! +} + +""" +The Vertex AI model version that is required in input `Vector_Embed`. + +It is recommended to use the latest stable model version: `textembedding-gecko@003`. + +View all supported [Vertex AI Text embeddings APIs](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings-api). +""" +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") + @fdc_example(value: "textembedding-gecko@001", description: "An older version of the textembedding-gecko model") + @fdc_example(value: "text-embedding-004", description: "Another text embedding model") + diff --git a/Examples/FriendlyFlix/dataconnect/data_seed.gql b/Examples/FriendlyFlix/dataconnect/data_seed.gql new file mode 100644 index 0000000..8b6dc7f --- /dev/null +++ b/Examples/FriendlyFlix/dataconnect/data_seed.gql @@ -0,0 +1,687 @@ +mutation { + movie_insertMany( + data: [ + { + id: "550e8400-e29b-41d4-a716-446655440000" + title: "Quantum Paradox" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fquantum_paradox.jpeg?alt=media&token=4142e2a1-bf43-43b5-b7cf-6616be3fd4e3" + releaseYear: 2025 + genre: "sci-fi" + rating: 7.9 + description: "A group of scientists accidentally open a portal to a parallel universe, causing a rift in time. As the team races to close the portal, they encounter alternate versions of themselves, leading to shocking revelations." + tags: ["thriller", "adventure"] + } + { + id: "550e8400-e29b-41d4-a716-446655440001" + title: "The Lone Outlaw" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Flone_outlaw.jpeg?alt=media&token=15525ffc-208f-4b59-b506-ae8348e06e85" + releaseYear: 2023 + genre: "western" + rating: 8.2 + description: "In the lawless Wild West, a mysterious gunslinger with a hidden past takes on a corrupt sheriff and his band of outlaws to bring justice to a small town." + tags: ["action", "drama"] + } + { + id: "550e8400-e29b-41d4-a716-446655440002" + title: "Celestial Harmony" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fcelestial_harmony.jpeg?alt=media&token=3edf1cf9-c2f5-4c75-9819-36ff6a734c9a" + releaseYear: 2024 + genre: "romance" + rating: 7.5 + description: "Two astronauts, stationed on a remote space station, fall in love amidst the isolation of deep space. But when a mysterious signal disrupts their communication, they must find a way to reconnect and survive." + tags: ["romance", "sci-fi"] + } + { + id: "550e8400-e29b-41d4-a716-446655440003" + title: "Noir Mystique" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fnoir_mystique.jpeg?alt=media&token=3299adba-cb98-4302-8b23-aeb679a4f913" + releaseYear: 2022 + genre: "mystery" + rating: 8.0 + description: "A private detective gets caught up in a web of lies, deception, and betrayal while investigating the disappearance of a famous actress in 1940s Hollywood." + tags: ["crime", "thriller"] + } + { + id: "550e8400-e29b-41d4-a716-446655440004" + title: "The Forgotten Island" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fforgotten_island.jpeg?alt=media&token=bc2b16e1-caed-4649-952c-73b6113f205c" + releaseYear: 2025 + genre: "adventure" + rating: 7.6 + description: "An explorer leads an expedition to a remote island rumored to be home to mythical creatures. As the team ventures deeper into the island, they uncover secrets that change the course of history." + tags: ["adventure", "fantasy"] + } + { + id: "550e8400-e29b-41d4-a716-446655440005" + title: "Digital Nightmare" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fdigital_nightmare.jpeg?alt=media&token=335ec842-1ca4-4b09-abd1-e96d9f5c0c2f" + releaseYear: 2024 + genre: "horror" + rating: 6.9 + description: "A tech-savvy teenager discovers a cursed app that brings nightmares to life. As the horrors of the digital world cross into reality, she must find a way to break the curse before it's too late." + tags: ["horror", "thriller"] + } + { + id: "550e8400-e29b-41d4-a716-446655440006" + title: "Eclipse of Destiny" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Feclipse_destiny.jpeg?alt=media&token=346649b3-cb5c-4d7e-b0d4-6f02e3df5959" + releaseYear: 2026 + genre: "fantasy" + rating: 8.1 + description: "In a kingdom on the brink of war, a prophecy speaks of an eclipse that will grant power to the rightful ruler. As factions vie for control, a young warrior must decide where his true loyalty lies." + tags: ["fantasy", "adventure"] + } + { + id: "550e8400-e29b-41d4-a716-446655440007" + title: "Heart of Steel" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fheart_steel.jpeg?alt=media&token=17883d71-329b-415a-86f8-dd4d9e941d7f" + releaseYear: 2023 + genre: "sci-fi" + rating: 7.7 + description: "A brilliant scientist creates a robot with a human heart. As the robot struggles to understand emotions, it becomes entangled in a plot that could change the fate of humanity." + tags: ["sci-fi", "drama"] + } + { + id: "550e8400-e29b-41d4-a716-446655440008" + title: "Rise of the Crimson Empire" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Frise_crimson_empire.jpeg?alt=media&token=6faa73ad-7504-4146-8f3a-50b90f607f33" + releaseYear: 2025 + genre: "action" + rating: 8.4 + description: "A legendary warrior rises to challenge the tyrannical rule of a powerful empire. As rebellion brews, the warrior must unite different factions to lead an uprising." + tags: ["action", "adventure"] + } + { + id: "550e8400-e29b-41d4-a716-446655440009" + title: "Silent Waves" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fsilent_waves.jpeg?alt=media&token=bd626bf1-ec60-4e57-aa07-87ba14e35bb7" + releaseYear: 2024 + genre: "drama" + rating: 8.2 + description: "A talented pianist, who loses his hearing in a tragic accident, must rediscover his passion for music with the help of a young music teacher who believes in him." + tags: ["drama", "music"] + } + { + id: "550e8400-e29b-41d4-a716-446655440010" + title: "Echoes of the Past" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fecho_of_past.jpeg?alt=media&token=d866aa27-8534-4d72-8988-9da4a1b9e452" + releaseYear: 2023 + genre: "historical" + rating: 7.8 + description: "A historian stumbles upon an ancient artifact that reveals hidden truths about an empire long forgotten. As she deciphers the clues, a shadowy organization tries to stop her from unearthing the past." + tags: ["drama", "mystery"] + } + { + id: "550e8400-e29b-41d4-a716-446655440011" + title: "Beyond the Horizon" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fbeyond_horizon.jpeg?alt=media&token=31493973-0692-4e6e-8b88-afb1aaea17ee" + releaseYear: 2026 + genre: "sci-fi" + rating: 8.5 + description: "In the future, Earth's best pilots are sent on a mission to explore a mysterious planet beyond the solar system. What they find changes humanity's understanding of the universe forever." + tags: ["sci-fi", "adventure"] + } + { + id: "550e8400-e29b-41d4-a716-446655440012" + title: "Shadows and Lies" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fshadows_lies.jpeg?alt=media&token=01afb80d-caee-47f8-a00e-aea8b9e459a2" + releaseYear: 2022 + genre: "crime" + rating: 7.9 + description: "A young detective with a dark past investigates a series of mysterious murders in a city plagued by corruption. As she digs deeper, she realizes nothing is as it seems." + tags: ["crime", "thriller"] + } + { + id: "550e8400-e29b-41d4-a716-446655440013" + title: "The Last Symphony" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Flast_symphony.jpeg?alt=media&token=f9bf80cd-3d8e-4e24-8503-7feb11f4e397" + releaseYear: 2024 + genre: "drama" + rating: 8.0 + description: "An aging composer struggling with memory loss attempts to complete his final symphony. With the help of a young prodigy, he embarks on an emotional journey through his memories and legacy." + tags: ["drama", "music"] + } + { + id: "550e8400-e29b-41d4-a716-446655440014" + title: "Moonlit Crusade" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fmoonlit_crusade.jpeg?alt=media&token=b13241f5-d7d0-4370-b651-07847ad99dc2" + releaseYear: 2025 + genre: "fantasy" + rating: 8.3 + description: "A knight is chosen by an ancient order to embark on a quest under the light of the full moon. Facing mythical beasts and treacherous landscapes, he seeks a relic that could save his kingdom." + tags: ["fantasy", "adventure"] + } + { + id: "550e8400-e29b-41d4-a716-446655440015" + title: "Abyss of the Deep" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fabyss_deep.jpeg?alt=media&token=2417321d-2451-4ec0-9ed6-6297042170e6" + releaseYear: 2023 + genre: "horror" + rating: 7.2 + description: "When a group of marine biologists descends into the unexplored depths of the ocean, they encounter a terrifying and ancient force. Now, they must survive as the abyss comes alive." + tags: ["horror", "thriller"] + } + { + id: "550e8400-e29b-41d4-a716-446655440016" + title: "Phoenix Rising" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fpheonix_rising.jpeg?alt=media&token=7298b1fd-833c-471c-a55d-e8fc798b4ab2" + releaseYear: 2025 + genre: "action" + rating: 8.6 + description: "A special forces operative, presumed dead, returns to avenge his fallen comrades. With nothing to lose, he faces a powerful enemy in a relentless pursuit for justice." + tags: ["action", "thriller"] + } + { + id: "550e8400-e29b-41d4-a716-446655440017" + title: "The Infinite Knot" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Finfinite_knot.jpeg?alt=media&token=93d54d93-d933-4663-a6fe-26b707ef823e" + releaseYear: 2026 + genre: "romance" + rating: 7.4 + description: "Two souls destined to meet across multiple lifetimes struggle to find each other in a chaotic world. With each incarnation, they get closer, but time itself becomes their greatest obstacle." + tags: ["romance", "fantasy"] + } + { + id: "550e8400-e29b-41d4-a716-446655440018" + title: "Parallel Justice" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fparalel_justice.jpeg?alt=media&token=4544b5e2-7a1d-46ca-a97f-eb6a490d4288" + releaseYear: 2024 + genre: "crime" + rating: 8.1 + description: "A lawyer who can see the outcomes of different timelines must choose between justice and personal gain. When a high-stakes case arises, he faces a moral dilemma that could alter his life forever." + tags: ["crime", "thriller"] + } + { + id: "550e8400-e29b-41d4-a716-446655440019" + title: "Veil of Illusion" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fveil_illusion.jpeg?alt=media&token=7bf09a3c-c531-478a-9d02-5d99fca9393b" + releaseYear: 2022 + genre: "mystery" + rating: 7.8 + description: "A magician-turned-detective uses his skills in illusion to solve crimes. When a series of murders leaves the city in fear, he must reveal the truth hidden behind a veil of deceit." + tags: ["mystery", "crime"] + } + ] + ) + actor_insertMany( + data: [ + { + id: "123e4567-e89b-12d3-a456-426614174020" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Foliver_blackwood.jpeg?alt=media&token=79cdbc29-c2c6-4dc3-b87f-f6dc4f1a8208" + name: "Oliver Blackwood" + } + { + id: "123e4567-e89b-12d3-a456-426614174021" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Femma_westfield.jpeg?alt=media&token=2991c3c9-cfa8-4067-8b26-c5239b6894c4" + name: "Emma Westfield" + } + { + id: "123e4567-e89b-12d3-a456-426614174022" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fjack_stone.jpeg?alt=media&token=74a564aa-d840-4bdd-a8a6-c6b17bbde608" + name: "Jack Stone" + } + { + id: "123e4567-e89b-12d3-a456-426614174023" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fclara_woods.jpeg?alt=media&token=b4ff2a15-ef6d-4f20-86c9-07d67015fb29" + name: "Clara Woods" + } + { + id: "123e4567-e89b-12d3-a456-426614174024" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fnoah_frost.jpeg?alt=media&token=0d08179a-7778-405e-9501-feac43b77a99" + name: "Noah Frost" + } + { + id: "123e4567-e89b-12d3-a456-426614174025" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fisabelle_hart.jpeg?alt=media&token=d4fdf896-0f5b-4c32-91a4-7a541a95e77d" + name: "Isabella Hart" + } + { + id: "123e4567-e89b-12d3-a456-426614174026" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fliam_hale.jpeg?alt=media&token=08e8eeee-b8ef-4e7b-8f97-a1e0b59321cc" + name: "Liam Hale" + } + { + id: "123e4567-e89b-12d3-a456-426614174027" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fsophia_knight.jpeg?alt=media&token=7a79ef21-93e0-46f9-934c-6bbef7b5d430" + name: "Sophia Knight" + } + { + id: "123e4567-e89b-12d3-a456-426614174028" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Falex_clay.jpeg?alt=media&token=2a798cdb-f44f-48d5-91bc-9d26a758944e" + name: "Alexander Clay" + } + { + id: "123e4567-e89b-12d3-a456-426614174029" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Famelia_stone.jpeg?alt=media&token=34f21ba9-9e28-4708-9e55-f123634ab506" + name: "Amelia Stone" + } + { + id: "123e4567-e89b-12d3-a456-426614174030" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fethan_blake.jpeg?alt=media&token=41352170-a5cd-4088-b8fd-1c4ee0d52cad" + name: "Ethan Blake" + } + { + id: "123e4567-e89b-12d3-a456-426614174031" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fmia_gray.jpeg?alt=media&token=1ba1831a-3ada-485a-b5c9-2d018bf1862b" + name: "Mia Gray" + } + { + id: "123e4567-e89b-12d3-a456-426614174032" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Flucas_reed.jpeg?alt=media&token=c74f44f3-ae98-4208-8e67-18c2db65a5c1" + name: "Lucas Reed" + } + { + id: "123e4567-e89b-12d3-a456-426614174033" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fevelyn_harper.jpeg?alt=media&token=b138b308-9589-4dfe-8c50-a6d70f06dfb1" + name: "Evelyn Harper" + } + { + id: "123e4567-e89b-12d3-a456-426614174034" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Foscar_smith.jpeg?alt=media&token=d493da85-644d-4d45-a09d-ecb5416645e4" + name: "Oscar Smith" + } + { + id: "123e4567-e89b-12d3-a456-426614174035" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fava_winter.jpeg?alt=media&token=757e4b11-0372-401e-8fa0-61797e90312a" + name: "Ava Winter" + } + { + id: "123e4567-e89b-12d3-a456-426614174036" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fleo_hunt.jpeg?alt=media&token=2cb14738-b39b-47b1-87f9-b45f38245179" + name: "Leo Hunt" + } + { + id: "123e4567-e89b-12d3-a456-426614174037" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Flucy_walsh.jpeg?alt=media&token=016a216c-f329-4c10-bbe8-b31425f73c69" + name: "Lucy Walsh" + } + { + id: "123e4567-e89b-12d3-a456-426614174038" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fmason_ford.jpeg?alt=media&token=55388be9-fdc8-483f-8352-c29755ed3574" + name: "Mason Ford" + } + { + id: "123e4567-e89b-12d3-a456-426614174039" + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Flily_moore.jpeg?alt=media&token=19538aa6-1baf-4033-8fd7-d2a62aa79f51" + name: "Lily Moore" + } + ] + ) + movieMetadata_insertMany( + data: [ + { + movieId: "550e8400-e29b-41d4-a716-446655440000" + director: "Henry Caldwell" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440001" + director: "Juliana Mason, Clark Avery" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440002" + director: "Diana Rivers" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440003" + director: "Liam Thatcher" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440004" + director: "Evelyn Hart" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440005" + director: "Grayson Brooks" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440006" + director: "Isabella Quinn" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440007" + director: "Vincent Hale" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440008" + director: "Amelia Sutton" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440009" + director: "Lucas Stone" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440010" + director: "Sophia Langford" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440011" + director: "Noah Bennett" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440012" + director: "Chloe Armstrong" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440013" + director: "Sebastian Crane" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440014" + director: "Isla Fitzgerald" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440015" + director: "Oliver Hayes" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440016" + director: "Mila Donovan" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440017" + director: "Carter Monroe, Elise Turner" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440018" + director: "Adrian Blake" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440019" + director: "Hazel Carter" + } + ] + ) +movieActor_insertMany( + data: [ + { + movieId: "550e8400-e29b-41d4-a716-446655440000" + actorId: "123e4567-e89b-12d3-a456-426614174020" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440000" + actorId: "123e4567-e89b-12d3-a456-426614174021" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440001" + actorId: "123e4567-e89b-12d3-a456-426614174021" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440001" + actorId: "123e4567-e89b-12d3-a456-426614174022" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440002" + actorId: "123e4567-e89b-12d3-a456-426614174022" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440002" + actorId: "123e4567-e89b-12d3-a456-426614174023" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440003" + actorId: "123e4567-e89b-12d3-a456-426614174023" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440003" + actorId: "123e4567-e89b-12d3-a456-426614174024" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440004" + actorId: "123e4567-e89b-12d3-a456-426614174024" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440004" + actorId: "123e4567-e89b-12d3-a456-426614174025" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440005" + actorId: "123e4567-e89b-12d3-a456-426614174025" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440005" + actorId: "123e4567-e89b-12d3-a456-426614174026" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440006" + actorId: "123e4567-e89b-12d3-a456-426614174026" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440006" + actorId: "123e4567-e89b-12d3-a456-426614174027" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440007" + actorId: "123e4567-e89b-12d3-a456-426614174027" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440007" + actorId: "123e4567-e89b-12d3-a456-426614174028" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440008" + actorId: "123e4567-e89b-12d3-a456-426614174028" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440008" + actorId: "123e4567-e89b-12d3-a456-426614174029" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440009" + actorId: "123e4567-e89b-12d3-a456-426614174029" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440009" + actorId: "123e4567-e89b-12d3-a456-426614174030" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440010" + actorId: "123e4567-e89b-12d3-a456-426614174030" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440010" + actorId: "123e4567-e89b-12d3-a456-426614174031" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440011" + actorId: "123e4567-e89b-12d3-a456-426614174031" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440011" + actorId: "123e4567-e89b-12d3-a456-426614174032" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440012" + actorId: "123e4567-e89b-12d3-a456-426614174032" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440012" + actorId: "123e4567-e89b-12d3-a456-426614174033" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440013" + actorId: "123e4567-e89b-12d3-a456-426614174033" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440013" + actorId: "123e4567-e89b-12d3-a456-426614174034" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440014" + actorId: "123e4567-e89b-12d3-a456-426614174034" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440014" + actorId: "123e4567-e89b-12d3-a456-426614174035" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440015" + actorId: "123e4567-e89b-12d3-a456-426614174035" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440015" + actorId: "123e4567-e89b-12d3-a456-426614174036" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440016" + actorId: "123e4567-e89b-12d3-a456-426614174036" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440016" + actorId: "123e4567-e89b-12d3-a456-426614174037" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440017" + actorId: "123e4567-e89b-12d3-a456-426614174037" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440017" + actorId: "123e4567-e89b-12d3-a456-426614174038" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440018" + actorId: "123e4567-e89b-12d3-a456-426614174038" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440018" + actorId: "123e4567-e89b-12d3-a456-426614174039" + role: "supporting" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440019" + actorId: "123e4567-e89b-12d3-a456-426614174039" + role: "main" + } + { + movieId: "550e8400-e29b-41d4-a716-446655440019" + actorId: "123e4567-e89b-12d3-a456-426614174020" + role: "supporting" + } + ] +) + user_insertMany( + data: [ + { id: "SnLgOC3lN4hcIl69s53cW0Q8R1T2", username: "sherlock_h" } + { id: "fep4fXpGWsaRpuphq9CIrBIXQ0S2", username: "hercule_p" } + { id: "TBedjwCX0Jf955Uuoxk6k74sY0l1", username: "jane_d" } + ] + ) + + review_insertMany( + data: [ + { + id: "345e4567-e89b-12d3-a456-426614174000" + userId: "SnLgOC3lN4hcIl69s53cW0Q8R1T2" + movieId: "550e8400-e29b-41d4-a716-446655440000" + rating: 5 + reviewText: "An incredible movie with a mind-blowing plot!" + reviewDate_date: { today: true } + } + { + id: "345e4567-e89b-12d3-a456-426614174001" + userId: "fep4fXpGWsaRpuphq9CIrBIXQ0S2" + movieId: "550e8400-e29b-41d4-a716-446655440001" + rating: 5 + reviewText: "A revolutionary film that changed cinema forever." + reviewDate_date: { today: true } + } + { + id: "345e4567-e89b-12d3-a456-426614174002" + userId: "TBedjwCX0Jf955Uuoxk6k74sY0l1" + movieId: "550e8400-e29b-41d4-a716-446655440002" + rating: 5 + reviewText: "A visually stunning and emotionally impactful movie." + reviewDate_date: { today: true } + } + { + id: "345e4567-e89b-12d3-a456-426614174003" + userId: "SnLgOC3lN4hcIl69s53cW0Q8R1T2" + movieId: "550e8400-e29b-41d4-a716-446655440003" + rating: 4 + reviewText: "A fantastic superhero film with great performances." + reviewDate_date: { today: true } + } + { + id: "345e4567-e89b-12d3-a456-426614174004" + userId: "fep4fXpGWsaRpuphq9CIrBIXQ0S2" + movieId: "550e8400-e29b-41d4-a716-446655440004" + rating: 5 + reviewText: "An amazing film that keeps you on the edge of your seat." + reviewDate_date: { today: true } + } + { + id: "345e4567-e89b-12d3-a456-426614174005" + userId: "TBedjwCX0Jf955Uuoxk6k74sY0l1" + movieId: "550e8400-e29b-41d4-a716-446655440005" + rating: 5 + reviewText: "An absolute classic with unforgettable dialogue." + reviewDate_date: { today: true } + } + ] + ) + + favorite_movie_insertMany( + data: [ + { + userId: "SnLgOC3lN4hcIl69s53cW0Q8R1T2" + movieId: "550e8400-e29b-41d4-a716-446655440000" + } + { + userId: "fep4fXpGWsaRpuphq9CIrBIXQ0S2" + movieId: "550e8400-e29b-41d4-a716-446655440001" + } + { + userId: "TBedjwCX0Jf955Uuoxk6k74sY0l1" + movieId: "550e8400-e29b-41d4-a716-446655440002" + } + { + userId: "SnLgOC3lN4hcIl69s53cW0Q8R1T2" + movieId: "550e8400-e29b-41d4-a716-446655440003" + } + { + userId: "fep4fXpGWsaRpuphq9CIrBIXQ0S2" + movieId: "550e8400-e29b-41d4-a716-446655440004" + } + { + userId: "TBedjwCX0Jf955Uuoxk6k74sY0l1" + movieId: "550e8400-e29b-41d4-a716-446655440005" + } + ] + ) +} \ No newline at end of file diff --git a/Examples/FriendlyFlix/dataconnect/dataconnect.yaml b/Examples/FriendlyFlix/dataconnect/dataconnect.yaml new file mode 100644 index 0000000..aac76c4 --- /dev/null +++ b/Examples/FriendlyFlix/dataconnect/dataconnect.yaml @@ -0,0 +1,11 @@ +specVersion: "v1beta" +serviceId: "dataconnect" +location: "us-central1" +schema: + source: "./schema" + datasource: + postgresql: + database: "fdcdb" + cloudSql: + instanceId: "fdc-sql" +connectorDirs: ["./movie-connector"] diff --git a/Examples/FriendlyFlix/dataconnect/movie-connector/connector.yaml b/Examples/FriendlyFlix/dataconnect/movie-connector/connector.yaml new file mode 100644 index 0000000..d8410a8 --- /dev/null +++ b/Examples/FriendlyFlix/dataconnect/movie-connector/connector.yaml @@ -0,0 +1,7 @@ +connectorId: friendly-flix +authMode: PUBLIC +generate: + swiftSdk: + outputDir: "../../app" + package: "FriendlyFlixSDK" + observablePublisher: observableMacro \ No newline at end of file diff --git a/Examples/FriendlyFlix/dataconnect/movie-connector/mutations.gql b/Examples/FriendlyFlix/dataconnect/movie-connector/mutations.gql new file mode 100644 index 0000000..0d7545e --- /dev/null +++ b/Examples/FriendlyFlix/dataconnect/movie-connector/mutations.gql @@ -0,0 +1,102 @@ +mutation UpsertUser($username: String!) @auth(level: USER) { + user_upsert( + data: { + id_expr: "auth.uid" + username: $username + } + ) +} + +# Add a movie to the user's favorites list +mutation AddFavoritedMovie($movieId: UUID!) @auth(level: USER) { + favorite_movie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId }) +} + +# Remove a movie from the user's favorites list +mutation DeleteFavoritedMovie($movieId: UUID!) @auth(level: USER) { + favorite_movie_delete(key: { userId_expr: "auth.uid", movieId: $movieId }) +} + +# Add a review for a movie +mutation AddReview($movieId: UUID!, $rating: Int!, $reviewText: String!) +@auth(level: USER) { + review_insert( + data: { + userId_expr: "auth.uid" + movieId: $movieId + rating: $rating + reviewText: $reviewText + reviewDate_date: { today: true } + } + ) +} + +# Update a user's review for a movie +mutation UpdateReview($movieId: UUID!, $rating: Int!, $reviewText: String!) +@auth(level: USER) { + review_update( + key: { userId_expr: "auth.uid", movieId: $movieId } + 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!) @auth(level: USER) { + review_delete(key: { userId_expr: "auth.uid", movieId: $movieId }) +} + +# The mutations below are unused by the application, but are useful examples for more complex cases + +# Create a movie based on user input +# mutation CreateMovie( +# $title: String! +# $releaseYear: Int! +# $genre: String! +# $rating: Float +# $description: String +# $imageUrl: String! +# $tags: [String!] = [] +# ) @auth(expr: "auth.token.isAdmin == true") { + +# } +# 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!] = [] +# ) @auth(level: USER_EMAIL_VERIFIED) { +# 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!) @auth(level: USER_EMAIL_VERIFIED) { +# movie_delete(id: $id) +# } + +# Delete movies with a rating lower than the specified minimum rating +# mutation DeleteUnpopularMovies($minRating: Float!) @auth(level: USER_EMAIL_VERIFIED) { +# movie_deleteMany(where: { rating: { le: $minRating } }) +# } +# End of example mutations \ No newline at end of file diff --git a/Examples/FriendlyFlix/dataconnect/movie-connector/queries.gql b/Examples/FriendlyFlix/dataconnect/movie-connector/queries.gql new file mode 100644 index 0000000..2ad3863 --- /dev/null +++ b/Examples/FriendlyFlix/dataconnect/movie-connector/queries.gql @@ -0,0 +1,331 @@ +# List subset of fields for movies +query ListMovies($orderByRating: OrderDirection, $orderByReleaseYear: OrderDirection, $limit: Int) @auth(level: PUBLIC) { + movies( + orderBy: [ + { rating: $orderByRating }, + { releaseYear: $orderByReleaseYear } + ] + limit: $limit + ) { + id + title + imageUrl + releaseYear + genre + rating + tags + description + } +} + +# 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 + } + 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 + 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 + } + } +} + + +# Get user by ID +query GetCurrentUser @auth(level: USER) { + user(key: { id_expr: "auth.uid" }) { + id + username + reviews: reviews_on_user { + id + rating + reviewDate + reviewText + movie { + id + title + } + } + favoriteMovies: favorite_movies_on_user { + movie { + id + title + genre + imageUrl + releaseYear + rating + description + tags + metadata: movieMetadatas_on_movie { + director + } + } + } + } +} + +query GetIfFavoritedMovie($movieId: UUID!) @auth(level: USER) { + favorite_movie(key: { userId_expr: "auth.uid", movieId: $movieId }) { + movieId + } +} + +# Search for movies, actors, and reviews +query SearchAll( + $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 +# limit: 5 +# ) { +# id +# title +# description +# tags +# rating +# imageUrl +# } +# } + + + +# # The queries below are unused by the application, but are useful examples for more complex cases + +# List movies by partial title match +query ListMoviesByPartialTitle($searchTerm: String!) @auth(level: PUBLIC) { + movies(where: { title: { contains: $searchTerm } }) { + id + title + imageUrl + releaseYear + genre + rating + description + } +} + +# # List movies by tag +# query ListMoviesByTag($tag: String!) @auth(level: PUBLIC) { +# movies(where: { tags: { includes: $tag } }) { +# id +# title +# imageUrl +# genre +# rating +# } +# } + +# # 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 movies by rating and genre with OR filters +# query SearchMovieOr( +# $minRating: Float +# $maxRating: Float +# $genre: String +# $tag: String +# $input: String +# ) @auth(level: PUBLIC) { +# movies( +# where: { +# _or: [ +# { rating: { ge: $minRating } } +# { rating: { le: $maxRating } } +# { genre: { eq: $genre } } +# { tags: { includes: $tag } } +# { title: { contains: $input } } +# ] +# } +# ) { +# id +# rating +# title +# imageUrl +# } +# } + +# # List movies by rating and genre with AND filters +# query SearchMovieAnd( +# $minRating: Float +# $maxRating: Float +# $genre: String +# $tag: String +# $input: String +# ) @auth(level: PUBLIC) { +# movies( +# where: { +# _and: [ +# { rating: { ge: $minRating } } +# { rating: { le: $maxRating } } +# { genre: { eq: $genre } } +# { tags: { includes: $tag } } +# { title: { contains: $input } } +# ] +# } +# ) { +# id +# rating +# title +# imageUrl +# } +# } + +# # Get favorite actors by user ID +# query GetFavoriteActors @auth(level: USER) { +# user(key: {id_expr: "auth.uid"}) { +# favorite_actors_on_user { +# actor { +# id +# name +# imageUrl +# } +# } +# } +# } + +# # Get favorite movies by user ID +query GetUserFavoriteMovies @auth(level: USER) { + user(id_expr: "auth.uid") { + favoriteMovies: favorite_movies_on_user { + movie { + id + title + genre + imageUrl + releaseYear + rating + description + } + } + } +} +# # end of example queries \ No newline at end of file diff --git a/Examples/FriendlyFlix/dataconnect/schema/schema.gql b/Examples/FriendlyFlix/dataconnect/schema/schema.gql new file mode 100644 index 0000000..edbebe5 --- /dev/null +++ b/Examples/FriendlyFlix/dataconnect/schema/schema.gql @@ -0,0 +1,88 @@ +# Movies +# TODO: Fill out Movie table +type Movie + # The below parameter values are generated by default with @table, and can be edited manually. + @table { + # implicitly calls @col to generates a column name. ex: @col(name: "movie_id") + id: UUID! @default(expr: "uuidV4()") + title: String! + imageUrl: String! + releaseYear: Int + genre: String + rating: Float + description: String + tags: [String] + # descriptionEmbedding: Vector @col(size:768) # Enables vector search +} + +# Movie Metadata +# Movie - MovieMetadata is a one-to-one relationship +# TODO: Fill out MovieMetadata table +type MovieMetadata + @table { + # @ref creates a field in the current table (MovieMetadata) + # It is a reference that holds the primary key of the referenced type + # In this case, @ref(fields: "movieId", references: "id") is implied + movie: Movie! @ref + # movieId: UUID <- this is created by the above @ref + director: String +} + +# 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 +# TODO: Fill out Actor table +type Actor @table { + id: UUID! + imageUrl: String! + name: String! @col(name: "name", dataType: "varchar(30)") +} + +# 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 +# TODO: Fill out User table +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 +} + +# Reviews +# TODO: Fill out Review table +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") +} + +# 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] +# TODO: Fill out MovieActor table +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" +} + +# Join table for many-to-many relationship for users and favorite movies +# TODO: Fill out FavoriteMovie table +type FavoriteMovie + @table(name: "FavoriteMovies", singular: "favorite_movie", plural: "favorite_movies", key: ["user", "movie"]) { + # @ref is implicit + user: User! + movie: Movie! +} \ No newline at end of file diff --git a/Examples/FriendlyFlix/firebase.json b/Examples/FriendlyFlix/firebase.json new file mode 100644 index 0000000..c46f2c5 --- /dev/null +++ b/Examples/FriendlyFlix/firebase.json @@ -0,0 +1,31 @@ +{ + "emulators": { + "auth": { + "port": 9099 + }, + "hosting": { + "port": 5000 + }, + "dataconnect": { + "port": 9399 + }, + "ui": { + "enabled": true + }, + "singleProjectMode": true + }, + "dataconnect": { + "source": "./dataconnect" + }, + "hosting": { + "source": "app", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "frameworksBackend": { + "region": "us-central1" + } + } +}