├── .editorconfig ├── .gitattributes ├── .github ├── codecov.yml ├── dependabot.yml └── workflows │ ├── build-artifacts-code.yml │ ├── codeql-analysis.yml │ ├── format.yml │ ├── publish-code.yml │ ├── test-code.yml │ └── wipcheck.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── GraphQL.Relay.sln ├── LICENSE.md ├── README.md ├── assets └── logo.64x64.png ├── codeql └── GraphQL.Relay.CodeQL.sln ├── package.json ├── src ├── Directory.Build.props ├── Directory.Build.targets ├── GraphQL.Relay.ApiTests │ ├── ApiApprovalTests.cs │ ├── GraphQL.Relay.ApiTests.csproj │ └── GraphQL.Relay.approved.txt ├── GraphQL.Relay.StarWars │ ├── Api │ │ ├── Cache.cs │ │ ├── Entities.cs │ │ └── Swapi.cs │ ├── GraphQL.Relay.StarWars.csproj │ ├── GraphQLContext.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── Types │ │ ├── EntityInterface.cs │ │ ├── Film.cs │ │ ├── People.cs │ │ ├── Planet.cs │ │ ├── Species.cs │ │ ├── StarWarsQuery.cs │ │ ├── StarWarsSchema.cs │ │ ├── Starship.cs │ │ └── Vehicle.cs │ ├── Utilities │ │ ├── ConnectionBuilderExtensions.cs │ │ ├── ConnectionResults.cs │ │ └── ResolveConnectionContextExtensions.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── GraphQL.Relay.Test │ ├── Fixtures │ │ ├── DatabaseFixture.cs │ │ ├── EnumerableFixture.cs │ │ └── Models │ │ │ └── Blog.cs │ ├── GraphQL.Relay.Test.csproj │ ├── NodeTypeTests.cs │ ├── Types │ │ ├── ArraySliceMetricesTests.cs │ │ ├── ConnectionUtilsTests.cs │ │ ├── EnumerableSliceMetricTests.cs │ │ ├── IncompleteSliceExceptionTests.cs │ │ ├── QueryGraphTypeTests.cs │ │ ├── QueryableSliceMetricTests.cs │ │ └── ResolveConnectionContextFactory.cs │ ├── UnitTest1.cs │ └── Utilities │ │ ├── EdgeRangeTests.cs │ │ ├── EnumerableExtensionsTests.cs │ │ ├── RelayPaginationTests.cs │ │ └── StringExtensionsTests.cs ├── GraphQL.Relay.Todo │ ├── .babelrc │ ├── ClientApp │ │ ├── __generated__ │ │ │ └── appQuery.graphql.js │ │ ├── app.js │ │ ├── components │ │ │ ├── Todo.js │ │ │ ├── TodoApp.js │ │ │ ├── TodoList.js │ │ │ ├── TodoListFooter.js │ │ │ ├── TodoTextInput.js │ │ │ └── __generated__ │ │ │ │ ├── TodoApp_viewer.graphql.js │ │ │ │ ├── TodoListFooter_viewer.graphql.js │ │ │ │ ├── TodoList_viewer.graphql.js │ │ │ │ ├── Todo_todo.graphql.js │ │ │ │ └── Todo_viewer.graphql.js │ │ ├── css │ │ │ ├── base.css │ │ │ └── index.css │ │ └── mutations │ │ │ ├── AddTodoMutation.js │ │ │ ├── ChangeTodoStatusMutation.js │ │ │ ├── MarkAllTodosMutation.js │ │ │ ├── RemoveCompletedTodosMutation.js │ │ │ ├── RemoveTodoMutation.js │ │ │ ├── RenameTodoMutation.js │ │ │ └── __generated__ │ │ │ ├── AddTodoMutation.graphql.js │ │ │ ├── ChangeTodoStatusMutation.graphql.js │ │ │ ├── MarkAllTodosMutation.graphql.js │ │ │ ├── RemoveCompletedTodosMutation.graphql.js │ │ │ ├── RemoveTodoMutation.graphql.js │ │ │ └── RenameTodoMutation.graphql.js │ ├── Database │ │ └── TodoDatabase.cs │ ├── GraphQL.Relay.Todo.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── README.md │ ├── Schema │ │ ├── Mutation.cs │ │ ├── Query.cs │ │ └── Schema.cs │ ├── SchemaWriter.cs │ ├── Startup.cs │ ├── package.json │ ├── webpack.config.js │ ├── wwwroot │ │ ├── index.html │ │ └── schema.json │ └── yarn.lock ├── GraphQL.Relay │ ├── GraphQL.Relay.csproj │ ├── Types │ │ ├── ArraySliceMetrics.cs │ │ ├── ConnectionUtils.cs │ │ ├── IncompleteSliceException.cs │ │ ├── MutationGraphType.cs │ │ ├── MutationInputGraphType.cs │ │ ├── MutationInputs.cs │ │ ├── MutationPayloadGraphType.cs │ │ ├── NodeGraphType.cs │ │ ├── NodeInterface.cs │ │ ├── QueryGraphType.cs │ │ └── SliceMetrics.cs │ ├── Utilities │ │ ├── EdgeRange.cs │ │ ├── EnumerableExtensions.cs │ │ ├── RelayPagination.cs │ │ ├── ResolveConnectionContextExtensions.cs │ │ └── StringExtensions.cs │ ├── package.json │ └── yarn.lock ├── Tests.props └── xunit.runner.json ├── tools ├── version.1.js └── version.js └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | # https://docs.codecov.com/docs/codecov-yaml 2 | comment: 3 | behavior: new 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "nuget" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | ignore: 9 | - dependency-name: "GraphQL" 10 | - dependency-name: "GraphQL.MicrosoftDI" 11 | - dependency-name: "GraphQL.SystemTextJson" 12 | - dependency-name: "GraphQL.Server.Transports.AspNetCore" 13 | - dependency-name: "GraphQL.Server.Ui.GraphiQL" 14 | 15 | - package-ecosystem: "github-actions" 16 | directory: "/" 17 | schedule: 18 | interval: "daily" 19 | -------------------------------------------------------------------------------- /.github/workflows/build-artifacts-code.yml: -------------------------------------------------------------------------------- 1 | name: Build artifacts 2 | 3 | # ==== NOTE: do not rename this yml file or the run_number will be reset ==== 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | - develop 10 | paths: 11 | - src/** 12 | - .github/workflows/** 13 | - "*.sln" 14 | 15 | env: 16 | DOTNET_NOLOGO: true 17 | DOTNET_CLI_TELEMETRY_OPTOUT: true 18 | 19 | jobs: 20 | pack: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Setup .NET SDK 25 | uses: actions/setup-dotnet@v3 26 | with: 27 | dotnet-version: '7.0.x' 28 | source-url: https://nuget.pkg.github.com/graphql-dotnet/index.json 29 | env: 30 | NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 31 | - name: Install dependencies 32 | run: dotnet restore 33 | - name: Build solution [Release] 34 | run: dotnet build --no-restore -c Release -p:VersionSuffix=$GITHUB_RUN_NUMBER 35 | - name: Pack solution [Release] 36 | run: dotnet pack --no-restore --no-build -c Release -p:VersionSuffix=$GITHUB_RUN_NUMBER -o out 37 | - name: Upload artifacts 38 | uses: actions/upload-artifact@v3 39 | with: 40 | name: Nuget packages 41 | path: | 42 | out/* 43 | - name: Publish Nuget packages to GitHub registry 44 | run: dotnet nuget push "out/*" -k ${{secrets.GITHUB_TOKEN}} 45 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/github/codeql 2 | # https://github.com/github/codeql-action 3 | name: CodeQL analysis 4 | 5 | on: 6 | push: 7 | branches: [master, develop] 8 | pull_request: 9 | branches: [master, develop] 10 | 11 | jobs: 12 | analyze: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout source 17 | uses: actions/checkout@v3 18 | 19 | - name: Setup .NET SDK 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: '7.0.x' 23 | 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v2 26 | with: 27 | queries: security-and-quality 28 | languages: csharp 29 | 30 | - name: Install dependencies 31 | run: dotnet restore 32 | 33 | - name: Build CodeQL solution 34 | # https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/troubleshooting-the-codeql-workflow#reduce-the-amount-of-code-being-analyzed-in-a-single-workflow 35 | working-directory: codeql 36 | run: dotnet build --no-restore -p:UseSharedCompilation=false 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Check formatting 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - develop 8 | paths: 9 | - src/** 10 | - .github/workflows/** 11 | 12 | env: 13 | DOTNET_NOLOGO: true 14 | DOTNET_CLI_TELEMETRY_OPTOUT: true 15 | 16 | jobs: 17 | format: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout source 21 | uses: actions/checkout@v3 22 | - name: Setup .NET SDK 23 | uses: actions/setup-dotnet@v3 24 | with: 25 | dotnet-version: 7.0.x 26 | source-url: https://nuget.pkg.github.com/graphql-dotnet/index.json 27 | env: 28 | NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 29 | - name: Install dependencies 30 | run: dotnet restore 31 | - name: Check formatting 32 | run: | 33 | dotnet format --no-restore --verify-no-changes --severity error || (echo "Run 'dotnet format' to fix issues" && exit 1) 34 | -------------------------------------------------------------------------------- /.github/workflows/publish-code.yml: -------------------------------------------------------------------------------- 1 | name: Publish code 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | env: 9 | DOTNET_NOLOGO: true 10 | DOTNET_CLI_TELEMETRY_OPTOUT: true 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Check github.ref starts with 'refs/tags/' 18 | if: ${{ !startsWith(github.ref, 'refs/tags/') }} 19 | run: | 20 | echo Error! github.ref does not start with 'refs/tags' 21 | echo github.ref: ${{ github.ref }} 22 | exit 1 23 | - name: Set version number environment variable 24 | env: 25 | github_ref: ${{ github.ref }} 26 | run: | 27 | version="${github_ref:10}" 28 | echo version=$version 29 | echo "version=$version" >> $GITHUB_ENV 30 | - name: Setup .NET SDK 31 | uses: actions/setup-dotnet@v3 32 | with: 33 | dotnet-version: '7.0.x' 34 | source-url: https://api.nuget.org/v3/index.json 35 | env: 36 | NUGET_AUTH_TOKEN: ${{secrets.RELAY_NUGET_TOKEN}} 37 | - name: Install dependencies 38 | run: dotnet restore 39 | - name: Build solution [Release] 40 | run: dotnet build --no-restore -c Release -p:Version=$version 41 | - name: Pack solution [Release] 42 | run: dotnet pack --no-restore --no-build -c Release -p:Version=$version -o out 43 | - name: Upload Nuget packages as workflow artifacts 44 | uses: actions/upload-artifact@v3 45 | with: 46 | name: Nuget packages 47 | path: | 48 | out/* 49 | - name: Publish Nuget packages to Nuget registry 50 | run: dotnet nuget push "out/*" -k ${{secrets.RELAY_NUGET_TOKEN}} 51 | - name: Upload Nuget packages as release artifacts 52 | uses: actions/github-script@v6 53 | with: 54 | github-token: ${{secrets.GITHUB_TOKEN}} 55 | script: | 56 | console.log('environment', process.versions); 57 | const fs = require('fs').promises; 58 | const { repo: { owner, repo }, sha } = context; 59 | 60 | for (let file of await fs.readdir('out')) { 61 | console.log('uploading', file); 62 | 63 | await github.rest.repos.uploadReleaseAsset({ 64 | owner, 65 | repo, 66 | release_id: ${{ github.event.release.id }}, 67 | name: file, 68 | data: await fs.readFile(`out/${file}`) 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/test-code.yml: -------------------------------------------------------------------------------- 1 | name: Test code 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - develop 8 | paths: 9 | - src/** 10 | - .github/workflows/** 11 | - "*.sln" 12 | # Upload code coverage results when PRs are merged 13 | push: 14 | branches: 15 | - master 16 | - develop 17 | paths: 18 | - src/** 19 | - .github/workflows/** 20 | 21 | env: 22 | DOTNET_NOLOGO: true 23 | DOTNET_CLI_TELEMETRY_OPTOUT: true 24 | 25 | jobs: 26 | test: 27 | runs-on: ${{ matrix.os }} 28 | strategy: 29 | matrix: 30 | os: 31 | - ubuntu-latest 32 | - windows-latest 33 | graphqlversion: 34 | - 7.0.2 35 | - 7.1.1 36 | - 7.4.0 37 | steps: 38 | - name: Checkout source 39 | uses: actions/checkout@v3 40 | - name: Setup .NET SDKs 41 | uses: actions/setup-dotnet@v3 42 | with: 43 | dotnet-version: '7.0.x' 44 | source-url: https://nuget.pkg.github.com/graphql-dotnet/index.json 45 | env: 46 | NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 47 | - name: Install dependencies with GraphQL version ${{ matrix.graphqlversion }} 48 | run: dotnet restore -p:GraphQLTestVersion=${{ matrix.graphqlversion }} 49 | - name: Build solution [Release] 50 | if: ${{ startsWith(matrix.os, 'ubuntu') }} 51 | run: dotnet build --no-restore -c Release -p:GraphQLTestVersion=${{ matrix.graphqlversion }} 52 | - name: Build solution [Debug] 53 | run: dotnet build --no-restore -c Debug -p:GraphQLTestVersion=${{ matrix.graphqlversion }} 54 | - name: Test solution [Debug] 55 | run: > 56 | dotnet test 57 | --no-build 58 | --collect "XPlat Code Coverage" 59 | --results-directory .coverage 60 | -- 61 | DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover 62 | DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Exclude=[GraphQL.Relay.StarWars]*,[GraphQL.Relay.Todo]* 63 | - name: Upload coverage to codecov 64 | if: ${{ startsWith(matrix.os, 'ubuntu') }} 65 | uses: codecov/codecov-action@v3 66 | with: 67 | files: '.coverage/**/coverage.opencover.xml' 68 | 69 | buildcheck: 70 | needs: 71 | - test 72 | runs-on: ubuntu-latest 73 | if: always() 74 | steps: 75 | - name: Pass build check 76 | if: ${{ needs.test.result == 'success' }} 77 | run: exit 0 78 | - name: Fail build check 79 | if: ${{ needs.test.result != 'success' }} 80 | run: exit 1 81 | -------------------------------------------------------------------------------- /.github/workflows/wipcheck.yml: -------------------------------------------------------------------------------- 1 | name: Check if PR title contains [WIP] 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened # when PR is opened 7 | - edited # when PR is edited 8 | - synchronize # when code is added 9 | - reopened # when a closed PR is reopened 10 | 11 | jobs: 12 | check-title: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Fail build if pull request title contains [WIP] 17 | env: 18 | TITLE: ${{ github.event.pull_request.title }} 19 | if: ${{ contains(github.event.pull_request.title, '[WIP]') }} # This function is case insensitive. 20 | run: | 21 | echo Warning! PR title "$TITLE" contains [WIP]. Remove [WIP] from the title when PR is ready. 22 | exit 1 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | node_modules 3 | .vs/ 4 | .idea/ 5 | *.user 6 | *.suo 7 | *.nupkg 8 | *.project.lock.json 9 | project.lock.json 10 | npm-debug.log 11 | bundle.js 12 | fixie-results.xml 13 | 14 | TestResults/ 15 | [Oo]bj/ 16 | [Bb]in/ 17 | packages/ 18 | node_modules/ 19 | nuget/ 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceRoot}/src/GraphQL.Relay.Todo/bin/Debug/netcoreapp2.0/GraphQL.Relay.Todo.dll", 13 | "args": [], 14 | "cwd": "${workspaceRoot}/src/GraphQL.Relay.Todo", 15 | "stopAtEntry": false, 16 | "requireExactSource": false, 17 | "launchBrowser": { 18 | "enabled": false, 19 | "args": "${auto-detect-url}", 20 | "windows": { 21 | "command": "cmd.exe", 22 | "args": "/C start ${auto-detect-url}" 23 | }, 24 | "osx": { 25 | "command": "open" 26 | }, 27 | "linux": { 28 | "command": "xdg-open" 29 | } 30 | }, 31 | "env": { 32 | "ASPNETCORE_ENVIRONMENT": "Development" 33 | }, 34 | "sourceFileMap": { 35 | "/Views": "${workspaceRoot}/Views" 36 | } 37 | }, 38 | { 39 | "name": ".NET Core Launch (console)", 40 | "type": "coreclr", 41 | "request": "launch", 42 | "preLaunchTask": "build", 43 | // If you have changed target frameworks, make sure to update the program path. 44 | "program": "${workspaceRoot}/src/GraphQL.Relay.Test/bin/Debug/netcoreapp2.0/GraphQL.Relay.StarWars.dll", 45 | "args": [], 46 | "cwd": "${workspaceRoot}/src/GraphQL.Relay.Starwars", 47 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 48 | "console": "internalConsole", 49 | "stopAtEntry": false, 50 | "internalConsoleOptions": "openOnSessionStart" 51 | }, 52 | { 53 | "name": ".NET Core Attach", 54 | "type": "coreclr", 55 | "request": "attach", 56 | "processId": "${command:pickProcess}" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "type": "shell", 7 | "command": "dotnet", 8 | "args": [ 9 | "build", 10 | "${workspaceRoot}/src/GraphQL.Relay.Todo/GraphQL.Relay.Todo.csproj" 11 | ], 12 | "problemMatcher": "$msCompile", 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /GraphQL.Relay.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.1.32228.430 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Relay", "src\GraphQL.Relay\GraphQL.Relay.csproj", "{46076F16-C4F7-4879-903D-70D75DD8B3EF}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Relay.Test", "src\GraphQL.Relay.Test\GraphQL.Relay.Test.csproj", "{98919E02-8204-469C-B3DB-16DF21EA50D0}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Relay.StarWars", "src\GraphQL.Relay.StarWars\GraphQL.Relay.StarWars.csproj", "{43F14E11-5D11-427C-B4F7-D68B2339E837}" 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Relay.Todo", "src\GraphQL.Relay.Todo\GraphQL.Relay.Todo.csproj", "{15AFC7EC-11E6-469F-87DF-D1E396463BE9}" 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{2F92B771-95CF-4FC3-AE9A-52E9273349F1}" 14 | ProjectSection(SolutionItems) = preProject 15 | .github\codecov.yml = .github\codecov.yml 16 | .github\dependabot.yml = .github\dependabot.yml 17 | EndProjectSection 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".Solution Items", ".Solution Items", "{5EBDC740-68FC-451D-9F02-B81314A5059E}" 20 | ProjectSection(SolutionItems) = preProject 21 | .editorconfig = .editorconfig 22 | src\Directory.Build.props = src\Directory.Build.props 23 | src\Directory.Build.targets = src\Directory.Build.targets 24 | README.md = README.md 25 | src\Tests.props = src\Tests.props 26 | EndProjectSection 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Relay.ApiTests", "src\GraphQL.Relay.ApiTests\GraphQL.Relay.ApiTests.csproj", "{40232599-AF52-440F-8EB9-F6B4ED184ECA}" 29 | EndProject 30 | Global 31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 32 | Debug|Any CPU = Debug|Any CPU 33 | Debug|x64 = Debug|x64 34 | Debug|x86 = Debug|x86 35 | Release|Any CPU = Release|Any CPU 36 | Release|x64 = Release|x64 37 | Release|x86 = Release|x86 38 | EndGlobalSection 39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 40 | {46076F16-C4F7-4879-903D-70D75DD8B3EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {46076F16-C4F7-4879-903D-70D75DD8B3EF}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {46076F16-C4F7-4879-903D-70D75DD8B3EF}.Debug|x64.ActiveCfg = Debug|Any CPU 43 | {46076F16-C4F7-4879-903D-70D75DD8B3EF}.Debug|x64.Build.0 = Debug|Any CPU 44 | {46076F16-C4F7-4879-903D-70D75DD8B3EF}.Debug|x86.ActiveCfg = Debug|Any CPU 45 | {46076F16-C4F7-4879-903D-70D75DD8B3EF}.Debug|x86.Build.0 = Debug|Any CPU 46 | {46076F16-C4F7-4879-903D-70D75DD8B3EF}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {46076F16-C4F7-4879-903D-70D75DD8B3EF}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {46076F16-C4F7-4879-903D-70D75DD8B3EF}.Release|x64.ActiveCfg = Release|Any CPU 49 | {46076F16-C4F7-4879-903D-70D75DD8B3EF}.Release|x64.Build.0 = Release|Any CPU 50 | {46076F16-C4F7-4879-903D-70D75DD8B3EF}.Release|x86.ActiveCfg = Release|Any CPU 51 | {46076F16-C4F7-4879-903D-70D75DD8B3EF}.Release|x86.Build.0 = Release|Any CPU 52 | {98919E02-8204-469C-B3DB-16DF21EA50D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {98919E02-8204-469C-B3DB-16DF21EA50D0}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {98919E02-8204-469C-B3DB-16DF21EA50D0}.Debug|x64.ActiveCfg = Debug|Any CPU 55 | {98919E02-8204-469C-B3DB-16DF21EA50D0}.Debug|x64.Build.0 = Debug|Any CPU 56 | {98919E02-8204-469C-B3DB-16DF21EA50D0}.Debug|x86.ActiveCfg = Debug|Any CPU 57 | {98919E02-8204-469C-B3DB-16DF21EA50D0}.Debug|x86.Build.0 = Debug|Any CPU 58 | {98919E02-8204-469C-B3DB-16DF21EA50D0}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {98919E02-8204-469C-B3DB-16DF21EA50D0}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {98919E02-8204-469C-B3DB-16DF21EA50D0}.Release|x64.ActiveCfg = Release|Any CPU 61 | {98919E02-8204-469C-B3DB-16DF21EA50D0}.Release|x64.Build.0 = Release|Any CPU 62 | {98919E02-8204-469C-B3DB-16DF21EA50D0}.Release|x86.ActiveCfg = Release|Any CPU 63 | {98919E02-8204-469C-B3DB-16DF21EA50D0}.Release|x86.Build.0 = Release|Any CPU 64 | {43F14E11-5D11-427C-B4F7-D68B2339E837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {43F14E11-5D11-427C-B4F7-D68B2339E837}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {43F14E11-5D11-427C-B4F7-D68B2339E837}.Debug|x64.ActiveCfg = Debug|Any CPU 67 | {43F14E11-5D11-427C-B4F7-D68B2339E837}.Debug|x64.Build.0 = Debug|Any CPU 68 | {43F14E11-5D11-427C-B4F7-D68B2339E837}.Debug|x86.ActiveCfg = Debug|Any CPU 69 | {43F14E11-5D11-427C-B4F7-D68B2339E837}.Debug|x86.Build.0 = Debug|Any CPU 70 | {43F14E11-5D11-427C-B4F7-D68B2339E837}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {43F14E11-5D11-427C-B4F7-D68B2339E837}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {43F14E11-5D11-427C-B4F7-D68B2339E837}.Release|x64.ActiveCfg = Release|Any CPU 73 | {43F14E11-5D11-427C-B4F7-D68B2339E837}.Release|x64.Build.0 = Release|Any CPU 74 | {43F14E11-5D11-427C-B4F7-D68B2339E837}.Release|x86.ActiveCfg = Release|Any CPU 75 | {43F14E11-5D11-427C-B4F7-D68B2339E837}.Release|x86.Build.0 = Release|Any CPU 76 | {15AFC7EC-11E6-469F-87DF-D1E396463BE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 77 | {15AFC7EC-11E6-469F-87DF-D1E396463BE9}.Debug|Any CPU.Build.0 = Debug|Any CPU 78 | {15AFC7EC-11E6-469F-87DF-D1E396463BE9}.Debug|x64.ActiveCfg = Debug|Any CPU 79 | {15AFC7EC-11E6-469F-87DF-D1E396463BE9}.Debug|x64.Build.0 = Debug|Any CPU 80 | {15AFC7EC-11E6-469F-87DF-D1E396463BE9}.Debug|x86.ActiveCfg = Debug|Any CPU 81 | {15AFC7EC-11E6-469F-87DF-D1E396463BE9}.Debug|x86.Build.0 = Debug|Any CPU 82 | {15AFC7EC-11E6-469F-87DF-D1E396463BE9}.Release|Any CPU.ActiveCfg = Release|Any CPU 83 | {15AFC7EC-11E6-469F-87DF-D1E396463BE9}.Release|Any CPU.Build.0 = Release|Any CPU 84 | {15AFC7EC-11E6-469F-87DF-D1E396463BE9}.Release|x64.ActiveCfg = Release|Any CPU 85 | {15AFC7EC-11E6-469F-87DF-D1E396463BE9}.Release|x64.Build.0 = Release|Any CPU 86 | {15AFC7EC-11E6-469F-87DF-D1E396463BE9}.Release|x86.ActiveCfg = Release|Any CPU 87 | {15AFC7EC-11E6-469F-87DF-D1E396463BE9}.Release|x86.Build.0 = Release|Any CPU 88 | {40232599-AF52-440F-8EB9-F6B4ED184ECA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 89 | {40232599-AF52-440F-8EB9-F6B4ED184ECA}.Debug|Any CPU.Build.0 = Debug|Any CPU 90 | {40232599-AF52-440F-8EB9-F6B4ED184ECA}.Debug|x64.ActiveCfg = Debug|Any CPU 91 | {40232599-AF52-440F-8EB9-F6B4ED184ECA}.Debug|x64.Build.0 = Debug|Any CPU 92 | {40232599-AF52-440F-8EB9-F6B4ED184ECA}.Debug|x86.ActiveCfg = Debug|Any CPU 93 | {40232599-AF52-440F-8EB9-F6B4ED184ECA}.Debug|x86.Build.0 = Debug|Any CPU 94 | {40232599-AF52-440F-8EB9-F6B4ED184ECA}.Release|Any CPU.ActiveCfg = Release|Any CPU 95 | {40232599-AF52-440F-8EB9-F6B4ED184ECA}.Release|Any CPU.Build.0 = Release|Any CPU 96 | {40232599-AF52-440F-8EB9-F6B4ED184ECA}.Release|x64.ActiveCfg = Release|Any CPU 97 | {40232599-AF52-440F-8EB9-F6B4ED184ECA}.Release|x64.Build.0 = Release|Any CPU 98 | {40232599-AF52-440F-8EB9-F6B4ED184ECA}.Release|x86.ActiveCfg = Release|Any CPU 99 | {40232599-AF52-440F-8EB9-F6B4ED184ECA}.Release|x86.Build.0 = Release|Any CPU 100 | EndGlobalSection 101 | GlobalSection(SolutionProperties) = preSolution 102 | HideSolutionNode = FALSE 103 | EndGlobalSection 104 | GlobalSection(ExtensibilityGlobals) = postSolution 105 | SolutionGuid = {26C9760C-72D8-43A7-B0D6-FA4AE10D5F06} 106 | EndGlobalSection 107 | EndGlobal 108 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Jason Quense 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/logo.64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-dotnet/relay/1d6795266603bf3ae5f289cac4eeea047eee17da/assets/logo.64x64.png -------------------------------------------------------------------------------- /codeql/GraphQL.Relay.CodeQL.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31919.166 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphQL.Relay", "..\src\GraphQL.Relay\GraphQL.Relay.csproj", "{BE20824B-4C76-4E91-B52F-87EA0CA40418}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {BE20824B-4C76-4E91-B52F-87EA0CA40418}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {BE20824B-4C76-4E91-B52F-87EA0CA40418}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {BE20824B-4C76-4E91-B52F-87EA0CA40418}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {BE20824B-4C76-4E91-B52F-87EA0CA40418}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {A0696AC3-8123-4422-B489-C58B78E24B27} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-relay", 3 | "version": "0.4.1", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "dotnet test src/GraphQL.Relay.Test", 8 | "pack": "rimraf nuget/* && dotnet pack src/GraphQL.Relay -o nuget -c Release --include-source --include-symbols", 9 | "version": "node tools/version", 10 | "release": "npm run test && npm run pack && cd nuget && nuget push *.symbols.nupkg -s https://www.nuget.org/api/v2/package" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/graphql-dotnet/relay.git" 15 | }, 16 | "author": "Jason Quense", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/graphql-dotnet/relay/issues" 20 | }, 21 | "homepage": "https://github.com/graphql-dotnet/relay#readme", 22 | "devDependencies": { 23 | "chalk": "^1.1.3", 24 | "lodash": "^4.17.19", 25 | "rimraf": "^2.5.4", 26 | "semver": "^5.3.0", 27 | "shells": "^2.0.0", 28 | "yargs": "^6.4.0" 29 | }, 30 | "dependencies": { 31 | "fs-extra": "^4.0.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0.7.0-preview 5 | latest 6 | Jason Quense 7 | MIT 8 | logo.64x64.png 9 | true 10 | git 11 | true 12 | true 13 | 14 | 15 | True 16 | embedded 17 | enable 18 | true 19 | true 20 | true 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | README.md 5 | 6 | 7 | 8 | 9 | $(NoWarn);1591 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.ApiTests/ApiApprovalTests.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.Types; 2 | using PublicApiGenerator; 3 | 4 | namespace GraphQL.Relay.ApiTests; 5 | 6 | public class ApiApprovalTests 7 | { 8 | [Theory] 9 | [InlineData(typeof(NodeInterface))] 10 | public void public_api_should_not_change_unintentionally(Type type) 11 | { 12 | string publicApi = type.Assembly.GeneratePublicApi(new() 13 | { 14 | IncludeAssemblyAttributes = false, 15 | }); 16 | 17 | // See: https://shouldly.readthedocs.io/en/latest/assertions/shouldMatchApproved.html 18 | // Note: If the AssemblyName.approved.txt file doesn't match the latest publicApi value, 19 | // this call will try to launch a diff tool to help you out but that can fail on 20 | // your machine if a diff tool isn't configured/setup. 21 | publicApi.ShouldMatchApproved(options => options.WithFilenameGenerator((testMethodInfo, discriminator, fileType, fileExtension) => $"{type.Assembly.GetName().Name}.{fileType}.{fileExtension}")); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.ApiTests/GraphQL.Relay.ApiTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net7 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Api/Cache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace GraphQL.Relay.StarWars.Api 4 | { 5 | public class ResponseCache : ConcurrentDictionary> 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Api/Entities.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Relay.StarWars.Api 2 | { 3 | public static class UriListExtensions 4 | { 5 | public static IEnumerable ToIds(this IList list) 6 | { 7 | return list 8 | .Select(url => url?.ToString().Split('/')[5]); 9 | } 10 | } 11 | 12 | public interface ISwapiResponse { } 13 | 14 | public class EntityList : ISwapiResponse 15 | { 16 | public int Count { get; set; } 17 | public Uri Next { get; set; } 18 | public Uri Previous { get; set; } 19 | public List Results = new(); 20 | } 21 | 22 | public class Entity : ISwapiResponse 23 | { 24 | public string Id => Url.ToString()?.Split('/')[5]; 25 | public DateTime Created { get; set; } 26 | public DateTime Edited { get; set; } 27 | public Uri Url { get; set; } 28 | } 29 | 30 | public class Films : Entity 31 | { 32 | public string Title { get; set; } 33 | public int EpisodeId { get; set; } 34 | public string OpeningCrawl { get; set; } 35 | public string Director { get; set; } 36 | public string Producer { get; set; } 37 | public string ReleaseDate { get; set; } 38 | public IList Characters { get; set; } 39 | public IList Planets { get; set; } 40 | public IList Starships { get; set; } 41 | public IList Vehicles { get; set; } 42 | public IList Species { get; set; } 43 | } 44 | 45 | public class Planets : Entity 46 | { 47 | public string Name { get; set; } 48 | public string RotationPeriod { get; set; } 49 | public string OrbitalPeriod { get; set; } 50 | public string Diameter { get; set; } 51 | public string Climate { get; set; } 52 | public string Gravity { get; set; } 53 | public string Terrain { get; set; } 54 | public string SurfaceWater { get; set; } 55 | public string Population { get; set; } 56 | public IList Residents { get; set; } 57 | public IList Films { get; set; } 58 | } 59 | 60 | public class Species : Entity 61 | { 62 | public string Name { get; set; } 63 | public string Classification { get; set; } 64 | public string Designation { get; set; } 65 | public string AverageHeight { get; set; } 66 | public string SkinColors { get; set; } 67 | public string HairColors { get; set; } 68 | public string EyeColors { get; set; } 69 | public string AverageLifespan { get; set; } 70 | public Uri Homeworld { get; set; } 71 | public string Language { get; set; } 72 | public IList People { get; set; } 73 | public IList Films { get; set; } 74 | } 75 | 76 | public class People : Entity 77 | { 78 | public string Name { get; set; } 79 | public string Height { get; set; } 80 | public string Mass { get; set; } 81 | public string HairColor { get; set; } 82 | public string SkinColor { get; set; } 83 | public string EyeColor { get; set; } 84 | public string BirthYear { get; set; } 85 | public string Gender { get; set; } 86 | public Uri Homeworld { get; set; } 87 | public IList Films { get; set; } 88 | public IList Species { get; set; } 89 | public IList Vehicles { get; set; } 90 | public IList Starships { get; set; } 91 | } 92 | 93 | public class Starships : Entity 94 | { 95 | public string Name { get; set; } 96 | public string Model { get; set; } 97 | public string Manufacturer { get; set; } 98 | public string CostInCredits { get; set; } 99 | public string Length { get; set; } 100 | public string MaxAtmospheringSpeed { get; set; } 101 | public int Crew { get; set; } 102 | public int Passengers { get; set; } 103 | public string CargoCapacity { get; set; } 104 | public string Consumables { get; set; } 105 | public double HyperdriveRating { get; set; } 106 | public string MGLT { get; set; } 107 | public string StarshipClass { get; set; } 108 | public IList Pilots { get; set; } 109 | public IList Films { get; set; } 110 | } 111 | 112 | public class Vehicles : Entity 113 | { 114 | public string Name { get; set; } 115 | public string Model { get; set; } 116 | public string Manufacturer { get; set; } 117 | public string CostInCredits { get; set; } 118 | public string Length { get; set; } 119 | public string MaxAtmospheringSpeed { get; set; } 120 | public string Crew { get; set; } 121 | public string Passengers { get; set; } 122 | public string CargoCapacity { get; set; } 123 | public string Consumables { get; set; } 124 | public string VehicleClass { get; set; } 125 | public IList Pilots { get; set; } 126 | public IList Films { get; set; } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Api/Swapi.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using GraphQL.Relay.StarWars.Utilities; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Serialization; 5 | 6 | namespace GraphQL.Relay.StarWars.Api 7 | { 8 | public class Swapi 9 | { 10 | private readonly HttpClient _client; 11 | 12 | private const string API_BASE = "http://swapi.dev/api"; 13 | private readonly ResponseCache _cache = new(); 14 | 15 | private static string GetResource() where T : Entity => typeof(T).Name.ToLower(); 16 | 17 | public Swapi(ResponseCache cache) 18 | { 19 | _cache = cache; 20 | _client = new HttpClient(new HttpClientHandler 21 | { 22 | AllowAutoRedirect = true, 23 | MaxAutomaticRedirections = 3 24 | }) 25 | { 26 | Timeout = TimeSpan.FromSeconds(30), 27 | }; 28 | 29 | _client.DefaultRequestHeaders.Accept.Add( 30 | new MediaTypeWithQualityHeaderValue("application/json") 31 | ); 32 | } 33 | 34 | public Task Fetch(Uri url) where T : ISwapiResponse 35 | { 36 | return _cache.GetOrAdd(url, async u => 37 | { 38 | var result = await _client.GetAsync(u); 39 | return DeserializeObject(await result.Content.ReadAsStringAsync()); 40 | }).ContinueWith( 41 | t => (T)t.Result, 42 | TaskContinuationOptions.OnlyOnRanToCompletion | 43 | TaskContinuationOptions.ExecuteSynchronously 44 | ); 45 | } 46 | 47 | public async Task> FetchManyAsync(IEnumerable urls) 48 | where T : Entity 49 | { 50 | var entities = await Task.WhenAll(urls.Select(Fetch)); 51 | return entities.AsEnumerable(); 52 | } 53 | 54 | public async Task GetEntityAsync(string id) where T : Entity 55 | { 56 | var name = GetResource(); 57 | var entity = await GetEntity(new Uri($"{API_BASE}/{name}/{id}")); 58 | 59 | return entity; 60 | } 61 | 62 | public Task GetEntity(Uri url) 63 | where T : Entity => 64 | Fetch(url); 65 | 66 | public Task> GetMany(IEnumerable urls) 67 | where T : Entity => 68 | FetchManyAsync(urls); 69 | 70 | private static bool DoneFetching(int count, ConnectionArguments args) 71 | { 72 | if (args.After != null || args.Before != null || args.Last != null || args.First == null) 73 | return false; 74 | return count >= args.First.Value; 75 | } 76 | 77 | public async Task> GetConnectionAsync(ConnectionArguments args) 78 | where T : Entity 79 | { 80 | var count = 0; 81 | var nextUrl = new Uri($"{API_BASE}/{typeof(T).Name.ToLower()}/"); 82 | var entities = new List(); 83 | 84 | EntityList page; 85 | while (nextUrl != null && !DoneFetching(entities.Count, args)) 86 | { 87 | page = await Fetch>(nextUrl); 88 | entities.AddRange(page.Results); 89 | nextUrl = page.Next; 90 | count = page.Count; 91 | } 92 | 93 | return ConnectionEntities.Create(entities, count); 94 | } 95 | 96 | private static T DeserializeObject(string payload) 97 | { 98 | return JsonConvert.DeserializeObject( 99 | payload, 100 | new JsonSerializerSettings 101 | { 102 | ContractResolver = new DefaultContractResolver 103 | { 104 | NamingStrategy = new SnakeCaseNamingStrategy() 105 | }, 106 | Converters = new List { 107 | new NumberConverter() 108 | } 109 | } 110 | ); 111 | } 112 | } 113 | 114 | public class NumberConverter : JsonConverter 115 | { 116 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => 117 | throw new NotImplementedException(); 118 | 119 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 120 | { 121 | if (objectType == typeof(int)) 122 | return int.Parse(reader.Value.ToString()); 123 | 124 | if (objectType == typeof(double)) 125 | return double.Parse(reader.Value.ToString()); 126 | 127 | return reader.Value; 128 | } 129 | 130 | public override bool CanConvert(Type objectType) => 131 | objectType == typeof(int) || objectType == typeof(double); 132 | 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/GraphQL.Relay.StarWars.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/GraphQLContext.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.StarWars.Api; 2 | 3 | namespace GraphQL.Relay.StarWars 4 | { 5 | public class GraphQLContext 6 | { 7 | public Swapi Api { get; } 8 | public GraphQLContext(Swapi api) 9 | { 10 | Api = api; 11 | } 12 | } 13 | 14 | public static class GraphQLUserContextExtensions 15 | { 16 | public static Swapi Api(this ResolveFieldContext context) 17 | { 18 | return ((GraphQLContext)context.UserContext).Api; 19 | } 20 | 21 | // public static IDataLoader GetDataLoader( 22 | // this ResolveFieldContext context, 23 | // Func, Task>> fetchDelegate 24 | // ) { 25 | // return ((GraphQLContext)context.UserContext).LoadContext.GetOrCreateLoader(context.FieldDefinition, fetchDelegate); 26 | // } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | 3 | namespace GraphQL.Relay.StarWars 4 | { 5 | public class Program 6 | { 7 | public static void Main(string[] args) 8 | { 9 | BuildWebHost(args).Run(); 10 | } 11 | 12 | public static IWebHost BuildWebHost(string[] args) => 13 | WebHost.CreateDefaultBuilder(args) 14 | .UseStartup() 15 | .Build(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:58358/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "GraphQL.Relay.StarWars": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:58359/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Startup.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.StarWars.Api; 2 | using GraphQL.Relay.StarWars.Types; 3 | using GraphQL.Relay.Types; 4 | using GraphQL.Types.Relay; 5 | 6 | namespace GraphQL.Relay.StarWars 7 | { 8 | public class Startup 9 | { 10 | public Startup(IConfiguration configuration) 11 | { 12 | Configuration = configuration; 13 | } 14 | 15 | public IConfiguration Configuration { get; } 16 | 17 | // This method gets called by the runtime. Use this method to add services to the container. 18 | public void ConfigureServices(IServiceCollection services) 19 | { 20 | services 21 | .AddTransient() 22 | .AddTransient() 23 | .AddTransient() 24 | .AddSingleton(); 25 | 26 | services.AddGraphQL(b => b 27 | .UseApolloTracing(true) 28 | .AddSchema() 29 | .AddAutoClrMappings() 30 | .AddSystemTextJson() 31 | .AddErrorInfoProvider(options => options.ExposeExceptionDetails = true) 32 | .AddGraphTypes(typeof(StarWarsSchema).Assembly)); 33 | } 34 | 35 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 36 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 37 | { 38 | if (env.IsDevelopment()) 39 | { 40 | app.UseDeveloperExceptionPage(); 41 | } 42 | 43 | app.UseGraphQL(); 44 | app.UseGraphQLGraphiQL(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Types/EntityInterface.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Relay.StarWars.Types 4 | { 5 | public class EntityInterface : InterfaceGraphType 6 | { 7 | public EntityInterface() 8 | { 9 | Field("id"); 10 | Field("created"); 11 | Field("edited"); 12 | Field("url"); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Types/Film.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.StarWars.Api; 2 | using GraphQL.Relay.Types; 3 | using GraphQL.Relay.Utilities; 4 | 5 | namespace GraphQL.Relay.StarWars.Types 6 | { 7 | public class FilmGraphType : AsyncNodeGraphType 8 | { 9 | private readonly Swapi _api; 10 | public FilmGraphType(Swapi api) 11 | { 12 | _api = api; 13 | 14 | Name = "Film"; 15 | 16 | Id(p => p.Id); 17 | Field(p => p.Title); 18 | Field(p => p.EpisodeId); 19 | Field(p => p.OpeningCrawl); 20 | Field(p => p.Director); 21 | Field(p => p.Producer); 22 | Field(p => p.ReleaseDate); 23 | 24 | Connection() 25 | .Name("characters") 26 | .ResolveAsync(async ctx => await api 27 | .GetMany(ctx.Source.Characters) 28 | .ContinueWith(t => ctx.ToConnection(t.Result)) 29 | ); 30 | 31 | Connection() 32 | .Name("planets") 33 | .ResolveAsync(async ctx => await api 34 | .GetMany(ctx.Source.Planets) 35 | .ContinueWith(t => ctx.ToConnection(t.Result)) 36 | ); 37 | 38 | Connection() 39 | .Name("species") 40 | .ResolveAsync(async ctx => await api 41 | .GetMany(ctx.Source.Species) 42 | .ContinueWith(t => ctx.ToConnection(t.Result)) 43 | ); 44 | 45 | Connection() 46 | .Name("starships") 47 | .ResolveAsync(async ctx => await api 48 | .GetMany(ctx.Source.Starships) 49 | .ContinueWith(t => ctx.ToConnection(t.Result)) 50 | ); 51 | 52 | Connection() 53 | .Name("vehicles") 54 | .ResolveAsync(async ctx => await api 55 | .GetMany(ctx.Source.Vehicles) 56 | .ContinueWith(t => ctx.ToConnection(t.Result)) 57 | ); 58 | } 59 | 60 | public override Task GetById(IResolveFieldContext context, string id) => 61 | _api.GetEntityAsync(id); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Types/People.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.StarWars.Api; 2 | using GraphQL.Relay.Types; 3 | using GraphQL.Relay.Utilities; 4 | 5 | namespace GraphQL.Relay.StarWars.Types 6 | { 7 | public class PeopleGraphType : NodeGraphType> 8 | { 9 | private readonly Swapi _api; 10 | 11 | public PeopleGraphType(Swapi api) 12 | { 13 | _api = api; 14 | 15 | Name = "People"; 16 | 17 | Id(p => p.Id); 18 | Field(p => p.Name); 19 | Field(p => p.Height); 20 | Field(p => p.Mass); 21 | Field(p => p.HairColor); 22 | Field(p => p.SkinColor); 23 | Field(p => p.EyeColor); 24 | Field(p => p.BirthYear); 25 | Field(p => p.Gender); 26 | Field("homeworld", typeof(PlanetGraphType)).ResolveAsync(async ctx => await _api.GetEntity(ctx.Source.Homeworld)); 27 | 28 | Connection() 29 | .Name("films") 30 | .ResolveAsync(async ctx => await api 31 | .GetMany(ctx.Source.Films) 32 | .ContinueWith(t => ctx.ToConnection(t.Result)) 33 | ); 34 | 35 | Connection() 36 | .Name("species") 37 | .ResolveAsync(async ctx => await api 38 | .GetMany(ctx.Source.Species) 39 | .ContinueWith(t => ctx.ToConnection(t.Result)) 40 | ); 41 | 42 | Connection() 43 | .Name("starships") 44 | .ResolveAsync(async ctx => await api 45 | .GetMany(ctx.Source.Starships) 46 | .ContinueWith(t => ctx.ToConnection(t.Result)) 47 | ); 48 | 49 | Connection() 50 | .Name("vehicles") 51 | .ResolveAsync(async ctx => await api 52 | .GetMany(ctx.Source.Vehicles) 53 | .ContinueWith(t => ctx.ToConnection(t.Result)) 54 | ); 55 | } 56 | 57 | public override Task GetById(IResolveFieldContext context, string id) => 58 | _api.GetEntityAsync(id); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Types/Planet.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.StarWars.Api; 2 | using GraphQL.Relay.Types; 3 | using GraphQL.Relay.Utilities; 4 | 5 | namespace GraphQL.Relay.StarWars.Types 6 | { 7 | public class PlanetGraphType : NodeGraphType> 8 | { 9 | private readonly Swapi _api; 10 | 11 | public PlanetGraphType(Swapi api) 12 | { 13 | _api = api; 14 | 15 | Name = "Planet"; 16 | 17 | Id(p => p.Id); 18 | Field(p => p.Name); 19 | Field(p => p.RotationPeriod); 20 | Field(p => p.OrbitalPeriod); 21 | Field(p => p.Diameter); 22 | Field(p => p.Climate); 23 | Field(p => p.Gravity); 24 | Field(p => p.Terrain); 25 | Field(p => p.SurfaceWater); 26 | Field(p => p.Population); 27 | 28 | Connection() 29 | .Name("residents") 30 | .ResolveAsync(async ctx => await api 31 | .GetMany(ctx.Source.Residents) 32 | .ContinueWith(t => ctx.ToConnection(t.Result)) 33 | ); 34 | 35 | Connection() 36 | .Name("films") 37 | .ResolveAsync(async ctx => await api 38 | .GetMany(ctx.Source.Films) 39 | .ContinueWith(t => ctx.ToConnection(t.Result)) 40 | ); 41 | } 42 | 43 | public override Task GetById(IResolveFieldContext context, string id) => 44 | _api.GetEntityAsync(id); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Types/Species.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.StarWars.Api; 2 | using GraphQL.Relay.Types; 3 | using GraphQL.Relay.Utilities; 4 | 5 | namespace GraphQL.Relay.StarWars.Types 6 | { 7 | public class SpeciesGraphType : NodeGraphType> 8 | { 9 | private readonly Swapi _api; 10 | 11 | public SpeciesGraphType(Swapi api) 12 | { 13 | _api = api; 14 | 15 | Name = "Species"; 16 | 17 | Id(p => p.Id); 18 | Field(p => p.Name); 19 | 20 | Field(p => p.Classification); 21 | Field(p => p.Designation); 22 | Field(p => p.AverageHeight); 23 | Field(p => p.SkinColors); 24 | Field(p => p.HairColors); 25 | Field(p => p.EyeColors); 26 | Field(p => p.AverageLifespan); 27 | Field(p => p.Language); 28 | Field("homeworld", typeof(PlanetGraphType)).ResolveAsync(async ctx => await _api.GetEntity(ctx.Source.Homeworld)); 29 | 30 | Connection() 31 | .Name("people") 32 | .ResolveAsync(async ctx => await api 33 | .GetMany(ctx.Source.People) 34 | .ContinueWith(t => ctx.ToConnection(t.Result)) 35 | ); 36 | 37 | Connection() 38 | .Name("films") 39 | .ResolveAsync(async ctx => await api 40 | .GetMany(ctx.Source.Films) 41 | .ContinueWith(t => ctx.ToConnection(t.Result)) 42 | ); 43 | } 44 | 45 | public override Task GetById(IResolveFieldContext context, string id) => 46 | _api.GetEntityAsync(id); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Types/StarWarsQuery.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.StarWars.Api; 2 | using GraphQL.Relay.StarWars.Utilities; 3 | using GraphQL.Relay.Types; 4 | 5 | namespace GraphQL.Relay.StarWars.Types 6 | { 7 | public class StarWarsQuery : QueryGraphType 8 | { 9 | public StarWarsQuery(Swapi api) 10 | { 11 | Name = "StarWarsQuery"; 12 | 13 | Connection() 14 | .Name("films") 15 | .ResolveApiConnection(api); 16 | 17 | Connection() 18 | .Name("people") 19 | .ResolveApiConnection(api); 20 | 21 | Connection() 22 | .Name("planets") 23 | .ResolveApiConnection(api); 24 | 25 | Connection() 26 | .Name("species") 27 | .ResolveApiConnection(api); 28 | 29 | Connection() 30 | .Name("starships") 31 | .ResolveApiConnection(api); 32 | 33 | Connection() 34 | .Name("vehicles") 35 | .ResolveApiConnection(api); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Types/StarWarsSchema.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Relay.StarWars.Types 4 | { 5 | public class StarWarsSchema : Schema 6 | { 7 | public StarWarsSchema(IServiceProvider provider) : base(provider) 8 | { 9 | Query = provider.GetService(); 10 | 11 | RegisterType(typeof(FilmGraphType)); 12 | RegisterType(typeof(PeopleGraphType)); 13 | RegisterType(typeof(PlanetGraphType)); 14 | RegisterType(typeof(SpeciesGraphType)); 15 | RegisterType(typeof(StarshipGraphType)); 16 | RegisterType(typeof(VehicleGraphType)); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Types/Starship.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.StarWars.Api; 2 | using GraphQL.Relay.Types; 3 | using GraphQL.Relay.Utilities; 4 | 5 | namespace GraphQL.Relay.StarWars.Types 6 | { 7 | public class StarshipGraphType : NodeGraphType> 8 | { 9 | private readonly Swapi _api; 10 | 11 | public StarshipGraphType(Swapi api) 12 | { 13 | _api = api; 14 | 15 | Name = "Starship"; 16 | 17 | Id(p => p.Id); 18 | Field(p => p.Name); 19 | Field(p => p.Model); 20 | Field(p => p.Manufacturer); 21 | Field(p => p.CostInCredits); 22 | Field(p => p.Length); 23 | Field(p => p.MaxAtmospheringSpeed); 24 | Field(p => p.Crew); 25 | Field(p => p.Passengers); 26 | Field(p => p.CargoCapacity); 27 | Field(p => p.Consumables); 28 | Field(p => p.HyperdriveRating); 29 | Field(p => p.MGLT); 30 | Field(p => p.StarshipClass); 31 | 32 | Connection() 33 | .Name("pilots") 34 | .ResolveAsync(async ctx => await api 35 | .GetMany(ctx.Source.Pilots) 36 | .ContinueWith(t => ctx.ToConnection(t.Result)) 37 | ); 38 | 39 | Connection() 40 | .Name("films") 41 | .ResolveAsync(async ctx => await api 42 | .GetMany(ctx.Source.Films) 43 | .ContinueWith(t => ctx.ToConnection(t.Result)) 44 | ); 45 | } 46 | 47 | public override Task GetById(IResolveFieldContext context, string id) => 48 | _api.GetEntityAsync(id); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Types/Vehicle.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.StarWars.Api; 2 | using GraphQL.Relay.Types; 3 | using GraphQL.Relay.Utilities; 4 | 5 | namespace GraphQL.Relay.StarWars.Types 6 | { 7 | public class VehicleGraphType : NodeGraphType> 8 | { 9 | private readonly Swapi _api; 10 | 11 | public VehicleGraphType(Swapi api) 12 | { 13 | _api = api; 14 | 15 | Name = "Vehicle"; 16 | 17 | Id(p => p.Id); 18 | Field(p => p.Name); 19 | Field(p => p.Model); 20 | Field(p => p.Manufacturer); 21 | Field(p => p.CostInCredits); 22 | Field(p => p.Length); 23 | Field(p => p.MaxAtmospheringSpeed); 24 | Field(p => p.Crew); 25 | Field(p => p.Passengers); 26 | Field(p => p.CargoCapacity); 27 | Field(p => p.VehicleClass); 28 | Field(p => p.Consumables); 29 | 30 | Connection() 31 | .Name("pilots") 32 | .ResolveAsync(async ctx => await api 33 | .GetMany(ctx.Source.Pilots) 34 | .ContinueWith(t => ctx.ToConnection(t.Result)) 35 | ); 36 | 37 | Connection() 38 | .Name("films") 39 | .ResolveAsync(async ctx => await api 40 | .GetMany(ctx.Source.Films) 41 | .ContinueWith(t => ctx.ToConnection(t.Result)) 42 | ); 43 | } 44 | 45 | public override Task GetById(IResolveFieldContext context, string id) => 46 | _api.GetEntityAsync(id); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Utilities/ConnectionBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Builders; 2 | using GraphQL.Relay.StarWars.Api; 3 | using GraphQL.Relay.Utilities; 4 | 5 | namespace GraphQL.Relay.StarWars.Utilities 6 | { 7 | public static class ConnectionBuilderExtensions 8 | { 9 | public static void ResolveApiConnection( 10 | this ConnectionBuilder builder, 11 | Swapi api 12 | ) where TEntity : Entity 13 | { 14 | builder 15 | .ResolveAsync(async ctx => await api 16 | .GetConnectionAsync(ctx.GetConnectionArguments()) 17 | .ContinueWith(t => ctx.ToConnection(t.Result.Entities, t.Result.TotalCount)) 18 | ); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Utilities/ConnectionResults.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Relay.StarWars.Utilities 2 | { 3 | public static class ConnectionEntities 4 | { 5 | public static ConnectionEntities Create( 6 | IEnumerable entities, 7 | int totalCount 8 | ) => new(entities, totalCount); 9 | } 10 | 11 | public class ConnectionEntities 12 | { 13 | public ConnectionEntities(IEnumerable entities, int totalCount) 14 | { 15 | Entities = entities; 16 | TotalCount = totalCount; 17 | } 18 | 19 | public int TotalCount { get; } 20 | public IEnumerable Entities { get; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/Utilities/ResolveConnectionContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Builders; 2 | 3 | namespace GraphQL.Relay.StarWars.Utilities 4 | { 5 | public class ConnectionArguments 6 | { 7 | public int? First { get; set; } 8 | public string After { get; set; } 9 | public int? Last { get; set; } 10 | public string Before { get; set; } 11 | } 12 | 13 | public static class ResolveConnectionContextExtensions 14 | { 15 | public static ConnectionArguments GetConnectionArguments(this IResolveConnectionContext ctx) 16 | { 17 | return new ConnectionArguments 18 | { 19 | First = ctx.First, 20 | After = ctx.After, 21 | Last = ctx.Last, 22 | Before = ctx.Before, 23 | }; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.StarWars/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Warning" 7 | } 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Warning" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Test/Fixtures/DatabaseFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using GraphQL.Relay.Test.Fixtures.Models; 3 | using Microsoft.Data.Sqlite; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace GraphQL.Relay.Test.Fixtures 7 | { 8 | public partial class DatabaseFixture : IDisposable 9 | { 10 | private readonly DbConnection _connection; 11 | private readonly DbContextOptions _contextOptions; 12 | 13 | public DatabaseFixture() 14 | { 15 | // Create and open a connection. This creates the SQLite in-memory database, which will persist until the connection is closed 16 | // at the end of the test (see Dispose below). 17 | _connection = new SqliteConnection("Filename=:memory:"); 18 | _connection.Open(); 19 | 20 | // These options will be used by the context instances in this test suite, including the connection opened above. 21 | _contextOptions = new DbContextOptionsBuilder() 22 | .UseSqlite(_connection) 23 | .Options; 24 | 25 | // Create the schema and seed some data 26 | Context = new(_contextOptions); 27 | 28 | _ = Context.Database.EnsureCreated(); 29 | 30 | var blogs = Enumerable.Range(1, TotalCount).Select(i => new Blog 31 | { 32 | Name = $"Blog{i}", 33 | Url = $"http://sample.com/{i}" 34 | }); 35 | 36 | Context.AddRange(blogs); 37 | 38 | _ = Context.SaveChanges(); 39 | } 40 | 41 | public void Dispose() 42 | { 43 | // Remove all records from the database 44 | Context.RemoveRange(Context.Blogs); 45 | _ = Context.SaveChanges(); 46 | 47 | Context.Dispose(); 48 | _connection.Dispose(); 49 | GC.SuppressFinalize(this); 50 | } 51 | 52 | public int TotalCount => 1000; 53 | 54 | public BloggingContext Context { get; } 55 | 56 | public class BloggingContext : DbContext 57 | { 58 | public BloggingContext(DbContextOptions options) : base(options) { } 59 | 60 | public DbSet Blogs => Set(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Test/Fixtures/EnumerableFixture.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.Test.Fixtures.Models; 2 | 3 | namespace GraphQL.Relay.Test.Fixtures 4 | { 5 | public partial class EnumerableFixture : IDisposable 6 | { 7 | private readonly List _blogs = new(); 8 | 9 | public EnumerableFixture() 10 | { 11 | var blogs = Enumerable.Range(1, TotalCount).Select(i => new Blog 12 | { 13 | Name = $"Blog{i}", 14 | Url = $"http://sample.com/{i}" 15 | }); 16 | 17 | _blogs.AddRange(blogs); 18 | } 19 | 20 | public void Dispose() 21 | { 22 | _blogs.Clear(); 23 | GC.SuppressFinalize(this); 24 | } 25 | 26 | public int TotalCount => 1000; 27 | 28 | public IList Blogs => _blogs; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Test/Fixtures/Models/Blog.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Relay.Test.Fixtures.Models 2 | { 3 | public class Blog 4 | { 5 | public int BlogId { get; set; } 6 | public string Name { get; set; } 7 | public string Url { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Test/GraphQL.Relay.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net7 6 | 7.0.2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Test/NodeTypeTests.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.Types; 2 | 3 | namespace GraphQL.Relay.Tests.Types 4 | { 5 | public class NodeTypeTests 6 | { 7 | public class Droid 8 | { 9 | public string Id { get; set; } 10 | public string Name { get; set; } 11 | } 12 | 13 | public abstract class DroidType : NodeGraphType 14 | { 15 | public DroidType() 16 | { 17 | Name = "Droid"; 18 | Id(d => d.Id); 19 | } 20 | } 21 | 22 | public class DroidType : DroidType 23 | { 24 | public override Droid GetById(IResolveFieldContext context, string id) 25 | { 26 | return new Droid { Id = id, Name = "text" }; 27 | } 28 | } 29 | 30 | public class DroidTypeAsync : DroidType> 31 | { 32 | public override async Task GetById(IResolveFieldContext context, string id) 33 | { 34 | await Task.Delay(0); 35 | return new Droid { Id = id, Name = "text" }; 36 | } 37 | } 38 | 39 | [Fact] 40 | public void it_should_handle_id_conflict() 41 | { 42 | var type = new DroidType(); 43 | 44 | type.Fields.Count().ShouldBe(2); 45 | type.HasField("id").ShouldBeTrue(); 46 | type.HasField("droidId").ShouldBeTrue(); 47 | } 48 | 49 | [Fact] 50 | public async void it_should_allow_async() 51 | { 52 | var type = new DroidTypeAsync(); 53 | 54 | var droid = await type.GetById(null, "3"); 55 | droid.Id.ShouldBe("3"); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Test/Types/IncompleteSliceExceptionTests.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.Types; 2 | 3 | namespace GraphQL.Relay.Test.Types 4 | { 5 | public class IncompleteSliceExceptionTests 6 | { 7 | [Fact] 8 | public void Message_WhenNoMessageWasPassedToCtor_ReturnsDefaultMessage() 9 | { 10 | var ex = new IncompleteSliceException(); 11 | Assert.Equal("The provided data slice does not contain all expected items.", ex.Message); 12 | } 13 | 14 | [Fact] 15 | public void Message_WhenMessageIsProvidedInCtor_ReturnsPassedMessage() 16 | { 17 | var ex = new IncompleteSliceException("Test"); 18 | Assert.Equal("Test", ex.Message); 19 | ex = new IncompleteSliceException("Test", "paramName"); 20 | Assert.StartsWith($"Test", ex.Message); 21 | Assert.Equal("paramName", ex.ParamName); 22 | } 23 | 24 | [Fact] 25 | public void InnerException_WhenInnerExceptionIsPassedToCtor_ReturnsPassedException() 26 | { 27 | var inner = new Exception(); 28 | var ex = new IncompleteSliceException("Test", inner); 29 | Assert.Equal("Test", ex.Message); 30 | Assert.Equal(inner, ex.InnerException); 31 | 32 | ex = new IncompleteSliceException("Test", "paramName", inner); 33 | Assert.StartsWith($"Test", ex.Message); 34 | Assert.Equal("paramName", ex.ParamName); 35 | Assert.Equal(inner, ex.InnerException); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Test/Types/QueryGraphTypeTests.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.Types; 2 | using GraphQL.SystemTextJson; 3 | using GraphQL.Types; 4 | 5 | namespace GraphQL.Relay.Test.Types 6 | { 7 | public class SimpleData 8 | { 9 | public string Id { get; set; } 10 | 11 | public static IEnumerable GetData() => new List { 12 | new SimpleData { Id = "1" }, 13 | new SimpleData { Id = "Banana" }, 14 | new SimpleData { Id = "71dfed33-67a8-4546-bb62-e1989bb99652" }, 15 | new SimpleData { Id = "c17b1a3c-de5c-44a7-b368-f99bd320f508" } 16 | }; 17 | } 18 | 19 | public class SimpleNodeGraphType : NodeGraphType 20 | { 21 | public SimpleNodeGraphType() : base() 22 | { 23 | Name = "SimpleNode"; 24 | 25 | Id(x => x.Id); 26 | } 27 | 28 | public override SimpleData GetById(IResolveFieldContext context, string id) => SimpleData 29 | .GetData() 30 | .FirstOrDefault(x => x.Id.Equals(id)); 31 | 32 | } 33 | 34 | public class SimpleSchema : Schema 35 | { 36 | public SimpleSchema() 37 | { 38 | Query = new QueryGraphType(); 39 | RegisterType(new SimpleNodeGraphType()); 40 | } 41 | } 42 | 43 | public class QueryGraphTypeTests 44 | { 45 | public Schema Schema { get; } 46 | 47 | public DocumentExecuter Executer { get; } 48 | 49 | public IGraphQLTextSerializer Serializer { get; } 50 | 51 | public QueryGraphTypeTests() 52 | { 53 | Schema = new SimpleSchema(); 54 | Executer = new DocumentExecuter(); 55 | Serializer = new GraphQLSerializer(); 56 | } 57 | 58 | /// 59 | /// Tests the implementation of the "node" query 60 | /// Tests the arguments and naming 61 | /// 62 | [Fact] 63 | public void Node_ShouldImplementNodeQuery() 64 | { 65 | var type = new QueryGraphType(); 66 | 67 | // should have node query 68 | type.HasField("node").ShouldBeTrue(); 69 | // query has one argument 70 | type.GetField("node").Arguments.Count.ShouldBe(1); 71 | // query has argument 72 | type.GetField("node").Arguments[0].Name.ShouldBe("id"); 73 | type.GetField("node").Arguments[0].Type.ShouldBe(typeof(NonNullGraphType)); 74 | } 75 | 76 | /// 77 | /// Tests the default name 78 | /// 79 | [Fact] 80 | public void Name_ShouldSetDefaultName() 81 | { 82 | var type = new QueryGraphType(); 83 | 84 | type.Name.ShouldNotBeEmpty(); 85 | type.Name.ShouldBe("Query"); 86 | } 87 | 88 | /// 89 | /// Tests the "node" query 90 | /// Should return a node by it's global ID 91 | /// 92 | /// 93 | [Theory] 94 | [InlineData( 95 | @"{ node(id: ""U2ltcGxlTm9kZTox"") { id } }", 96 | "{\"data\":{\"node\":{\"id\":\"U2ltcGxlTm9kZTox\"}}}" 97 | )] 98 | [InlineData( 99 | @"{ node(id: ""U2ltcGxlTm9kZTpCYW5hbmE="") { id } }", 100 | "{\"data\":{\"node\":{\"id\":\"U2ltcGxlTm9kZTpCYW5hbmE=\"}}}" 101 | )] 102 | [InlineData( 103 | @"{ node(id: ""U2ltcGxlTm9kZTo3MWRmZWQzMy02N2E4LTQ1NDYtYmI2Mi1lMTk4OWJiOTk2NTI="") { id } }", 104 | "{\"data\":{\"node\":{\"id\":\"U2ltcGxlTm9kZTo3MWRmZWQzMy02N2E4LTQ1NDYtYmI2Mi1lMTk4OWJiOTk2NTI=\"}}}" 105 | )] 106 | [InlineData( 107 | @"{ node(id: ""U2ltcGxlTm9kZTpjMTdiMWEzYy1kZTVjLTQ0YTctYjM2OC1mOTliZDMyMGY1MDg="") { id } }", 108 | "{\"data\":{\"node\":{\"id\":\"U2ltcGxlTm9kZTpjMTdiMWEzYy1kZTVjLTQ0YTctYjM2OC1mOTliZDMyMGY1MDg=\"}}}" 109 | )] 110 | public async Task NodeQuery_ShouldReturnNodeForId(string query, string expected) 111 | { 112 | var result = await Executer.ExecuteAsync(options => 113 | { 114 | options.Schema = Schema; 115 | options.Query = query; 116 | }); 117 | 118 | var writtenResult = Serializer.Serialize(result); 119 | writtenResult.ShouldBe(expected); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Test/Types/ResolveConnectionContextFactory.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Builders; 2 | using GraphQL.Execution; 3 | 4 | namespace GraphQL.Relay.Test.Types 5 | { 6 | public class ResolveConnectionContextFactory 7 | { 8 | public static ResolveConnectionContext CreateContext( 9 | int? first = null, 10 | string after = null, 11 | int? last = null, 12 | string before = null, 13 | int? defaultPageSize = null 14 | ) => CreateContext(first, after, last, before, defaultPageSize); 15 | 16 | public static ResolveConnectionContext CreateContext(int? first = null, string after = null, 17 | int? last = null, string before = null, int? defaultPageSize = null) => new( 18 | new ResolveFieldContext() { }, 19 | true, 20 | defaultPageSize 21 | ) 22 | { 23 | Arguments = new Dictionary 24 | { 25 | ["first"] = new ArgumentValue(first, ArgumentSource.Variable), 26 | ["last"] = new ArgumentValue(last, ArgumentSource.Variable), 27 | ["after"] = new ArgumentValue(after, ArgumentSource.Variable), 28 | ["before"] = new ArgumentValue(before, ArgumentSource.Variable) 29 | } 30 | }; 31 | 32 | public class TestParent 33 | { 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Test/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Relay.Test 2 | { 3 | public class UnitTest1 4 | { 5 | [Fact] 6 | public void Test1() 7 | { 8 | 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Test/Utilities/EdgeRangeTests.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.Utilities; 2 | 3 | namespace GraphQL.Relay.Test.Utilities 4 | { 5 | public class EdgeRangeTests 6 | { 7 | [Fact] 8 | public void Ctor_WhenStartOffsetIsNegative_ThrowsArgumentOutOfRangeException() 9 | { 10 | Assert.Throws(() => new EdgeRange(-1, 0)); 11 | } 12 | 13 | [Fact] 14 | public void Ctor_WhenEndOffsetIsLessThanMinusOne_ThrowsArgumentOutOfRangeException() 15 | { 16 | Assert.Throws(() => new EdgeRange(0, -2)); 17 | } 18 | 19 | [Theory] 20 | [InlineData(3)] 21 | [InlineData(6)] 22 | public void Count_WhenStartEqualsEnd_Returns1(int offset) 23 | { 24 | var range = new EdgeRange(offset, offset); 25 | Assert.Equal(1, range.Count); 26 | } 27 | 28 | [Theory] 29 | [InlineData(10, 9)] 30 | [InlineData(10, 8)] 31 | public void Ctor_WhenEndOffsetIsLessThanStartOffset_ClampsEndOffsetToOneLessThanStart(int start, int end) 32 | { 33 | var range = new EdgeRange(start, end); 34 | Assert.Equal(start, range.StartOffset); 35 | Assert.Equal(start - 1, range.EndOffset); 36 | Assert.Equal(0, range.Count); 37 | Assert.True(range.IsEmpty); 38 | } 39 | 40 | [Fact] 41 | public void LimitCountFromStart_IfMaxLengthIsNegative_ThrowsException() 42 | { 43 | var range = new EdgeRange(0, 10); 44 | Assert.Throws(() => range.LimitCountFromStart(-1)); 45 | } 46 | 47 | [Fact] 48 | public void LimitCountFromStart_WhenProvidingMaxLengthLessThanCount_MovesEndOffet() 49 | { 50 | var range = new EdgeRange(0, 9); 51 | Assert.Equal(10, range.Count); 52 | 53 | range.LimitCountFromStart(5); 54 | 55 | Assert.Equal(5, range.Count); 56 | Assert.Equal(4, range.EndOffset); 57 | Assert.Equal(0, range.StartOffset); 58 | } 59 | 60 | [Fact] 61 | public void LimitCountToEnd_IfMaxLengthIsNegative_ThrowsException() 62 | { 63 | var range = new EdgeRange(0, 10); 64 | Assert.Throws(() => range.LimitCountToEnd(-1)); 65 | } 66 | 67 | [Fact] 68 | public void LimitCountToEnd_WhenProvidingMaxLengthLessThanCount_MovesStartOffet() 69 | { 70 | var range = new EdgeRange(0, 9); 71 | Assert.Equal(10, range.Count); 72 | 73 | range.LimitCountToEnd(5); 74 | 75 | Assert.Equal(5, range.Count); 76 | Assert.Equal(9, range.EndOffset); 77 | Assert.Equal(5, range.StartOffset); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Test/Utilities/EnumerableExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Relay.Test.Utilities 2 | { 3 | public class EnumerableExtensionsTests 4 | { 5 | [Fact] 6 | public void Slice_IfStartAndEndAreLessThanZero_ReturnsEnumerableStartingAtTheEnd() 7 | { 8 | var source = new[] { 1, 2, 3, 4, 5 }; 9 | var slice = Relay.Utilities.EnumerableExtensions 10 | .Slice(source, -2, -1) 11 | .ToList(); 12 | 13 | Assert.Equal(source[^2], slice[0]); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Test/Utilities/StringExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GraphQL.Relay.Test.Utilities 3 | { 4 | public class StringExtensionsTests 5 | { 6 | [Fact] 7 | public void Base64Encode_IfValueIsNull_ReturnsNull() 8 | { 9 | var encoded = Relay.Utilities.StringExtensions.Base64Encode(null); 10 | Assert.Null(encoded); 11 | } 12 | 13 | [Fact] 14 | public void Base64Decode_IfValueIsNull_ReturnsNull() 15 | { 16 | var decoded = Relay.Utilities.StringExtensions.Base64Decode(null); 17 | Assert.Null(decoded); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "jason" 4 | ], 5 | "plugins": [ 6 | "relay" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | import 'todomvc-common'; 14 | import './css/base.css'; 15 | import './css/index.css'; 16 | 17 | import React from 'react'; 18 | import ReactDOM from 'react-dom'; 19 | 20 | import { 21 | QueryRenderer, 22 | graphql, 23 | } from 'react-relay'; 24 | import { 25 | Environment, 26 | Network, 27 | RecordSource, 28 | Store, 29 | } from 'relay-runtime'; 30 | 31 | import TodoApp from './components/TodoApp'; 32 | 33 | 34 | const mountNode = document.getElementById('root'); 35 | 36 | function fetchQuery( 37 | operation, 38 | variables, 39 | ) { 40 | return fetch('/graphql', { 41 | method: 'POST', 42 | headers: { 43 | 'Content-Type': 'application/json', 44 | }, 45 | body: JSON.stringify({ 46 | query: operation.text, 47 | variables, 48 | }), 49 | }).then(response => { 50 | return response.json(); 51 | }); 52 | } 53 | 54 | const modernEnvironment = new Environment({ 55 | network: Network.create(fetchQuery), 56 | store: new Store(new RecordSource()), 57 | }); 58 | 59 | ReactDOM.render( 60 | { 71 | if (props) { 72 | return ; 73 | } else { 74 | return
Loading
; 75 | } 76 | }} 77 | />, 78 | mountNode 79 | ); 80 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/components/Todo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | import ChangeTodoStatusMutation from '../mutations/ChangeTodoStatusMutation' 14 | import RemoveTodoMutation from '../mutations/RemoveTodoMutation' 15 | import RenameTodoMutation from '../mutations/RenameTodoMutation' 16 | import TodoTextInput from './TodoTextInput' 17 | 18 | import React from 'react' 19 | import { createFragmentContainer, graphql } from 'react-relay' 20 | import classnames from 'classnames' 21 | 22 | class Todo extends React.Component { 23 | state = { 24 | isEditing: false, 25 | } 26 | _handleCompleteChange = e => { 27 | const complete = e.target.checked 28 | ChangeTodoStatusMutation.commit( 29 | this.props.relay.environment, 30 | complete, 31 | this.props.todo, 32 | this.props.viewer 33 | ) 34 | } 35 | _handleDestroyClick = () => { 36 | this._removeTodo() 37 | } 38 | _handleLabelDoubleClick = () => { 39 | this._setEditMode(true) 40 | } 41 | _handleTextInputCancel = () => { 42 | this._setEditMode(false) 43 | } 44 | _handleTextInputDelete = () => { 45 | this._setEditMode(false) 46 | this._removeTodo() 47 | } 48 | _handleTextInputSave = text => { 49 | this._setEditMode(false) 50 | RenameTodoMutation.commit( 51 | this.props.relay.environment, 52 | text, 53 | this.props.todo 54 | ) 55 | } 56 | _removeTodo() { 57 | RemoveTodoMutation.commit( 58 | this.props.relay.environment, 59 | this.props.todo, 60 | this.props.viewer 61 | ) 62 | } 63 | _setEditMode = shouldEdit => { 64 | this.setState({ isEditing: shouldEdit }) 65 | } 66 | renderTextInput() { 67 | return ( 68 | 76 | ) 77 | } 78 | render() { 79 | return ( 80 |
  • 86 |
    87 | 93 | 96 |
    98 | {this.state.isEditing && this.renderTextInput()} 99 |
  • 100 | ) 101 | } 102 | } 103 | 104 | export default createFragmentContainer(Todo, { 105 | todo: graphql` 106 | fragment Todo_todo on Todo { 107 | id 108 | complete 109 | text 110 | } 111 | `, 112 | viewer: graphql` 113 | fragment Todo_viewer on User { 114 | id 115 | totalCount 116 | completedCount 117 | } 118 | `, 119 | }) 120 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/components/TodoApp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | import AddTodoMutation from '../mutations/AddTodoMutation'; 14 | import TodoList from './TodoList'; 15 | import TodoListFooter from './TodoListFooter'; 16 | import TodoTextInput from './TodoTextInput'; 17 | 18 | import React from 'react'; 19 | import { 20 | createFragmentContainer, 21 | graphql, 22 | } from 'react-relay'; 23 | 24 | class TodoApp extends React.Component { 25 | _handleTextInputSave = (text) => { 26 | AddTodoMutation.commit( 27 | this.props.relay.environment, 28 | text, 29 | this.props.viewer, 30 | ); 31 | }; 32 | render() { 33 | const hasTodos = this.props.viewer.totalCount > 0; 34 | return ( 35 |
    36 |
    37 |
    38 |

    39 | todos 40 |

    41 | 47 |
    48 | 49 | {hasTodos && 50 | 54 | } 55 |
    56 | 69 |
    70 | ); 71 | } 72 | } 73 | 74 | export default createFragmentContainer(TodoApp, { 75 | viewer: graphql` 76 | fragment TodoApp_viewer on User { 77 | id, 78 | totalCount, 79 | ...TodoListFooter_viewer, 80 | ...TodoList_viewer, 81 | } 82 | `, 83 | }); 84 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/components/TodoList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | import MarkAllTodosMutation from '../mutations/MarkAllTodosMutation' 14 | import Todo from './Todo' 15 | 16 | import React from 'react' 17 | import { createFragmentContainer, graphql } from 'react-relay' 18 | 19 | class TodoList extends React.Component { 20 | _handleMarkAllChange = e => { 21 | const complete = e.target.checked 22 | MarkAllTodosMutation.commit( 23 | this.props.relay.environment, 24 | complete, 25 | this.props.viewer.todos, 26 | this.props.viewer 27 | ) 28 | } 29 | 30 | renderTodos() { 31 | return this.props.viewer.todos.edges.map(edge => ( 32 | 37 | )) 38 | } 39 | 40 | render() { 41 | const numTodos = this.props.viewer.totalCount 42 | const numCompletedTodos = this.props.viewer.completedCount 43 | return ( 44 |
    45 | 51 | 52 |
      {this.renderTodos()}
    53 |
    54 | ) 55 | } 56 | } 57 | 58 | export default createFragmentContainer(TodoList, { 59 | viewer: graphql` 60 | fragment TodoList_viewer on User { 61 | todos( 62 | first: 2147483647 # max GraphQLInt 63 | ) @connection(key: "TodoList_todos") { 64 | edges { 65 | node { 66 | id 67 | complete 68 | ...Todo_todo 69 | } 70 | } 71 | } 72 | id 73 | totalCount 74 | completedCount 75 | ...Todo_viewer 76 | } 77 | `, 78 | }) 79 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/components/TodoListFooter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | import RemoveCompletedTodosMutation from '../mutations/RemoveCompletedTodosMutation'; 14 | 15 | import React from 'react'; 16 | import { 17 | graphql, 18 | createFragmentContainer, 19 | } from 'react-relay'; 20 | 21 | class TodoListFooter extends React.Component { 22 | _handleRemoveCompletedTodosClick = () => { 23 | RemoveCompletedTodosMutation.commit( 24 | this.props.relay.environment, 25 | this.props.viewer.completedTodos, 26 | this.props.viewer, 27 | ); 28 | }; 29 | render() { 30 | const numCompletedTodos = this.props.viewer.completedCount; 31 | const numRemainingTodos = this.props.viewer.totalCount - numCompletedTodos; 32 | return ( 33 |
    34 | 35 | {numRemainingTodos} item{numRemainingTodos === 1 ? '' : 's'} left 36 | 37 | {numCompletedTodos > 0 && 38 | 43 | } 44 |
    45 | ); 46 | } 47 | } 48 | 49 | export default createFragmentContainer( 50 | TodoListFooter, 51 | graphql` 52 | fragment TodoListFooter_viewer on User { 53 | id, 54 | completedCount, 55 | completedTodos: todos( 56 | status: "completed", 57 | first: 2147483647 # max GraphQLInt 58 | ) { 59 | edges { 60 | node { 61 | id 62 | complete 63 | } 64 | } 65 | }, 66 | totalCount, 67 | } 68 | ` 69 | ); 70 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/components/TodoTextInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | import React from 'react'; 14 | import ReactDOM from 'react-dom'; 15 | 16 | const PropTypes = require('prop-types'); 17 | 18 | const ENTER_KEY_CODE = 13; 19 | const ESC_KEY_CODE = 27; 20 | 21 | export default class TodoTextInput extends React.Component { 22 | static defaultProps = { 23 | commitOnBlur: false, 24 | }; 25 | static propTypes = { 26 | className: PropTypes.string, 27 | commitOnBlur: PropTypes.bool.isRequired, 28 | initialValue: PropTypes.string, 29 | onCancel: PropTypes.func, 30 | onDelete: PropTypes.func, 31 | onSave: PropTypes.func.isRequired, 32 | placeholder: PropTypes.string, 33 | }; 34 | state = { 35 | isEditing: false, 36 | text: this.props.initialValue || '', 37 | }; 38 | componentDidMount() { 39 | ReactDOM.findDOMNode(this).focus(); 40 | } 41 | _commitChanges = () => { 42 | const newText = this.state.text.trim(); 43 | if (this.props.onDelete && newText === '') { 44 | this.props.onDelete(); 45 | } else if (this.props.onCancel && newText === this.props.initialValue) { 46 | this.props.onCancel(); 47 | } else if (newText !== '') { 48 | this.props.onSave(newText); 49 | this.setState({text: ''}); 50 | } 51 | }; 52 | _handleBlur = () => { 53 | if (this.props.commitOnBlur) { 54 | this._commitChanges(); 55 | } 56 | }; 57 | _handleChange = (e) => { 58 | this.setState({text: e.target.value}); 59 | }; 60 | _handleKeyDown = (e) => { 61 | if (this.props.onCancel && e.keyCode === ESC_KEY_CODE) { 62 | this.props.onCancel(); 63 | } else if (e.keyCode === ENTER_KEY_CODE) { 64 | this._commitChanges(); 65 | } 66 | }; 67 | render() { 68 | return ( 69 | 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/components/__generated__/TodoApp_viewer.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type TodoApp_viewer = {| 12 | +id: string; 13 | +totalCount: ?number; 14 | |}; 15 | */ 16 | 17 | 18 | const fragment /*: ConcreteFragment*/ = { 19 | "argumentDefinitions": [], 20 | "kind": "Fragment", 21 | "metadata": null, 22 | "name": "TodoApp_viewer", 23 | "selections": [ 24 | { 25 | "kind": "ScalarField", 26 | "alias": null, 27 | "args": null, 28 | "name": "id", 29 | "storageKey": null 30 | }, 31 | { 32 | "kind": "ScalarField", 33 | "alias": null, 34 | "args": null, 35 | "name": "totalCount", 36 | "storageKey": null 37 | }, 38 | { 39 | "kind": "FragmentSpread", 40 | "name": "TodoListFooter_viewer", 41 | "args": null 42 | }, 43 | { 44 | "kind": "FragmentSpread", 45 | "name": "TodoList_viewer", 46 | "args": null 47 | } 48 | ], 49 | "type": "User" 50 | }; 51 | 52 | module.exports = fragment; 53 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/components/__generated__/TodoListFooter_viewer.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type TodoListFooter_viewer = {| 12 | +id: string; 13 | +completedCount: ?number; 14 | +completedTodos: ?{| 15 | +edges: ?$ReadOnlyArray; 21 | |}; 22 | +totalCount: ?number; 23 | |}; 24 | */ 25 | 26 | 27 | const fragment /*: ConcreteFragment*/ = { 28 | "argumentDefinitions": [], 29 | "kind": "Fragment", 30 | "metadata": null, 31 | "name": "TodoListFooter_viewer", 32 | "selections": [ 33 | { 34 | "kind": "ScalarField", 35 | "alias": null, 36 | "args": null, 37 | "name": "id", 38 | "storageKey": null 39 | }, 40 | { 41 | "kind": "ScalarField", 42 | "alias": null, 43 | "args": null, 44 | "name": "completedCount", 45 | "storageKey": null 46 | }, 47 | { 48 | "kind": "LinkedField", 49 | "alias": "completedTodos", 50 | "args": [ 51 | { 52 | "kind": "Literal", 53 | "name": "first", 54 | "value": 2147483647, 55 | "type": "Int" 56 | }, 57 | { 58 | "kind": "Literal", 59 | "name": "status", 60 | "value": "completed", 61 | "type": "String" 62 | } 63 | ], 64 | "concreteType": "TodoConnection", 65 | "name": "todos", 66 | "plural": false, 67 | "selections": [ 68 | { 69 | "kind": "LinkedField", 70 | "alias": null, 71 | "args": null, 72 | "concreteType": "TodoEdge", 73 | "name": "edges", 74 | "plural": true, 75 | "selections": [ 76 | { 77 | "kind": "LinkedField", 78 | "alias": null, 79 | "args": null, 80 | "concreteType": "Todo", 81 | "name": "node", 82 | "plural": false, 83 | "selections": [ 84 | { 85 | "kind": "ScalarField", 86 | "alias": null, 87 | "args": null, 88 | "name": "id", 89 | "storageKey": null 90 | }, 91 | { 92 | "kind": "ScalarField", 93 | "alias": null, 94 | "args": null, 95 | "name": "complete", 96 | "storageKey": null 97 | } 98 | ], 99 | "storageKey": null 100 | } 101 | ], 102 | "storageKey": null 103 | } 104 | ], 105 | "storageKey": "todos{\"first\":2147483647,\"status\":\"completed\"}" 106 | }, 107 | { 108 | "kind": "ScalarField", 109 | "alias": null, 110 | "args": null, 111 | "name": "totalCount", 112 | "storageKey": null 113 | } 114 | ], 115 | "type": "User" 116 | }; 117 | 118 | module.exports = fragment; 119 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/components/__generated__/TodoList_viewer.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type TodoList_viewer = {| 12 | +todos: ?{| 13 | +edges: ?$ReadOnlyArray; 19 | |}; 20 | +id: string; 21 | +totalCount: ?number; 22 | +completedCount: ?number; 23 | |}; 24 | */ 25 | 26 | 27 | const fragment /*: ConcreteFragment*/ = { 28 | "argumentDefinitions": [], 29 | "kind": "Fragment", 30 | "metadata": { 31 | "connection": [ 32 | { 33 | "count": null, 34 | "cursor": null, 35 | "direction": "forward", 36 | "path": [ 37 | "todos" 38 | ] 39 | } 40 | ] 41 | }, 42 | "name": "TodoList_viewer", 43 | "selections": [ 44 | { 45 | "kind": "LinkedField", 46 | "alias": "todos", 47 | "args": null, 48 | "concreteType": "TodoConnection", 49 | "name": "__TodoList_todos_connection", 50 | "plural": false, 51 | "selections": [ 52 | { 53 | "kind": "LinkedField", 54 | "alias": null, 55 | "args": null, 56 | "concreteType": "TodoEdge", 57 | "name": "edges", 58 | "plural": true, 59 | "selections": [ 60 | { 61 | "kind": "LinkedField", 62 | "alias": null, 63 | "args": null, 64 | "concreteType": "Todo", 65 | "name": "node", 66 | "plural": false, 67 | "selections": [ 68 | { 69 | "kind": "ScalarField", 70 | "alias": null, 71 | "args": null, 72 | "name": "id", 73 | "storageKey": null 74 | }, 75 | { 76 | "kind": "ScalarField", 77 | "alias": null, 78 | "args": null, 79 | "name": "complete", 80 | "storageKey": null 81 | }, 82 | { 83 | "kind": "FragmentSpread", 84 | "name": "Todo_todo", 85 | "args": null 86 | }, 87 | { 88 | "kind": "ScalarField", 89 | "alias": null, 90 | "args": null, 91 | "name": "__typename", 92 | "storageKey": null 93 | } 94 | ], 95 | "storageKey": null 96 | }, 97 | { 98 | "kind": "ScalarField", 99 | "alias": null, 100 | "args": null, 101 | "name": "cursor", 102 | "storageKey": null 103 | } 104 | ], 105 | "storageKey": null 106 | }, 107 | { 108 | "kind": "LinkedField", 109 | "alias": null, 110 | "args": null, 111 | "concreteType": "PageInfo", 112 | "name": "pageInfo", 113 | "plural": false, 114 | "selections": [ 115 | { 116 | "kind": "ScalarField", 117 | "alias": null, 118 | "args": null, 119 | "name": "endCursor", 120 | "storageKey": null 121 | }, 122 | { 123 | "kind": "ScalarField", 124 | "alias": null, 125 | "args": null, 126 | "name": "hasNextPage", 127 | "storageKey": null 128 | } 129 | ], 130 | "storageKey": null 131 | } 132 | ], 133 | "storageKey": null 134 | }, 135 | { 136 | "kind": "ScalarField", 137 | "alias": null, 138 | "args": null, 139 | "name": "id", 140 | "storageKey": null 141 | }, 142 | { 143 | "kind": "ScalarField", 144 | "alias": null, 145 | "args": null, 146 | "name": "totalCount", 147 | "storageKey": null 148 | }, 149 | { 150 | "kind": "ScalarField", 151 | "alias": null, 152 | "args": null, 153 | "name": "completedCount", 154 | "storageKey": null 155 | }, 156 | { 157 | "kind": "FragmentSpread", 158 | "name": "Todo_viewer", 159 | "args": null 160 | } 161 | ], 162 | "type": "User" 163 | }; 164 | 165 | module.exports = fragment; 166 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/components/__generated__/Todo_todo.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type Todo_todo = {| 12 | +id: string; 13 | +complete: boolean; 14 | +text: string; 15 | |}; 16 | */ 17 | 18 | 19 | const fragment /*: ConcreteFragment*/ = { 20 | "argumentDefinitions": [], 21 | "kind": "Fragment", 22 | "metadata": null, 23 | "name": "Todo_todo", 24 | "selections": [ 25 | { 26 | "kind": "ScalarField", 27 | "alias": null, 28 | "args": null, 29 | "name": "id", 30 | "storageKey": null 31 | }, 32 | { 33 | "kind": "ScalarField", 34 | "alias": null, 35 | "args": null, 36 | "name": "complete", 37 | "storageKey": null 38 | }, 39 | { 40 | "kind": "ScalarField", 41 | "alias": null, 42 | "args": null, 43 | "name": "text", 44 | "storageKey": null 45 | } 46 | ], 47 | "type": "Todo" 48 | }; 49 | 50 | module.exports = fragment; 51 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/components/__generated__/Todo_viewer.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | */ 4 | 5 | /* eslint-disable */ 6 | 7 | 'use strict'; 8 | 9 | /*:: 10 | import type {ConcreteFragment} from 'relay-runtime'; 11 | export type Todo_viewer = {| 12 | +id: string; 13 | +totalCount: ?number; 14 | +completedCount: ?number; 15 | |}; 16 | */ 17 | 18 | 19 | const fragment /*: ConcreteFragment*/ = { 20 | "argumentDefinitions": [], 21 | "kind": "Fragment", 22 | "metadata": null, 23 | "name": "Todo_viewer", 24 | "selections": [ 25 | { 26 | "kind": "ScalarField", 27 | "alias": null, 28 | "args": null, 29 | "name": "id", 30 | "storageKey": null 31 | }, 32 | { 33 | "kind": "ScalarField", 34 | "alias": null, 35 | "args": null, 36 | "name": "totalCount", 37 | "storageKey": null 38 | }, 39 | { 40 | "kind": "ScalarField", 41 | "alias": null, 42 | "args": null, 43 | "name": "completedCount", 44 | "storageKey": null 45 | } 46 | ], 47 | "type": "User" 48 | }; 49 | 50 | module.exports = fragment; 51 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/css/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/mutations/AddTodoMutation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | import { 14 | commitMutation, 15 | graphql, 16 | } from 'react-relay'; 17 | import {ConnectionHandler} from 'relay-runtime'; 18 | 19 | const mutation = graphql` 20 | mutation AddTodoMutation($input: AddTodoInput!) { 21 | addTodo(input:$input) { 22 | todoEdge { 23 | __typename 24 | cursor 25 | node { 26 | id 27 | complete 28 | text 29 | } 30 | } 31 | viewer { 32 | id 33 | totalCount 34 | } 35 | } 36 | } 37 | `; 38 | 39 | function sharedUpdater(store, user, newEdge) { 40 | const userProxy = store.get(user.id); 41 | const conn = ConnectionHandler.getConnection( 42 | userProxy, 43 | 'TodoList_todos', 44 | ); 45 | ConnectionHandler.insertEdgeAfter(conn, newEdge); 46 | } 47 | 48 | let tempID = 0; 49 | 50 | function commit( 51 | environment, 52 | text, 53 | user 54 | ) { 55 | return commitMutation( 56 | environment, 57 | { 58 | mutation, 59 | variables: { 60 | input: { 61 | text, 62 | clientMutationId: tempID++, 63 | }, 64 | }, 65 | updater: (store) => { 66 | const payload = store.getRootField('addTodo'); 67 | const newEdge = payload.getLinkedRecord('todoEdge'); 68 | sharedUpdater(store, user, newEdge); 69 | }, 70 | optimisticUpdater: (store) => { 71 | const id = 'client:newTodo:' + tempID++; 72 | const node = store.create(id, 'Todo'); 73 | node.setValue(text, 'text'); 74 | node.setValue(id, 'id'); 75 | 76 | const newEdge = store.create( 77 | 'client:newEdge:' + tempID++, 78 | 'TodoEdge', 79 | ); 80 | newEdge.setLinkedRecord(node, 'node'); 81 | sharedUpdater(store, user, newEdge); 82 | const userProxy = store.get(user.id); 83 | userProxy.setValue( 84 | userProxy.getValue('totalCount') + 1, 85 | 'totalCount', 86 | ); 87 | }, 88 | } 89 | ); 90 | } 91 | 92 | export default {commit}; 93 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/mutations/ChangeTodoStatusMutation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | import { commitMutation, graphql } from 'react-relay' 14 | 15 | const mutation = graphql` 16 | mutation ChangeTodoStatusMutation($input: ChangeTodoStatusInput!) { 17 | changeTodoStatus(input: $input) { 18 | todo { 19 | todoId 20 | complete 21 | } 22 | viewer { 23 | id 24 | completedCount 25 | } 26 | } 27 | } 28 | ` 29 | 30 | function getOptimisticResponse(complete, todo, user) { 31 | const viewerPayload = { id: user.id } 32 | if (user.completedCount != null) { 33 | viewerPayload.completedCount = complete 34 | ? user.completedCount + 1 35 | : user.completedCount - 1 36 | } 37 | return { 38 | changeTodoStatus: { 39 | todo: { 40 | complete: complete, 41 | id: todo.id, 42 | }, 43 | viewer: viewerPayload, 44 | }, 45 | } 46 | } 47 | 48 | function commit(environment, complete, todo, user) { 49 | return commitMutation(environment, { 50 | mutation, 51 | variables: { 52 | input: { complete, id: todo.id }, 53 | }, 54 | optimisticResponse: getOptimisticResponse(complete, todo, user), 55 | }) 56 | } 57 | 58 | export default { commit } 59 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/mutations/MarkAllTodosMutation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | import { commitMutation, graphql } from 'react-relay' 14 | 15 | const mutation = graphql` 16 | mutation MarkAllTodosMutation($input: MarkAllTodosInput!) { 17 | markAllTodos(input: $input) { 18 | changedTodos { 19 | id 20 | complete 21 | } 22 | viewer { 23 | id 24 | completedCount 25 | } 26 | } 27 | } 28 | ` 29 | 30 | function getOptimisticResponse(complete, todos, user) { 31 | const payload = { viewer: { id: user.id } } 32 | if (todos && todos.edges) { 33 | payload.changedTodos = todos.edges 34 | .filter(edge => edge.node.complete !== complete) 35 | .map(edge => ({ 36 | complete: complete, 37 | id: edge.node.id, 38 | })) 39 | } 40 | if (user.totalCount != null) { 41 | payload.viewer.completedCount = complete ? user.totalCount : 0 42 | } 43 | return { 44 | markAllTodos: payload, 45 | } 46 | } 47 | 48 | function commit(environment, complete, todos, user) { 49 | return commitMutation(environment, { 50 | mutation, 51 | variables: { 52 | input: { complete }, 53 | }, 54 | optimisticResponse: getOptimisticResponse(complete, todos, user), 55 | }) 56 | } 57 | 58 | export default { commit } 59 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/mutations/RemoveCompletedTodosMutation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | import { 14 | commitMutation, 15 | graphql, 16 | } from 'react-relay'; 17 | import {ConnectionHandler} from 'relay-runtime'; 18 | 19 | const mutation = graphql` 20 | mutation RemoveCompletedTodosMutation($input: RemoveCompletedTodosInput!) { 21 | removeCompletedTodos(input: $input) { 22 | deletedTodoIds, 23 | viewer { 24 | completedCount, 25 | totalCount, 26 | }, 27 | } 28 | } 29 | `; 30 | 31 | function sharedUpdater(store, user, deletedIDs) { 32 | const userProxy = store.get(user.id); 33 | const conn = ConnectionHandler.getConnection( 34 | userProxy, 35 | 'TodoList_todos', 36 | ); 37 | deletedIDs.forEach((deletedID) => 38 | ConnectionHandler.deleteNode(conn, deletedID) 39 | ); 40 | } 41 | 42 | function commit( 43 | environment, 44 | todos, 45 | user, 46 | ) { 47 | return commitMutation( 48 | environment, 49 | { 50 | mutation, 51 | variables: { 52 | input: {}, 53 | }, 54 | updater: (store) => { 55 | const payload = store.getRootField('removeCompletedTodos'); 56 | sharedUpdater(store, user, payload.getValue('deletedTodoIds')); 57 | }, 58 | optimisticUpdater: (store) => { 59 | if (todos && todos.edges) { 60 | const deletedIDs = todos.edges 61 | .filter(edge => edge.node.complete) 62 | .map(edge => edge.node.id); 63 | sharedUpdater(store, user, deletedIDs); 64 | } 65 | }, 66 | } 67 | ); 68 | } 69 | 70 | export default {commit}; 71 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/mutations/RemoveTodoMutation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | import { commitMutation, graphql } from 'react-relay' 14 | import { ConnectionHandler } from 'relay-runtime' 15 | 16 | const mutation = graphql` 17 | mutation RemoveTodoMutation($input: RemoveTodoInput!) { 18 | removeTodo(input: $input) { 19 | deletedTodoId 20 | viewer { 21 | completedCount 22 | totalCount 23 | } 24 | } 25 | } 26 | ` 27 | 28 | function sharedUpdater(store, user, deletedID) { 29 | const userProxy = store.get(user.id) 30 | const conn = ConnectionHandler.getConnection(userProxy, 'TodoList_todos') 31 | ConnectionHandler.deleteNode(conn, deletedID) 32 | } 33 | 34 | function commit(environment, todo, user) { 35 | return commitMutation(environment, { 36 | mutation, 37 | variables: { 38 | input: { id: todo.id }, 39 | }, 40 | updater: store => { 41 | const payload = store.getRootField('removeTodo') 42 | sharedUpdater(store, user, payload.getValue('deletedTodoId')) 43 | }, 44 | optimisticUpdater: store => { 45 | sharedUpdater(store, user, todo.id) 46 | }, 47 | }) 48 | } 49 | 50 | export default { commit } 51 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/mutations/RenameTodoMutation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provided by Facebook is for non-commercial testing and evaluation 3 | * purposes only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 9 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | import { commitMutation, graphql } from 'react-relay' 14 | 15 | const mutation = graphql` 16 | mutation RenameTodoMutation($input: RenameTodoInput!) { 17 | renameTodo(input: $input) { 18 | todo { 19 | id 20 | text 21 | } 22 | } 23 | } 24 | ` 25 | 26 | function getOptimisticResponse(text, todo) { 27 | return { 28 | renameTodo: { 29 | todo: { 30 | id: todo.id, 31 | text: text, 32 | }, 33 | }, 34 | } 35 | } 36 | 37 | function commit(environment, text, todo) { 38 | return commitMutation(environment, { 39 | mutation, 40 | variables: { 41 | input: { text, id: todo.id }, 42 | }, 43 | optimisticResponse: getOptimisticResponse(text, todo), 44 | }) 45 | } 46 | 47 | export default { commit } 48 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/mutations/__generated__/ChangeTodoStatusMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash f1e93f4a42cccdfd76aeabfc274034f2 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type ChangeTodoStatusMutationVariables = {| 13 | input: { 14 | clientMutationId?: ?string; 15 | id?: ?string; 16 | complete?: ?boolean; 17 | }; 18 | |}; 19 | export type ChangeTodoStatusMutationResponse = {| 20 | +changeTodoStatus: ?{| 21 | +todo: ?{| 22 | +todoId: string; 23 | +complete: boolean; 24 | |}; 25 | +viewer: ?{| 26 | +id: string; 27 | +completedCount: ?number; 28 | |}; 29 | |}; 30 | |}; 31 | */ 32 | 33 | 34 | /* 35 | mutation ChangeTodoStatusMutation( 36 | $input: ChangeTodoStatusInput! 37 | ) { 38 | changeTodoStatus(input: $input) { 39 | todo { 40 | todoId 41 | complete 42 | id 43 | } 44 | viewer { 45 | id 46 | completedCount 47 | } 48 | } 49 | } 50 | */ 51 | 52 | const batch /*: ConcreteBatch*/ = { 53 | "fragment": { 54 | "argumentDefinitions": [ 55 | { 56 | "kind": "LocalArgument", 57 | "name": "input", 58 | "type": "ChangeTodoStatusInput!", 59 | "defaultValue": null 60 | } 61 | ], 62 | "kind": "Fragment", 63 | "metadata": null, 64 | "name": "ChangeTodoStatusMutation", 65 | "selections": [ 66 | { 67 | "kind": "LinkedField", 68 | "alias": null, 69 | "args": [ 70 | { 71 | "kind": "Variable", 72 | "name": "input", 73 | "variableName": "input", 74 | "type": "ChangeTodoStatusInput!" 75 | } 76 | ], 77 | "concreteType": "ChangeTodoStatusPayload", 78 | "name": "changeTodoStatus", 79 | "plural": false, 80 | "selections": [ 81 | { 82 | "kind": "LinkedField", 83 | "alias": null, 84 | "args": null, 85 | "concreteType": "Todo", 86 | "name": "todo", 87 | "plural": false, 88 | "selections": [ 89 | { 90 | "kind": "ScalarField", 91 | "alias": null, 92 | "args": null, 93 | "name": "todoId", 94 | "storageKey": null 95 | }, 96 | { 97 | "kind": "ScalarField", 98 | "alias": null, 99 | "args": null, 100 | "name": "complete", 101 | "storageKey": null 102 | } 103 | ], 104 | "storageKey": null 105 | }, 106 | { 107 | "kind": "LinkedField", 108 | "alias": null, 109 | "args": null, 110 | "concreteType": "User", 111 | "name": "viewer", 112 | "plural": false, 113 | "selections": [ 114 | { 115 | "kind": "ScalarField", 116 | "alias": null, 117 | "args": null, 118 | "name": "id", 119 | "storageKey": null 120 | }, 121 | { 122 | "kind": "ScalarField", 123 | "alias": null, 124 | "args": null, 125 | "name": "completedCount", 126 | "storageKey": null 127 | } 128 | ], 129 | "storageKey": null 130 | } 131 | ], 132 | "storageKey": null 133 | } 134 | ], 135 | "type": "Mutation" 136 | }, 137 | "id": null, 138 | "kind": "Batch", 139 | "metadata": {}, 140 | "name": "ChangeTodoStatusMutation", 141 | "query": { 142 | "argumentDefinitions": [ 143 | { 144 | "kind": "LocalArgument", 145 | "name": "input", 146 | "type": "ChangeTodoStatusInput!", 147 | "defaultValue": null 148 | } 149 | ], 150 | "kind": "Root", 151 | "name": "ChangeTodoStatusMutation", 152 | "operation": "mutation", 153 | "selections": [ 154 | { 155 | "kind": "LinkedField", 156 | "alias": null, 157 | "args": [ 158 | { 159 | "kind": "Variable", 160 | "name": "input", 161 | "variableName": "input", 162 | "type": "ChangeTodoStatusInput!" 163 | } 164 | ], 165 | "concreteType": "ChangeTodoStatusPayload", 166 | "name": "changeTodoStatus", 167 | "plural": false, 168 | "selections": [ 169 | { 170 | "kind": "LinkedField", 171 | "alias": null, 172 | "args": null, 173 | "concreteType": "Todo", 174 | "name": "todo", 175 | "plural": false, 176 | "selections": [ 177 | { 178 | "kind": "ScalarField", 179 | "alias": null, 180 | "args": null, 181 | "name": "todoId", 182 | "storageKey": null 183 | }, 184 | { 185 | "kind": "ScalarField", 186 | "alias": null, 187 | "args": null, 188 | "name": "complete", 189 | "storageKey": null 190 | }, 191 | { 192 | "kind": "ScalarField", 193 | "alias": null, 194 | "args": null, 195 | "name": "id", 196 | "storageKey": null 197 | } 198 | ], 199 | "storageKey": null 200 | }, 201 | { 202 | "kind": "LinkedField", 203 | "alias": null, 204 | "args": null, 205 | "concreteType": "User", 206 | "name": "viewer", 207 | "plural": false, 208 | "selections": [ 209 | { 210 | "kind": "ScalarField", 211 | "alias": null, 212 | "args": null, 213 | "name": "id", 214 | "storageKey": null 215 | }, 216 | { 217 | "kind": "ScalarField", 218 | "alias": null, 219 | "args": null, 220 | "name": "completedCount", 221 | "storageKey": null 222 | } 223 | ], 224 | "storageKey": null 225 | } 226 | ], 227 | "storageKey": null 228 | } 229 | ] 230 | }, 231 | "text": "mutation ChangeTodoStatusMutation(\n $input: ChangeTodoStatusInput!\n) {\n changeTodoStatus(input: $input) {\n todo {\n todoId\n complete\n id\n }\n viewer {\n id\n completedCount\n }\n }\n}\n" 232 | }; 233 | 234 | module.exports = batch; 235 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/mutations/__generated__/MarkAllTodosMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash ca24d33385a1c8aa0297aee219065972 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type MarkAllTodosMutationVariables = {| 13 | input: { 14 | clientMutationId?: ?string; 15 | complete?: ?boolean; 16 | }; 17 | |}; 18 | export type MarkAllTodosMutationResponse = {| 19 | +markAllTodos: ?{| 20 | +changedTodos: ?$ReadOnlyArray; 24 | +viewer: ?{| 25 | +id: string; 26 | +completedCount: ?number; 27 | |}; 28 | |}; 29 | |}; 30 | */ 31 | 32 | 33 | /* 34 | mutation MarkAllTodosMutation( 35 | $input: MarkAllTodosInput! 36 | ) { 37 | markAllTodos(input: $input) { 38 | changedTodos { 39 | id 40 | complete 41 | } 42 | viewer { 43 | id 44 | completedCount 45 | } 46 | } 47 | } 48 | */ 49 | 50 | const batch /*: ConcreteBatch*/ = { 51 | "fragment": { 52 | "argumentDefinitions": [ 53 | { 54 | "kind": "LocalArgument", 55 | "name": "input", 56 | "type": "MarkAllTodosInput!", 57 | "defaultValue": null 58 | } 59 | ], 60 | "kind": "Fragment", 61 | "metadata": null, 62 | "name": "MarkAllTodosMutation", 63 | "selections": [ 64 | { 65 | "kind": "LinkedField", 66 | "alias": null, 67 | "args": [ 68 | { 69 | "kind": "Variable", 70 | "name": "input", 71 | "variableName": "input", 72 | "type": "MarkAllTodosInput!" 73 | } 74 | ], 75 | "concreteType": "MarkAllTodosPayload", 76 | "name": "markAllTodos", 77 | "plural": false, 78 | "selections": [ 79 | { 80 | "kind": "LinkedField", 81 | "alias": null, 82 | "args": null, 83 | "concreteType": "Todo", 84 | "name": "changedTodos", 85 | "plural": true, 86 | "selections": [ 87 | { 88 | "kind": "ScalarField", 89 | "alias": null, 90 | "args": null, 91 | "name": "id", 92 | "storageKey": null 93 | }, 94 | { 95 | "kind": "ScalarField", 96 | "alias": null, 97 | "args": null, 98 | "name": "complete", 99 | "storageKey": null 100 | } 101 | ], 102 | "storageKey": null 103 | }, 104 | { 105 | "kind": "LinkedField", 106 | "alias": null, 107 | "args": null, 108 | "concreteType": "User", 109 | "name": "viewer", 110 | "plural": false, 111 | "selections": [ 112 | { 113 | "kind": "ScalarField", 114 | "alias": null, 115 | "args": null, 116 | "name": "id", 117 | "storageKey": null 118 | }, 119 | { 120 | "kind": "ScalarField", 121 | "alias": null, 122 | "args": null, 123 | "name": "completedCount", 124 | "storageKey": null 125 | } 126 | ], 127 | "storageKey": null 128 | } 129 | ], 130 | "storageKey": null 131 | } 132 | ], 133 | "type": "Mutation" 134 | }, 135 | "id": null, 136 | "kind": "Batch", 137 | "metadata": {}, 138 | "name": "MarkAllTodosMutation", 139 | "query": { 140 | "argumentDefinitions": [ 141 | { 142 | "kind": "LocalArgument", 143 | "name": "input", 144 | "type": "MarkAllTodosInput!", 145 | "defaultValue": null 146 | } 147 | ], 148 | "kind": "Root", 149 | "name": "MarkAllTodosMutation", 150 | "operation": "mutation", 151 | "selections": [ 152 | { 153 | "kind": "LinkedField", 154 | "alias": null, 155 | "args": [ 156 | { 157 | "kind": "Variable", 158 | "name": "input", 159 | "variableName": "input", 160 | "type": "MarkAllTodosInput!" 161 | } 162 | ], 163 | "concreteType": "MarkAllTodosPayload", 164 | "name": "markAllTodos", 165 | "plural": false, 166 | "selections": [ 167 | { 168 | "kind": "LinkedField", 169 | "alias": null, 170 | "args": null, 171 | "concreteType": "Todo", 172 | "name": "changedTodos", 173 | "plural": true, 174 | "selections": [ 175 | { 176 | "kind": "ScalarField", 177 | "alias": null, 178 | "args": null, 179 | "name": "id", 180 | "storageKey": null 181 | }, 182 | { 183 | "kind": "ScalarField", 184 | "alias": null, 185 | "args": null, 186 | "name": "complete", 187 | "storageKey": null 188 | } 189 | ], 190 | "storageKey": null 191 | }, 192 | { 193 | "kind": "LinkedField", 194 | "alias": null, 195 | "args": null, 196 | "concreteType": "User", 197 | "name": "viewer", 198 | "plural": false, 199 | "selections": [ 200 | { 201 | "kind": "ScalarField", 202 | "alias": null, 203 | "args": null, 204 | "name": "id", 205 | "storageKey": null 206 | }, 207 | { 208 | "kind": "ScalarField", 209 | "alias": null, 210 | "args": null, 211 | "name": "completedCount", 212 | "storageKey": null 213 | } 214 | ], 215 | "storageKey": null 216 | } 217 | ], 218 | "storageKey": null 219 | } 220 | ] 221 | }, 222 | "text": "mutation MarkAllTodosMutation(\n $input: MarkAllTodosInput!\n) {\n markAllTodos(input: $input) {\n changedTodos {\n id\n complete\n }\n viewer {\n id\n completedCount\n }\n }\n}\n" 223 | }; 224 | 225 | module.exports = batch; 226 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/mutations/__generated__/RemoveCompletedTodosMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash 05d4aaca3140107c65dfd77d1d390c87 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type RemoveCompletedTodosMutationVariables = {| 13 | input: { 14 | clientMutationId?: ?string; 15 | }; 16 | |}; 17 | export type RemoveCompletedTodosMutationResponse = {| 18 | +removeCompletedTodos: ?{| 19 | +deletedTodoIds: ?$ReadOnlyArray; 20 | +viewer: ?{| 21 | +completedCount: ?number; 22 | +totalCount: ?number; 23 | |}; 24 | |}; 25 | |}; 26 | */ 27 | 28 | 29 | /* 30 | mutation RemoveCompletedTodosMutation( 31 | $input: RemoveCompletedTodosInput! 32 | ) { 33 | removeCompletedTodos(input: $input) { 34 | deletedTodoIds 35 | viewer { 36 | completedCount 37 | totalCount 38 | id 39 | } 40 | } 41 | } 42 | */ 43 | 44 | const batch /*: ConcreteBatch*/ = { 45 | "fragment": { 46 | "argumentDefinitions": [ 47 | { 48 | "kind": "LocalArgument", 49 | "name": "input", 50 | "type": "RemoveCompletedTodosInput!", 51 | "defaultValue": null 52 | } 53 | ], 54 | "kind": "Fragment", 55 | "metadata": null, 56 | "name": "RemoveCompletedTodosMutation", 57 | "selections": [ 58 | { 59 | "kind": "LinkedField", 60 | "alias": null, 61 | "args": [ 62 | { 63 | "kind": "Variable", 64 | "name": "input", 65 | "variableName": "input", 66 | "type": "RemoveCompletedTodosInput!" 67 | } 68 | ], 69 | "concreteType": "RemoveCompletedTodosPayload", 70 | "name": "removeCompletedTodos", 71 | "plural": false, 72 | "selections": [ 73 | { 74 | "kind": "ScalarField", 75 | "alias": null, 76 | "args": null, 77 | "name": "deletedTodoIds", 78 | "storageKey": null 79 | }, 80 | { 81 | "kind": "LinkedField", 82 | "alias": null, 83 | "args": null, 84 | "concreteType": "User", 85 | "name": "viewer", 86 | "plural": false, 87 | "selections": [ 88 | { 89 | "kind": "ScalarField", 90 | "alias": null, 91 | "args": null, 92 | "name": "completedCount", 93 | "storageKey": null 94 | }, 95 | { 96 | "kind": "ScalarField", 97 | "alias": null, 98 | "args": null, 99 | "name": "totalCount", 100 | "storageKey": null 101 | } 102 | ], 103 | "storageKey": null 104 | } 105 | ], 106 | "storageKey": null 107 | } 108 | ], 109 | "type": "Mutation" 110 | }, 111 | "id": null, 112 | "kind": "Batch", 113 | "metadata": {}, 114 | "name": "RemoveCompletedTodosMutation", 115 | "query": { 116 | "argumentDefinitions": [ 117 | { 118 | "kind": "LocalArgument", 119 | "name": "input", 120 | "type": "RemoveCompletedTodosInput!", 121 | "defaultValue": null 122 | } 123 | ], 124 | "kind": "Root", 125 | "name": "RemoveCompletedTodosMutation", 126 | "operation": "mutation", 127 | "selections": [ 128 | { 129 | "kind": "LinkedField", 130 | "alias": null, 131 | "args": [ 132 | { 133 | "kind": "Variable", 134 | "name": "input", 135 | "variableName": "input", 136 | "type": "RemoveCompletedTodosInput!" 137 | } 138 | ], 139 | "concreteType": "RemoveCompletedTodosPayload", 140 | "name": "removeCompletedTodos", 141 | "plural": false, 142 | "selections": [ 143 | { 144 | "kind": "ScalarField", 145 | "alias": null, 146 | "args": null, 147 | "name": "deletedTodoIds", 148 | "storageKey": null 149 | }, 150 | { 151 | "kind": "LinkedField", 152 | "alias": null, 153 | "args": null, 154 | "concreteType": "User", 155 | "name": "viewer", 156 | "plural": false, 157 | "selections": [ 158 | { 159 | "kind": "ScalarField", 160 | "alias": null, 161 | "args": null, 162 | "name": "completedCount", 163 | "storageKey": null 164 | }, 165 | { 166 | "kind": "ScalarField", 167 | "alias": null, 168 | "args": null, 169 | "name": "totalCount", 170 | "storageKey": null 171 | }, 172 | { 173 | "kind": "ScalarField", 174 | "alias": null, 175 | "args": null, 176 | "name": "id", 177 | "storageKey": null 178 | } 179 | ], 180 | "storageKey": null 181 | } 182 | ], 183 | "storageKey": null 184 | } 185 | ] 186 | }, 187 | "text": "mutation RemoveCompletedTodosMutation(\n $input: RemoveCompletedTodosInput!\n) {\n removeCompletedTodos(input: $input) {\n deletedTodoIds\n viewer {\n completedCount\n totalCount\n id\n }\n }\n}\n" 188 | }; 189 | 190 | module.exports = batch; 191 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/mutations/__generated__/RemoveTodoMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash 0a8876b18d239f935cb5ad4f92bf4a8a 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type RemoveTodoMutationVariables = {| 13 | input: { 14 | clientMutationId?: ?string; 15 | id?: ?string; 16 | }; 17 | |}; 18 | export type RemoveTodoMutationResponse = {| 19 | +removeTodo: ?{| 20 | +deletedTodoId: ?string; 21 | +viewer: ?{| 22 | +completedCount: ?number; 23 | +totalCount: ?number; 24 | |}; 25 | |}; 26 | |}; 27 | */ 28 | 29 | 30 | /* 31 | mutation RemoveTodoMutation( 32 | $input: RemoveTodoInput! 33 | ) { 34 | removeTodo(input: $input) { 35 | deletedTodoId 36 | viewer { 37 | completedCount 38 | totalCount 39 | id 40 | } 41 | } 42 | } 43 | */ 44 | 45 | const batch /*: ConcreteBatch*/ = { 46 | "fragment": { 47 | "argumentDefinitions": [ 48 | { 49 | "kind": "LocalArgument", 50 | "name": "input", 51 | "type": "RemoveTodoInput!", 52 | "defaultValue": null 53 | } 54 | ], 55 | "kind": "Fragment", 56 | "metadata": null, 57 | "name": "RemoveTodoMutation", 58 | "selections": [ 59 | { 60 | "kind": "LinkedField", 61 | "alias": null, 62 | "args": [ 63 | { 64 | "kind": "Variable", 65 | "name": "input", 66 | "variableName": "input", 67 | "type": "RemoveTodoInput!" 68 | } 69 | ], 70 | "concreteType": "RemoveTodoPayload", 71 | "name": "removeTodo", 72 | "plural": false, 73 | "selections": [ 74 | { 75 | "kind": "ScalarField", 76 | "alias": null, 77 | "args": null, 78 | "name": "deletedTodoId", 79 | "storageKey": null 80 | }, 81 | { 82 | "kind": "LinkedField", 83 | "alias": null, 84 | "args": null, 85 | "concreteType": "User", 86 | "name": "viewer", 87 | "plural": false, 88 | "selections": [ 89 | { 90 | "kind": "ScalarField", 91 | "alias": null, 92 | "args": null, 93 | "name": "completedCount", 94 | "storageKey": null 95 | }, 96 | { 97 | "kind": "ScalarField", 98 | "alias": null, 99 | "args": null, 100 | "name": "totalCount", 101 | "storageKey": null 102 | } 103 | ], 104 | "storageKey": null 105 | } 106 | ], 107 | "storageKey": null 108 | } 109 | ], 110 | "type": "Mutation" 111 | }, 112 | "id": null, 113 | "kind": "Batch", 114 | "metadata": {}, 115 | "name": "RemoveTodoMutation", 116 | "query": { 117 | "argumentDefinitions": [ 118 | { 119 | "kind": "LocalArgument", 120 | "name": "input", 121 | "type": "RemoveTodoInput!", 122 | "defaultValue": null 123 | } 124 | ], 125 | "kind": "Root", 126 | "name": "RemoveTodoMutation", 127 | "operation": "mutation", 128 | "selections": [ 129 | { 130 | "kind": "LinkedField", 131 | "alias": null, 132 | "args": [ 133 | { 134 | "kind": "Variable", 135 | "name": "input", 136 | "variableName": "input", 137 | "type": "RemoveTodoInput!" 138 | } 139 | ], 140 | "concreteType": "RemoveTodoPayload", 141 | "name": "removeTodo", 142 | "plural": false, 143 | "selections": [ 144 | { 145 | "kind": "ScalarField", 146 | "alias": null, 147 | "args": null, 148 | "name": "deletedTodoId", 149 | "storageKey": null 150 | }, 151 | { 152 | "kind": "LinkedField", 153 | "alias": null, 154 | "args": null, 155 | "concreteType": "User", 156 | "name": "viewer", 157 | "plural": false, 158 | "selections": [ 159 | { 160 | "kind": "ScalarField", 161 | "alias": null, 162 | "args": null, 163 | "name": "completedCount", 164 | "storageKey": null 165 | }, 166 | { 167 | "kind": "ScalarField", 168 | "alias": null, 169 | "args": null, 170 | "name": "totalCount", 171 | "storageKey": null 172 | }, 173 | { 174 | "kind": "ScalarField", 175 | "alias": null, 176 | "args": null, 177 | "name": "id", 178 | "storageKey": null 179 | } 180 | ], 181 | "storageKey": null 182 | } 183 | ], 184 | "storageKey": null 185 | } 186 | ] 187 | }, 188 | "text": "mutation RemoveTodoMutation(\n $input: RemoveTodoInput!\n) {\n removeTodo(input: $input) {\n deletedTodoId\n viewer {\n completedCount\n totalCount\n id\n }\n }\n}\n" 189 | }; 190 | 191 | module.exports = batch; 192 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/ClientApp/mutations/__generated__/RenameTodoMutation.graphql.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @flow 3 | * @relayHash ca4ce0e6808fa6741913b717f1995ba9 4 | */ 5 | 6 | /* eslint-disable */ 7 | 8 | 'use strict'; 9 | 10 | /*:: 11 | import type {ConcreteBatch} from 'relay-runtime'; 12 | export type RenameTodoMutationVariables = {| 13 | input: { 14 | clientMutationId?: ?string; 15 | id?: ?string; 16 | text?: ?string; 17 | }; 18 | |}; 19 | export type RenameTodoMutationResponse = {| 20 | +renameTodo: ?{| 21 | +todo: ?{| 22 | +id: string; 23 | +text: string; 24 | |}; 25 | |}; 26 | |}; 27 | */ 28 | 29 | 30 | /* 31 | mutation RenameTodoMutation( 32 | $input: RenameTodoInput! 33 | ) { 34 | renameTodo(input: $input) { 35 | todo { 36 | id 37 | text 38 | } 39 | } 40 | } 41 | */ 42 | 43 | const batch /*: ConcreteBatch*/ = { 44 | "fragment": { 45 | "argumentDefinitions": [ 46 | { 47 | "kind": "LocalArgument", 48 | "name": "input", 49 | "type": "RenameTodoInput!", 50 | "defaultValue": null 51 | } 52 | ], 53 | "kind": "Fragment", 54 | "metadata": null, 55 | "name": "RenameTodoMutation", 56 | "selections": [ 57 | { 58 | "kind": "LinkedField", 59 | "alias": null, 60 | "args": [ 61 | { 62 | "kind": "Variable", 63 | "name": "input", 64 | "variableName": "input", 65 | "type": "RenameTodoInput!" 66 | } 67 | ], 68 | "concreteType": "RenameTodoPayload", 69 | "name": "renameTodo", 70 | "plural": false, 71 | "selections": [ 72 | { 73 | "kind": "LinkedField", 74 | "alias": null, 75 | "args": null, 76 | "concreteType": "Todo", 77 | "name": "todo", 78 | "plural": false, 79 | "selections": [ 80 | { 81 | "kind": "ScalarField", 82 | "alias": null, 83 | "args": null, 84 | "name": "id", 85 | "storageKey": null 86 | }, 87 | { 88 | "kind": "ScalarField", 89 | "alias": null, 90 | "args": null, 91 | "name": "text", 92 | "storageKey": null 93 | } 94 | ], 95 | "storageKey": null 96 | } 97 | ], 98 | "storageKey": null 99 | } 100 | ], 101 | "type": "Mutation" 102 | }, 103 | "id": null, 104 | "kind": "Batch", 105 | "metadata": {}, 106 | "name": "RenameTodoMutation", 107 | "query": { 108 | "argumentDefinitions": [ 109 | { 110 | "kind": "LocalArgument", 111 | "name": "input", 112 | "type": "RenameTodoInput!", 113 | "defaultValue": null 114 | } 115 | ], 116 | "kind": "Root", 117 | "name": "RenameTodoMutation", 118 | "operation": "mutation", 119 | "selections": [ 120 | { 121 | "kind": "LinkedField", 122 | "alias": null, 123 | "args": [ 124 | { 125 | "kind": "Variable", 126 | "name": "input", 127 | "variableName": "input", 128 | "type": "RenameTodoInput!" 129 | } 130 | ], 131 | "concreteType": "RenameTodoPayload", 132 | "name": "renameTodo", 133 | "plural": false, 134 | "selections": [ 135 | { 136 | "kind": "LinkedField", 137 | "alias": null, 138 | "args": null, 139 | "concreteType": "Todo", 140 | "name": "todo", 141 | "plural": false, 142 | "selections": [ 143 | { 144 | "kind": "ScalarField", 145 | "alias": null, 146 | "args": null, 147 | "name": "id", 148 | "storageKey": null 149 | }, 150 | { 151 | "kind": "ScalarField", 152 | "alias": null, 153 | "args": null, 154 | "name": "text", 155 | "storageKey": null 156 | } 157 | ], 158 | "storageKey": null 159 | } 160 | ], 161 | "storageKey": null 162 | } 163 | ] 164 | }, 165 | "text": "mutation RenameTodoMutation(\n $input: RenameTodoInput!\n) {\n renameTodo(input: $input) {\n todo {\n id\n text\n }\n }\n}\n" 166 | }; 167 | 168 | module.exports = batch; 169 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/Database/TodoDatabase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace GraphQL.Relay.Todo 4 | { 5 | public class Todo 6 | { 7 | public string Id { get; set; } 8 | public string Text { get; set; } 9 | public bool Completed { get; set; } 10 | } 11 | 12 | public class User 13 | { 14 | public string Id { get; set; } 15 | // public Lazy> Todos { get; set; } 16 | } 17 | 18 | public class Todos : ConcurrentDictionary 19 | { 20 | } 21 | 22 | public class Users : ConcurrentDictionary 23 | { 24 | public Users() : base() 25 | { 26 | this["me"] = new User { Id = "me" }; 27 | } 28 | } 29 | 30 | internal class TodoDatabaseContext 31 | { 32 | public readonly Todos todos; 33 | public readonly Users users; 34 | 35 | internal TodoDatabaseContext() 36 | { 37 | todos = new Todos(); 38 | users = new Users(); 39 | } 40 | } 41 | 42 | public static class Database 43 | { 44 | private static readonly TodoDatabaseContext _context = new(); 45 | 46 | public static User GetUser(string id) => _context.users[id]; 47 | 48 | public static User GetViewer() => GetUser("me"); 49 | 50 | public static Todo AddTodo(string text, bool complete = false) 51 | { 52 | var todo = new Todo 53 | { 54 | Id = Guid.NewGuid().ToString(), 55 | Text = text, 56 | Completed = complete, 57 | }; 58 | 59 | _context.todos[todo.Id] = todo; 60 | return todo; 61 | } 62 | 63 | public static Todo GetTodoById(string id) => _context.todos[id]; 64 | 65 | public static IEnumerable GetTodos() => GetTodosByStatus(); 66 | 67 | public static IEnumerable GetTodosByStatus(string status = "any") 68 | { 69 | var todos = _context.todos.Select(t => t.Value); 70 | if (status == "any") 71 | return todos; 72 | return todos.Where(t => t.Completed == (status == "completed")); 73 | } 74 | 75 | private static Todo ChangeTodoStatus(Todo todo, bool complete) 76 | { 77 | todo.Completed = complete; 78 | return todo; 79 | } 80 | 81 | public static Todo ChangeTodoStatus(string id, bool complete) 82 | { 83 | return ChangeTodoStatus(GetTodoById(id), complete); 84 | } 85 | 86 | public static IEnumerable MarkAllTodos(bool complete) 87 | { 88 | return GetTodosByStatus() 89 | .Select(t => ChangeTodoStatus(t, complete)); 90 | } 91 | 92 | public static void RemoveTodo(string id) 93 | { 94 | _context.todos.Remove(id, out _); 95 | } 96 | 97 | public static Todo RenameTodo(string id, string text) 98 | { 99 | var todo = GetTodoById(id); 100 | todo.Text = text; 101 | return todo; 102 | } 103 | 104 | public static IEnumerable RemoveCompletedTodos(bool complete) 105 | { 106 | complete.ToString(); // TODO: unused parameter, check this example 107 | 108 | var deleted = new List(); 109 | 110 | foreach (var todo in GetTodosByStatus("completed")) 111 | { 112 | deleted.Add(todo.Id); 113 | RemoveTodo(todo.Id); 114 | } 115 | 116 | return deleted; 117 | } 118 | 119 | public static User GetUserById(string id) => _context.users[id]; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/GraphQL.Relay.Todo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | 3 | namespace GraphQL.Relay.Todo 4 | { 5 | public class Program 6 | { 7 | public static void Main(string[] args) 8 | { 9 | BuildWebHost(args).Run(); 10 | } 11 | 12 | public static IWebHost BuildWebHost(string[] args) => 13 | WebHost.CreateDefaultBuilder(args) 14 | .UseStartup() 15 | .Build(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:62774/", 7 | "sslPort": 44327 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "GraphQL.Relay.Todo": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Getting started 4 | 5 | ```sh 6 | yarn install 7 | dotnet run 8 | ``` 9 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/Schema/Mutation.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.Types; 2 | using GraphQL.Types; 3 | using GraphQL.Types.Relay; 4 | using GraphQL.Types.Relay.DataObjects; 5 | 6 | namespace GraphQL.Relay.Todo.Schema 7 | { 8 | public class TodoMutation : MutationGraphType 9 | { 10 | public TodoMutation() : base() 11 | { 12 | Mutation("addTodo"); 13 | Mutation("changeTodoStatus"); 14 | Mutation("markAllTodos"); 15 | Mutation("removeCompletedTodos"); 16 | Mutation("removeTodo"); 17 | Mutation("renameTodo"); 18 | } 19 | } 20 | 21 | public class AddTodoInput : MutationInputGraphType 22 | { 23 | public AddTodoInput() 24 | { 25 | Name = "AddTodoInput"; 26 | 27 | Field("text"); 28 | } 29 | } 30 | 31 | public class AddTodoPayload : MutationPayloadGraphType 32 | { 33 | public AddTodoPayload() 34 | { 35 | Name = "AddTodoPayload"; 36 | Field>("todoEdge"); 37 | Field("viewer"); 38 | } 39 | 40 | public override object MutateAndGetPayload( 41 | MutationInputs inputs, 42 | IResolveFieldContext context 43 | ) 44 | { 45 | var todo = Database.AddTodo(inputs.Get("text")); 46 | 47 | return new 48 | { 49 | TodoEdge = new Edge 50 | { 51 | Node = todo, 52 | Cursor = ConnectionUtils.CursorForObjectInConnection(Database.GetTodos(), todo) 53 | }, 54 | Viewer = Database.GetViewer(), 55 | }; 56 | } 57 | } 58 | 59 | public class ChangeTodoStatusInput : MutationInputGraphType 60 | { 61 | public ChangeTodoStatusInput() 62 | { 63 | Name = "ChangeTodoStatusInput"; 64 | 65 | Field("id"); 66 | Field("complete"); 67 | } 68 | } 69 | 70 | public class ChangeTodoStatusPayload : MutationPayloadGraphType 71 | { 72 | public ChangeTodoStatusPayload() 73 | { 74 | Name = "ChangeTodoStatusPayload"; 75 | 76 | Field("todo"); 77 | Field("viewer"); 78 | } 79 | 80 | public override object MutateAndGetPayload( 81 | MutationInputs inputs, 82 | IResolveFieldContext context 83 | ) 84 | { 85 | return new 86 | { 87 | Viewer = Database.GetViewer(), 88 | Todo = Database.ChangeTodoStatus( 89 | Node.FromGlobalId(inputs.Get("id")).Id, 90 | inputs.Get("complete") 91 | ), 92 | }; 93 | } 94 | } 95 | 96 | public class MarkAllTodosInput : MutationInputGraphType 97 | { 98 | public MarkAllTodosInput() 99 | { 100 | Name = "MarkAllTodosInput"; 101 | 102 | Field("complete"); 103 | } 104 | } 105 | 106 | public class MarkAllTodosPayload : MutationPayloadGraphType 107 | { 108 | public MarkAllTodosPayload() 109 | { 110 | Name = "MarkAllTodosPayload"; 111 | 112 | Field>("changedTodos"); 113 | Field("viewer"); 114 | } 115 | 116 | public override object MutateAndGetPayload( 117 | MutationInputs inputs, 118 | IResolveFieldContext context 119 | ) 120 | { 121 | return new 122 | { 123 | Viewer = Database.GetViewer(), 124 | ChangedTodos = Database.MarkAllTodos( 125 | inputs.Get("complete") 126 | ), 127 | }; 128 | } 129 | } 130 | 131 | public class RemoveCompletedTodosInput : MutationInputGraphType 132 | { 133 | public RemoveCompletedTodosInput() 134 | { 135 | Name = "RemoveCompletedTodosInput"; 136 | } 137 | } 138 | 139 | public class RemoveCompletedTodosPayload : MutationPayloadGraphType 140 | { 141 | public RemoveCompletedTodosPayload() 142 | { 143 | Name = "RemoveCompletedTodosPayload"; 144 | 145 | Field>("deletedTodoIds"); 146 | Field("viewer"); 147 | } 148 | 149 | public override object MutateAndGetPayload( 150 | MutationInputs inputs, 151 | IResolveFieldContext context 152 | ) 153 | { 154 | return new 155 | { 156 | Viewer = Database.GetViewer(), 157 | DeletedTodoIds = Database 158 | .RemoveCompletedTodos(inputs.Get("complete")) 159 | .Select(id => Node.ToGlobalId("Todo", id)), 160 | }; 161 | } 162 | } 163 | 164 | public class RemoveTodoInput : MutationInputGraphType 165 | { 166 | public RemoveTodoInput() 167 | { 168 | Name = "RemoveTodoInput"; 169 | 170 | Field("id"); 171 | } 172 | } 173 | 174 | public class RemoveTodoPayload : MutationPayloadGraphType 175 | { 176 | public RemoveTodoPayload() 177 | { 178 | Name = "RemoveTodoPayload"; 179 | 180 | Field("deletedTodoId"); 181 | Field("viewer"); 182 | } 183 | 184 | public override object MutateAndGetPayload( 185 | MutationInputs inputs, 186 | IResolveFieldContext context 187 | ) 188 | { 189 | Database.RemoveTodo( 190 | Node.FromGlobalId(inputs.Get("id")).Id 191 | ); 192 | 193 | return new 194 | { 195 | Viewer = Database.GetViewer(), 196 | deletedTodoId = inputs.Get("id"), 197 | }; 198 | } 199 | } 200 | 201 | public class RenameTodoInput : MutationInputGraphType 202 | { 203 | public RenameTodoInput() 204 | { 205 | Name = "RenameTodoInput"; 206 | 207 | Field("id"); 208 | Field("text"); 209 | } 210 | } 211 | 212 | public class RenameTodoPayload : MutationPayloadGraphType 213 | { 214 | public RenameTodoPayload() 215 | { 216 | Name = "RenameTodoPayload"; 217 | 218 | Field("todo"); 219 | Field("viewer"); 220 | } 221 | 222 | public override object MutateAndGetPayload( 223 | MutationInputs inputs, 224 | IResolveFieldContext context 225 | ) 226 | { 227 | return new 228 | { 229 | Viewer = Database.GetViewer(), 230 | Todo = Database.RenameTodo( 231 | Node.FromGlobalId(inputs.Get("id")).Id, 232 | inputs.Get("text") 233 | ), 234 | }; 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/Schema/Query.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Relay.Types; 2 | using GraphQL.Relay.Utilities; 3 | using GraphQL.Types; 4 | 5 | namespace GraphQL.Relay.Todo.Schema 6 | { 7 | public class TodoQuery : QueryGraphType 8 | { 9 | public TodoQuery() 10 | { 11 | Name = "Query"; 12 | 13 | Field("viewer").Resolve(ctx => Database.GetViewer()); 14 | } 15 | } 16 | 17 | public class TodoGraphType : NodeGraphType 18 | { 19 | public TodoGraphType() 20 | { 21 | Name = "Todo"; 22 | 23 | Id(t => t.Id); 24 | Field(t => t.Text); 25 | Field("complete", t => t.Completed); 26 | } 27 | 28 | public override Todo GetById(IResolveFieldContext context, string id) => 29 | Database.GetTodoById(id); 30 | } 31 | 32 | public class UserGraphType : NodeGraphType 33 | { 34 | public UserGraphType() 35 | { 36 | Name = "User"; 37 | 38 | Id(t => t.Id); 39 | 40 | Connection() 41 | .Name("todos") 42 | .Argument( 43 | name: "status", 44 | description: "Filter todos by their status", 45 | defaultValue: "any" 46 | ) 47 | .Resolve(ctx => ctx.ToConnection( 48 | Database.GetTodosByStatus(ctx.GetArgument("status")) 49 | )); 50 | 51 | Field("totalCount").Resolve(ctx => Database.GetTodos().Count()); 52 | Field("completedCount").Resolve(ctx => Database.GetTodosByStatus("completed").Count()); 53 | } 54 | 55 | public override User GetById(IResolveFieldContext context, string id) => Database.GetUserById(id); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/Schema/Schema.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Relay.Todo.Schema 2 | { 3 | public class TodoSchema : GraphQL.Types.Schema 4 | { 5 | public TodoSchema() 6 | { 7 | Query = new TodoQuery(); 8 | Mutation = new TodoMutation(); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/SchemaWriter.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.SystemTextJson; 2 | 3 | namespace GraphQL.Relay.Todo 4 | { 5 | public class SchemaWriter 6 | { 7 | private readonly IGraphQLTextSerializer _serializer = new GraphQLSerializer(); 8 | private readonly IDocumentExecuter _executor; 9 | private readonly GraphQL.Types.Schema _schema; 10 | 11 | public SchemaWriter(GraphQL.Types.Schema schema) 12 | { 13 | _executor = new DocumentExecuter(); 14 | _schema = schema; 15 | } 16 | 17 | public async Task GenerateAsync() 18 | { 19 | var result = await _executor.ExecuteAsync( 20 | new ExecutionOptions 21 | { 22 | Schema = _schema, 23 | Query = _introspectionQuery 24 | } 25 | ); 26 | 27 | if (result.Errors?.Any() ?? false) 28 | throw result.Errors.First(); 29 | 30 | return _serializer.Serialize(result); 31 | } 32 | 33 | private readonly string _introspectionQuery = @" 34 | query IntrospectionQuery { 35 | __schema { 36 | queryType { name } 37 | mutationType { name } 38 | subscriptionType { name } 39 | types { 40 | ...FullType 41 | } 42 | directives { 43 | name 44 | description 45 | locations 46 | args { 47 | ...InputValue 48 | } 49 | } 50 | } 51 | } 52 | fragment FullType on __Type { 53 | kind 54 | name 55 | description 56 | fields(includeDeprecated: true) { 57 | name 58 | description 59 | args { 60 | ...InputValue 61 | } 62 | type { 63 | ...TypeRef 64 | } 65 | isDeprecated 66 | deprecationReason 67 | } 68 | inputFields { 69 | ...InputValue 70 | } 71 | interfaces { 72 | ...TypeRef 73 | } 74 | enumValues(includeDeprecated: true) { 75 | name 76 | description 77 | isDeprecated 78 | deprecationReason 79 | } 80 | possibleTypes { 81 | ...TypeRef 82 | } 83 | } 84 | fragment InputValue on __InputValue { 85 | name 86 | description 87 | type { ...TypeRef } 88 | defaultValue 89 | } 90 | fragment TypeRef on __Type { 91 | kind 92 | name 93 | ofType { 94 | kind 95 | name 96 | ofType { 97 | kind 98 | name 99 | ofType { 100 | kind 101 | name 102 | ofType { 103 | kind 104 | name 105 | ofType { 106 | kind 107 | name 108 | ofType { 109 | kind 110 | name 111 | ofType { 112 | kind 113 | name 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | "; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using GraphQL.Relay.Todo.Schema; 3 | 4 | namespace GraphQL.Relay.Todo 5 | { 6 | public class Startup 7 | { 8 | public void ConfigureServices(IServiceCollection services) 9 | { 10 | services.AddGraphQL(b => b 11 | .UseApolloTracing(true) 12 | .AddSchema() 13 | .AddAutoClrMappings() 14 | .AddSystemTextJson() 15 | .AddErrorInfoProvider(options => options.ExposeExceptionDetails = true)); 16 | } 17 | 18 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 19 | { 20 | var writer = new SchemaWriter(new TodoSchema()); 21 | 22 | // TODO: Why is it all necessary? 23 | string schema = writer.GenerateAsync().GetAwaiter().GetResult(); 24 | using (FileStream fs = File.Create(Path.Combine(env.WebRootPath, "schema.json"))) 25 | { 26 | byte[] info = new UTF8Encoding(true).GetBytes(schema); 27 | fs.Write(info, 0, info.Length); 28 | } 29 | 30 | if (env.IsDevelopment()) 31 | { 32 | app.UseDeveloperExceptionPage(); 33 | } 34 | 35 | app 36 | .UseStaticFiles() 37 | .UseDefaultFiles() 38 | .UseGraphQL() 39 | .UseGraphQLGraphiQL(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realy-example-todo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "compile": "relay-compiler --src ./ClientApp --schema ./wwwroot/schema.json" 7 | }, 8 | "devDependencies": { 9 | "@dhau/relay-compiler-webpack-plugin": "^0.5.3", 10 | "aspnet-webpack": "^2.0.1", 11 | "relay-compiler": "^1.4.1", 12 | "webpack": "^3.8.1", 13 | "webpack-atoms": "^4.1.2" 14 | }, 15 | "dependencies": { 16 | "babel-core": "^6.26.0", 17 | "babel-plugin-relay": "^1.4.1", 18 | "babel-preset-jason": "^3.1.1", 19 | "babel-preset-react": "^6.11.1", 20 | "classnames": "2.2.5", 21 | "prop-types": "^15.5.10", 22 | "react": "^15.6.1", 23 | "react-dom": "^15.6.1", 24 | "react-relay": "^1.4.0", 25 | "relay-compiler": "^1.4.1", 26 | "todomvc-app-css": "^2.1.0", 27 | "todomvc-common": "^1.0.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const RelayCompilerWebpackPlugin = require('@dhau/relay-compiler-webpack-plugin') 3 | const { plugins, rules } = require('webpack-atoms') 4 | 5 | module.exports = { 6 | devtool: "cheap-module-source-map", 7 | entry: './ClientApp/app.js', 8 | output: { 9 | path: `${__dirname}/wwwroot/dist`, 10 | filename: 'bundle.js', 11 | publicPath: '/' 12 | }, 13 | module: { 14 | rules: [ 15 | rules.js(), 16 | rules.css(), 17 | rules.images(), 18 | ] 19 | }, 20 | plugins: [ 21 | plugins.extractText({ disable: true }), 22 | new RelayCompilerWebpackPlugin({ 23 | schema: path.resolve(__dirname, 'wwwroot/schema.json'), 24 | src: path.resolve(__dirname, './ClientApp'), 25 | }) 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/GraphQL.Relay.Todo/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
    8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/GraphQL.Relay.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | GraphQL;Relay;React 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Types/ArraySliceMetrics.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Builders; 2 | using GraphQL.Relay.Utilities; 3 | 4 | namespace GraphQL.Relay.Types 5 | { 6 | /// 7 | /// Factory methods for 8 | /// 9 | public static class ArraySliceMetrics 10 | { 11 | public static ArraySliceMetrics Create( 12 | IList slice, 13 | int? first = null, 14 | string after = null, 15 | int? last = null, 16 | string before = null, 17 | bool strictCheck = true 18 | ) 19 | { 20 | return new ArraySliceMetrics(slice, first, after, last, before, strictCheck); 21 | } 22 | 23 | public static ArraySliceMetrics Create( 24 | IList slice, 25 | int sliceStartIndex, 26 | int totalCount, 27 | int? first = null, 28 | string after = null, 29 | int? last = null, 30 | string before = null, 31 | bool strictCheck = true 32 | ) 33 | { 34 | return new ArraySliceMetrics(slice, first, after, last, before, sliceStartIndex, totalCount, 35 | strictCheck); 36 | } 37 | 38 | public static ArraySliceMetrics Create( 39 | IList slice, 40 | ResolveConnectionContext context, 41 | bool strictCheck = true 42 | ) 43 | { 44 | return new ArraySliceMetrics(slice, context.First, context.After, context.Last, context.Before, strictCheck); 45 | } 46 | 47 | public static ArraySliceMetrics Create( 48 | IList slice, 49 | IResolveConnectionContext context, 50 | int sliceStartIndex, 51 | int totalCount, 52 | bool strictCheck = true 53 | ) 54 | { 55 | return new ArraySliceMetrics(slice, context.First, context.After, context.Last, context.Before, 56 | sliceStartIndex, totalCount, strictCheck); 57 | } 58 | } 59 | 60 | public class ArraySliceMetrics 61 | { 62 | private readonly IList _items; 63 | 64 | /// 65 | /// The Total number of items in outer list. May be >= the SliceSize 66 | /// 67 | public int TotalCount { get; } 68 | 69 | /// 70 | /// The local total of the list slice. 71 | /// 72 | public int SliceSize => _items.Count; 73 | 74 | /// 75 | /// The start index of the slice within the larger List 76 | /// 77 | /// 78 | public int StartIndex { get; } 79 | 80 | /// 81 | /// The end index of the slice within the larger List 82 | /// 83 | public int EndIndex => StartIndex + SliceSize - 1; 84 | 85 | public int FirstValidOffset => 0; 86 | public int LastValidOffset => TotalCount - 1; 87 | public int StartOffset { get; } 88 | public int EndOffset { get; } 89 | public bool HasPrevious { get; } 90 | public bool HasNext { get; } 91 | 92 | public bool IsEmpty => EndOffset < StartOffset; 93 | 94 | public IEnumerable Slice => _items.Slice( 95 | Math.Max(StartOffset - StartIndex, 0), 96 | SliceSize - (EndIndex - EndOffset) 97 | ); 98 | 99 | public ArraySliceMetrics( 100 | IList slice, 101 | int? first, 102 | string after, 103 | int? last, 104 | string before, 105 | bool strictCheck = true 106 | ) : this(slice, first, after, last, before, 0, slice.Count, strictCheck) 107 | { 108 | } 109 | 110 | public ArraySliceMetrics( 111 | IList slice, 112 | int? first, 113 | string after, 114 | int? last, 115 | string before, 116 | int sliceStartIndex, 117 | int totalCount, 118 | bool strictCheck = true 119 | ) 120 | { 121 | _items = slice; 122 | 123 | var range = RelayPagination.CalculateEdgeRange(totalCount, first, after, last, before); 124 | 125 | StartIndex = sliceStartIndex; 126 | TotalCount = totalCount; 127 | StartOffset = Math.Max(range.StartOffset, StartIndex); 128 | EndOffset = Math.Max(StartOffset - 1, Math.Min(range.EndOffset, EndIndex)); 129 | 130 | // Determine hasPrevious/hasNext according to specs 131 | // https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo.Fields 132 | // Because we work with offsets as cursors, we can use 133 | // a rather intuitive way to determine hasPrevious/hasNext. 134 | // The only special case we deal with is an empty edge list. 135 | // As an empty edge list does not contain any cursors, 136 | // pagination cannot continue from such a situation. 137 | HasPrevious = !IsEmpty && StartOffset > FirstValidOffset; 138 | HasNext = !IsEmpty && EndOffset < LastValidOffset; 139 | 140 | if (strictCheck) 141 | { 142 | if (!SliceCoversRange(StartIndex, EndIndex, range)) 143 | { 144 | throw new IncompleteSliceException( 145 | $"Provided slice data with index range [{StartIndex},{EndIndex}] does not " + 146 | $"completely contain the expected data range [{range.StartOffset}, {range.EndOffset}]", nameof(slice)); 147 | } 148 | } 149 | } 150 | 151 | private static bool SliceCoversRange(int sliceStartIndex, int sliceEndIndex, EdgeRange range) 152 | { 153 | return sliceStartIndex <= range.StartOffset && sliceEndIndex >= range.EndOffset; 154 | } 155 | } 156 | 157 | [Obsolete("Use ArraySliceMetrics.Create instead")] 158 | public class ArraySliceMetrics : ArraySliceMetrics 159 | { 160 | [Obsolete("Use ArraySliceMetrics.Create instead")] 161 | public ArraySliceMetrics( 162 | IList slice, 163 | ResolveConnectionContext context, 164 | bool strictCheck = true 165 | ) : base(slice, context.First, context.After, context.Last, context.Before, 0, slice.Count, strictCheck) 166 | { 167 | } 168 | 169 | [Obsolete("Use ArraySliceMetrics.Create instead")] 170 | public ArraySliceMetrics( 171 | IList slice, 172 | ResolveConnectionContext context, 173 | int sliceStartIndex, 174 | int totalCount, 175 | bool strictCheck = true 176 | ) : base(slice, context.First, context.After, context.Last, context.Before, sliceStartIndex, totalCount, 177 | strictCheck) 178 | { 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Types/ConnectionUtils.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Builders; 2 | using GraphQL.Relay.Utilities; 3 | using GraphQL.Types.Relay.DataObjects; 4 | 5 | namespace GraphQL.Relay.Types 6 | { 7 | public static class ConnectionUtils 8 | { 9 | private const string PREFIX = "arrayconnection"; 10 | 11 | public static Connection ToConnection( 12 | IEnumerable items, 13 | IResolveConnectionContext context, 14 | bool strictCheck = true 15 | ) 16 | { 17 | var list = items.ToList(); 18 | return ToConnection(list, context, sliceStartIndex: 0, totalCount: list.Count, strictCheck); 19 | } 20 | 21 | public static Connection ToConnection( 22 | IEnumerable slice, 23 | IResolveConnectionContext context, 24 | int sliceStartIndex, 25 | int totalCount, 26 | bool strictCheck = true 27 | ) 28 | { 29 | var sliceList = slice as IList ?? slice.ToList(); 30 | 31 | var metrics = ArraySliceMetrics.Create( 32 | sliceList, 33 | context, 34 | sliceStartIndex, 35 | totalCount, 36 | strictCheck 37 | ); 38 | 39 | var edges = metrics.Slice.Select((item, i) => new Edge 40 | { 41 | Node = item, 42 | Cursor = OffsetToCursor(metrics.StartOffset + i) 43 | }) 44 | .ToList(); 45 | 46 | var firstEdge = edges.FirstOrDefault(); 47 | var lastEdge = edges.LastOrDefault(); 48 | 49 | return new Connection 50 | { 51 | Edges = edges, 52 | TotalCount = totalCount, 53 | PageInfo = new PageInfo 54 | { 55 | StartCursor = firstEdge?.Cursor, 56 | EndCursor = lastEdge?.Cursor, 57 | HasPreviousPage = metrics.HasPrevious, 58 | HasNextPage = metrics.HasNext, 59 | } 60 | }; 61 | } 62 | 63 | public static string CursorForObjectInConnection( 64 | IEnumerable slice, 65 | T item 66 | ) 67 | { 68 | int idx = slice.ToList().IndexOf(item); 69 | 70 | return idx == -1 ? null : OffsetToCursor(idx); 71 | } 72 | 73 | public static int CursorToOffset(string cursor) 74 | { 75 | return int.Parse(cursor.Base64Decode().Substring(PREFIX.Length + 1)); 76 | } 77 | 78 | public static string OffsetToCursor(int offset) 79 | { 80 | return $"{PREFIX}:{offset}".Base64Encode(); 81 | } 82 | 83 | public static int OffsetOrDefault(string cursor, int defaultOffset) 84 | { 85 | if (cursor == null) 86 | return defaultOffset; 87 | 88 | try 89 | { 90 | return CursorToOffset(cursor); 91 | } 92 | catch 93 | { 94 | return defaultOffset; 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Types/IncompleteSliceException.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Relay.Types 2 | { 3 | [Serializable] 4 | public class IncompleteSliceException : ArgumentException 5 | { 6 | public IncompleteSliceException() : this("The provided data slice does not contain all expected items.") 7 | { 8 | } 9 | 10 | public IncompleteSliceException(string message) : base(message) 11 | { 12 | } 13 | 14 | public IncompleteSliceException(string message, Exception innerException) : base(message, innerException) 15 | { 16 | } 17 | 18 | public IncompleteSliceException(string message, string paramName, Exception innerException) : base(message, 19 | paramName, innerException) 20 | { 21 | } 22 | 23 | public IncompleteSliceException(string message, string paramName) : base(message, paramName) 24 | { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Types/MutationGraphType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Relay.Types 4 | { 5 | public class MutationGraphType : ObjectGraphType 6 | { 7 | public MutationGraphType() 8 | { 9 | Name = "Mutation"; 10 | } 11 | 12 | public FieldType Mutation(string name) 13 | where TMutationType : IMutationPayload 14 | where TMutationInput : MutationInputGraphType 15 | { 16 | return Field(name, typeof(TMutationType)) 17 | .Argument>("input") 18 | .Resolve(c => 19 | { 20 | var inputs = c.GetArgument>("input"); 21 | return ((TMutationType)c.FieldDefinition.ResolvedType).MutateAndGetPayload(new MutationInputs(inputs), c); 22 | }) 23 | .FieldType; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Types/MutationInputGraphType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Relay.Types 4 | { 5 | public class MutationInputGraphType : InputObjectGraphType 6 | { 7 | public MutationInputGraphType() 8 | { 9 | Field("clientMutationId"); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Types/MutationInputs.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Relay.Types 2 | { 3 | public class MutationInputs : Dictionary 4 | { 5 | public MutationInputs() 6 | { 7 | } 8 | 9 | public MutationInputs(IDictionary dict) : base(dict) 10 | { 11 | } 12 | 13 | public object Get(string key) 14 | { 15 | return this[key]; 16 | } 17 | 18 | public T Get(string key, T defaultValue = default) 19 | { 20 | return TryGetValue(key, out object value) ? (T)value : defaultValue; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Types/MutationPayloadGraphType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | using GraphQLParser.AST; 3 | 4 | namespace GraphQL.Relay.Types 5 | { 6 | public interface IMutationPayload 7 | { 8 | T MutateAndGetPayload(MutationInputs inputs, IResolveFieldContext context); 9 | } 10 | 11 | public abstract class MutationPayloadGraphType : ObjectGraphType, IMutationPayload 12 | { 13 | protected MutationPayloadGraphType() 14 | { 15 | Field("clientMutationId", typeof(StringGraphType)).Resolve(GetClientId); 16 | } 17 | 18 | public abstract TOut MutateAndGetPayload(MutationInputs inputs, IResolveFieldContext context); 19 | 20 | private string GetClientId(IResolveFieldContext context) 21 | { 22 | var field = context.Operation.SelectionSet.Selections 23 | .Where(s => s is GraphQLField) 24 | .Cast() 25 | .First(s => IsCorrectSelection(context, s)); 26 | 27 | var arg = field.Arguments.First(a => a.Name == "input"); 28 | 29 | if (arg.Value is GraphQLVariable variable) 30 | { 31 | var name = variable.Name; 32 | var inputs = context.Variables.First(v => v.Name == name).Value as Dictionary; 33 | 34 | return inputs["clientMutationId"] as string; 35 | } 36 | 37 | var value = 38 | ((GraphQLObjectValue)arg.Value).Fields.First(f => f.Name == "clientMutationId").Value as GraphQLStringValue; 39 | return (string)value.Value; // TODO: string allocation 40 | } 41 | 42 | private static bool IsCorrectSelection(IResolveFieldContext context, GraphQLField field) 43 | { 44 | return Enumerable.Any(field.SelectionSet.Selections, 45 | s => s.Location.Equals(context.FieldAst.Location)); 46 | } 47 | } 48 | 49 | public abstract class MutationPayloadGraphType : MutationPayloadGraphType 50 | { 51 | } 52 | 53 | public abstract class MutationPayloadGraphType : MutationPayloadGraphType 54 | { 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Types/NodeGraphType.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using GraphQL.Relay.Utilities; 3 | using GraphQL.Types; 4 | using GraphQL.Types.Relay; 5 | 6 | namespace GraphQL.Relay.Types 7 | { 8 | public class GlobalId 9 | { 10 | public string Type, Id; 11 | } 12 | 13 | public interface IRelayNode 14 | { 15 | T GetById(IResolveFieldContext context, string id); 16 | } 17 | 18 | public static class Node 19 | { 20 | public static NodeGraphType For(Func getById) 21 | { 22 | var type = new DefaultNodeGraphType(getById); 23 | return type; 24 | } 25 | 26 | public static string ToGlobalId(string name, object id) 27 | { 28 | return $"{name}:{id}".Base64Encode(); 29 | } 30 | 31 | public static GlobalId FromGlobalId(string globalId) 32 | { 33 | var parts = globalId.Base64Decode().Split(':'); 34 | return new GlobalId 35 | { 36 | Type = parts[0], 37 | Id = string.Join(":", parts.Skip(count: 1)), 38 | }; 39 | } 40 | } 41 | 42 | public abstract class NodeGraphType : ObjectGraphType, IRelayNode 43 | { 44 | public static Type Edge => typeof(EdgeType>); 45 | 46 | public static Type Connection => typeof(ConnectionType>); 47 | 48 | protected NodeGraphType() 49 | { 50 | Interface(); 51 | } 52 | 53 | public abstract TOut GetById(IResolveFieldContext context, string id); 54 | 55 | public FieldType Id(Expression> expression) 56 | { 57 | string name = null; 58 | try 59 | { 60 | name = expression.NameOf().ToCamelCase(); 61 | } 62 | catch 63 | { 64 | } 65 | 66 | return Id(name, expression); 67 | } 68 | 69 | public FieldType Id(string name, Expression> expression) 70 | { 71 | if (!string.IsNullOrWhiteSpace(name)) 72 | { 73 | // if there is a field called "ID" on the object, namespace it to "contactId" 74 | if (name.ToLower() == "id") 75 | { 76 | if (string.IsNullOrWhiteSpace(Name)) 77 | { 78 | throw new InvalidOperationException( 79 | "The parent GraphQL type must define a Name before declaring the Id field " + 80 | "in order to properly prefix the local id field"); 81 | } 82 | 83 | name = (Name + "Id").ToCamelCase(); 84 | } 85 | 86 | Field(name, expression) 87 | .Description($"The Id of the {Name ?? "node"}") 88 | .FieldType.Metadata["RelayLocalIdField"] = true; 89 | } 90 | 91 | var fn = expression.Compile(); 92 | 93 | var field = Field>("id") 94 | .Description($"The Global Id of the {Name ?? "node"}") 95 | .Resolve(context => Node.ToGlobalId(context.ParentType.Name, fn(context.Source))) 96 | .FieldType; 97 | 98 | field.Metadata["RelayGlobalIdField"] = true; 99 | 100 | if (!string.IsNullOrWhiteSpace(name)) 101 | field.Metadata["RelayRelatedLocalIdField"] = name; 102 | 103 | return field; 104 | } 105 | } 106 | 107 | public abstract class NodeGraphType : NodeGraphType 108 | { 109 | } 110 | 111 | public abstract class NodeGraphType : NodeGraphType 112 | { 113 | } 114 | 115 | public abstract class AsyncNodeGraphType : NodeGraphType> 116 | { 117 | } 118 | 119 | public class DefaultNodeGraphType : NodeGraphType 120 | { 121 | private readonly Func _getById; 122 | 123 | public DefaultNodeGraphType(Func getById) 124 | { 125 | _getById = getById; 126 | } 127 | 128 | public override TOut GetById(IResolveFieldContext context, string id) 129 | { 130 | return _getById(id); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Types/NodeInterface.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Relay.Types 4 | { 5 | public class NodeInterface : InterfaceGraphType 6 | { 7 | public NodeInterface() 8 | { 9 | Name = "Node"; 10 | 11 | Field("id").Description("Global node Id"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Types/QueryGraphType.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Types; 2 | 3 | namespace GraphQL.Relay.Types 4 | { 5 | public class QueryGraphType : ObjectGraphType 6 | { 7 | public QueryGraphType() 8 | { 9 | Name = "Query"; 10 | 11 | Field("node") 12 | .Description("Fetches an object given its global Id") 13 | .Argument>("id", "The global Id of the object") 14 | .Resolve(ResolveObjectFromGlobalId); 15 | } 16 | 17 | private object ResolveObjectFromGlobalId(IResolveFieldContext context) 18 | { 19 | var globalId = context.GetArgument("id"); 20 | var parts = Node.FromGlobalId(globalId); 21 | var node = context.Schema.AllTypes[parts.Type] as IRelayNode; 22 | 23 | return node.GetById(context, parts.Id); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Types/SliceMetrics.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Builders; 2 | using GraphQL.Relay.Utilities; 3 | using GraphQL.Types.Relay.DataObjects; 4 | 5 | namespace GraphQL.Relay.Types 6 | { 7 | /// 8 | /// Factory methods for 9 | /// 10 | public static class SliceMetrics 11 | { 12 | /// 13 | /// Factory method to create an 14 | /// 15 | public static SliceMetrics Create( 16 | IEnumerable source, 17 | IResolveConnectionContext context, 18 | int? totalCount = null 19 | ) 20 | { 21 | var totalSourceRowCount = totalCount ?? source.Count(); 22 | var edges = context.EdgesToReturn(totalSourceRowCount); 23 | var nodes = source.Skip(edges.StartOffset).Take(edges.Count); 24 | 25 | return new( 26 | source: nodes.ToList(), 27 | edges, 28 | totalSourceRowCount 29 | ); 30 | } 31 | 32 | /// 33 | /// Factory method to create an 34 | /// 35 | public static SliceMetrics Create( 36 | IQueryable source, 37 | IResolveConnectionContext context 38 | ) 39 | { 40 | var totalCount = source.Count(); 41 | var edges = context.EdgesToReturn(totalCount); 42 | var nodes = source.Skip(edges.StartOffset).Take(edges.Count); 43 | 44 | return new( 45 | source: nodes.ToList(), 46 | edges, 47 | totalCount 48 | ); 49 | } 50 | } 51 | 52 | /// 53 | /// Relay connection slice metrics 54 | /// 55 | public class SliceMetrics 56 | { 57 | /// 58 | /// The Total number of items in outer list. May be >= the SliceSize 59 | /// 60 | public int TotalCount { get; } 61 | 62 | /// 63 | /// The local total of the list slice. 64 | /// 65 | public int SliceSize { get; } 66 | 67 | /// 68 | /// Starting index for a slice of an item source 69 | /// 70 | public int StartIndex { get; } 71 | 72 | /// 73 | /// When a slice of a larger source has any records before it, this will be 74 | /// 75 | public bool HasPrevious { get; } 76 | 77 | /// 78 | /// When a slice of a larger source has any records after it, this will be 79 | /// 80 | public bool HasNext { get; } 81 | 82 | /// 83 | /// A particular section of a larger source 84 | /// 85 | public IEnumerable Slice { get; } 86 | 87 | /// 88 | /// Constructor for relay connection slice metrics 89 | /// 90 | /// 91 | /// 92 | /// 93 | /// 94 | public SliceMetrics( 95 | IList source, 96 | EdgeRange edges, 97 | int totalCount 98 | ) 99 | { 100 | TotalCount = totalCount; 101 | 102 | Slice = source; 103 | SliceSize = source.Count; 104 | 105 | HasNext = (edges.StartOffset + SliceSize) < TotalCount; 106 | HasPrevious = edges.StartOffset > 0; 107 | 108 | StartIndex = edges.StartOffset; 109 | } 110 | 111 | /// 112 | /// Converts current slice to a 113 | /// 114 | /// 115 | public Connection ToConnection() 116 | { 117 | var edges = Slice 118 | .Select( 119 | (item, i) => 120 | new Edge 121 | { 122 | Node = item, 123 | Cursor = ConnectionUtils.OffsetToCursor(StartIndex + i) 124 | } 125 | ) 126 | .ToList(); 127 | 128 | var firstEdge = edges.FirstOrDefault(); 129 | var lastEdge = edges.LastOrDefault(); 130 | 131 | return new Connection 132 | { 133 | Edges = edges, 134 | TotalCount = TotalCount, 135 | PageInfo = new PageInfo 136 | { 137 | StartCursor = firstEdge?.Cursor, 138 | EndCursor = lastEdge?.Cursor, 139 | HasPreviousPage = HasPrevious, 140 | HasNextPage = HasNext, 141 | } 142 | }; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Utilities/EdgeRange.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Relay.Utilities 2 | { 3 | public struct EdgeRange 4 | { 5 | public EdgeRange(int startOffset, int endOffset) 6 | { 7 | if (startOffset < 0) 8 | throw new ArgumentOutOfRangeException(nameof(startOffset)); 9 | if (endOffset < -1) 10 | throw new ArgumentOutOfRangeException(nameof(endOffset)); 11 | 12 | StartOffset = startOffset; 13 | EndOffset = Math.Max(startOffset - 1, endOffset); 14 | } 15 | 16 | public int StartOffset { get; private set; } 17 | 18 | public int EndOffset { get; private set; } 19 | 20 | public int Count => EndOffset - StartOffset + 1; 21 | 22 | public bool IsEmpty => Count == 0; 23 | 24 | /// 25 | /// Ensures that is equal to or less than 26 | /// by moving the end offset towards start offset. 27 | /// 28 | /// Maximum count 29 | public void LimitCountFromStart(int maxLength) 30 | { 31 | if (maxLength < 0) 32 | throw new ArgumentOutOfRangeException(nameof(maxLength)); 33 | if (maxLength < Count) 34 | { 35 | EndOffset = StartOffset + maxLength - 1; 36 | } 37 | } 38 | 39 | /// 40 | /// Ensures that is equal to or less than 41 | /// by moving the start offset towards start offset. 42 | /// 43 | /// Maximum count 44 | public void LimitCountToEnd(int maxLength) 45 | { 46 | if (maxLength < 0) 47 | throw new ArgumentOutOfRangeException(nameof(maxLength)); 48 | if (maxLength < Count) 49 | { 50 | StartOffset = EndOffset - maxLength + 1; 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Utilities/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Relay.Utilities 2 | { 3 | public static class EnumerableExtensions 4 | { 5 | public static IEnumerable Slice(this IEnumerable collection, int start, int end) 6 | { 7 | int index = 0; 8 | int count = 0; 9 | 10 | count = collection.Count(); 11 | 12 | // Get start/end indexes, negative numbers start at the end of the collection. 13 | if (start < 0) 14 | start += count; 15 | if (end < 0) 16 | end += count; 17 | 18 | foreach (T item in collection) 19 | { 20 | if (index >= end) 21 | yield break; 22 | if (index >= start) 23 | yield return item; 24 | ++index; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Utilities/RelayPagination.cs: -------------------------------------------------------------------------------- 1 | using static GraphQL.Relay.Types.ConnectionUtils; 2 | 3 | namespace GraphQL.Relay.Utilities 4 | { 5 | public static class RelayPagination 6 | { 7 | /// 8 | /// Apply Facebook Relay pagination as described at 9 | /// https://facebook.github.io/relay/graphql/connections.htm#sec-Pagination-algorithm 10 | /// 11 | /// Total number of edges. 12 | /// Maximum number of edges to return after (if provided) or from 13 | /// the start of the edge list. 14 | /// 15 | /// Only return edges coming after the edge represented by this cursor. 16 | /// Maximum number of edges to return before (if provided) 17 | /// or from the end of the edge list. 18 | /// Only return edges coming before the edge represented by this cursor. 19 | /// An that defines the range of edges to return. 20 | public static EdgeRange CalculateEdgeRange(int edgeCount, int? first = null, string after = null, int? last = null, 21 | string before = null) 22 | { 23 | var range = ApplyCursorToEdges(edgeCount, after, before); 24 | 25 | if (first != null) 26 | { 27 | if (first.Value < 0) 28 | { 29 | throw new ArgumentOutOfRangeException(nameof(first)); 30 | } 31 | range.LimitCountFromStart(first.Value); 32 | } 33 | 34 | if (last != null) 35 | { 36 | if (last.Value < 0) 37 | { 38 | throw new ArgumentOutOfRangeException(nameof(last)); 39 | } 40 | 41 | range.LimitCountToEnd(last.Value); 42 | } 43 | 44 | return range; 45 | } 46 | 47 | private static EdgeRange ApplyCursorToEdges(int edgeCount, string after, string before) 48 | { 49 | const int outOfRange = -2; 50 | 51 | // only use "after" cursor if it represents an edge in [0, edgeCount-1] 52 | var afterOffset = CheckRange(OffsetOrDefault(after, outOfRange), edgeCount, -1); 53 | 54 | // only use "before" cursor if it represents an edge in [0, edgeCount-1] 55 | var beforeOffset = CheckRange(OffsetOrDefault(before, outOfRange), edgeCount, edgeCount); 56 | 57 | int startOffset = afterOffset + 1; 58 | int endOffset = beforeOffset - 1; 59 | 60 | return new EdgeRange(startOffset, endOffset); 61 | } 62 | 63 | private static int CheckRange(int value, int edgeCount, int defaultIfOutOfRange) 64 | { 65 | return value < 0 || value >= edgeCount ? defaultIfOutOfRange : value; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Utilities/ResolveConnectionContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using GraphQL.Builders; 2 | using GraphQL.Relay.Types; 3 | using GraphQL.Types.Relay.DataObjects; 4 | 5 | namespace GraphQL.Relay.Utilities 6 | { 7 | /// 8 | /// Provide extension methods for . 9 | /// 10 | public static class ResolveConnectionContextExtensions 11 | { 12 | /// 13 | /// Calculates an object based on the current 14 | /// and the provided edge items count 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static EdgeRange EdgesToReturn( 20 | this IResolveConnectionContext context, 21 | int edgeCount 22 | ) 23 | { 24 | var first = (!context.First.HasValue && !context.Last.HasValue) 25 | ? (context.PageSize ?? edgeCount) 26 | : default(int?); 27 | 28 | return RelayPagination.CalculateEdgeRange( 29 | edgeCount, 30 | first ?? context.First, 31 | context.After, 32 | context.Last, 33 | context.Before 34 | ); 35 | } 36 | 37 | /// 38 | /// From the connection context, , 39 | /// it creates a based on the given 40 | /// 41 | /// 42 | /// 43 | /// 44 | /// 45 | public static Connection ToConnection( 46 | this IResolveConnectionContext context, 47 | IQueryable items 48 | ) => SliceMetrics.Create(items, context).ToConnection(); 49 | 50 | /// 51 | /// From the connection context, , 52 | /// it creates a based on the given 53 | /// 54 | /// 55 | /// 56 | /// 57 | /// 58 | /// 59 | public static Connection ToConnection( 60 | this IResolveConnectionContext context, 61 | IEnumerable items, 62 | int? totalCount = null 63 | ) => SliceMetrics.Create(items, context, totalCount).ToConnection(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/Utilities/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace GraphQL.Relay.Utilities; 2 | 3 | /// 4 | /// Provides extension methods for string values. 5 | /// 6 | public static class StringExtensions 7 | { 8 | /// 9 | /// Returns a Base64 encoding of a UTF8-encoded string. 10 | /// 11 | public static string Base64Encode(this string value) 12 | { 13 | return value == null 14 | ? null 15 | : Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(value)); 16 | } 17 | 18 | /// 19 | /// Decodes a Base64 string and interprets the result as a UTF8-encoded string. 20 | /// 21 | public static string Base64Decode(this string value) 22 | { 23 | return value == null 24 | ? null 25 | : System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(value)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "fs-extra": "^4.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/GraphQL.Relay/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | fs-extra@^4.0.1: 6 | version "4.0.1" 7 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.1.tgz#7fc0c6c8957f983f57f306a24e5b9ddd8d0dd880" 8 | dependencies: 9 | graceful-fs "^4.1.2" 10 | jsonfile "^3.0.0" 11 | universalify "^0.1.0" 12 | 13 | graceful-fs@^4.1.2, graceful-fs@^4.1.6: 14 | version "4.1.11" 15 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 16 | 17 | jsonfile@^3.0.0: 18 | version "3.0.1" 19 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66" 20 | optionalDependencies: 21 | graceful-fs "^4.1.6" 22 | 23 | universalify@^0.1.0: 24 | version "0.1.1" 25 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" 26 | -------------------------------------------------------------------------------- /src/Tests.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(NoWarn);IDE1006;CS0618 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Always 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "diagnosticMessages": true, 4 | "longRunningTestSeconds": 10 5 | } 6 | -------------------------------------------------------------------------------- /tools/version.1.js: -------------------------------------------------------------------------------- 1 | const { blue, red, green } = require('chalk'); 2 | const { writeFileSync } = require('fs'); 3 | const { resolve, join } = require('path'); 4 | const yargs = require('yargs'); 5 | const get = require('lodash/get'); 6 | const set = require('lodash/set'); 7 | const glob = require('glob'); 8 | const semver = require('semver'); 9 | 10 | let { _, path } = yargs 11 | .help() 12 | .usage('$0 [args]') 13 | .option('path', { string: true, required: true, 'default': 'version' }) 14 | .argv; 15 | 16 | const version = _.pop(); 17 | const files = _.reduce((arr, pattern) => arr.concat(glob.sync(pattern)), []); 18 | 19 | files.forEach(file => { 20 | const json = require(join(process.cwd(), file)) 21 | let oldVersion = get(json, path); 22 | 23 | let nextVersion; 24 | if (['alpha', 'beta', 'rc'].includes(version)) { 25 | oldVersion = oldVersion.replace(/(.+[a-z])(\d)/g, (_, a, b) => `${a}.${b}`) // nuget doesn't like semver2 26 | nextVersion = semver.inc(oldVersion, 'pre', version) 27 | nextVersion = nextVersion 28 | .replace(new RegExp(`(${version})\\.(.+)`), (_, a, b) => a + b) 29 | } 30 | else if (['major', 'minor', 'patch'].includes(version)) 31 | nextVersion = semver.inc(oldVersion, version) 32 | else 33 | nextVersion = version 34 | 35 | console.log('updating ' + blue(file) + ' to version to: ' + blue(nextVersion) + '\n'); 36 | set(json, path, nextVersion); 37 | writeFileSync(file, JSON.stringify(json, null, 2) + '\n', 'utf8'); 38 | }) 39 | -------------------------------------------------------------------------------- /tools/version.js: -------------------------------------------------------------------------------- 1 | const { blue, red, green } = require('chalk'); 2 | const { readJson, readFile, writeFile, writeJson } = require('fs-extra'); 3 | const { resolve, join } = require('path'); 4 | const yargs = require('yargs'); 5 | const get = require('lodash/get'); 6 | const set = require('lodash/set'); 7 | const glob = require('glob'); 8 | const semver = require('semver'); 9 | 10 | let { _ } = yargs 11 | .help() 12 | .usage('$0 ') 13 | .argv; 14 | 15 | 16 | (async () => { 17 | let version = getNextVersion() 18 | 19 | await updateFile('package.json', d => Object.assign(d, { version })) 20 | 21 | await updateFile('./src/GraphQL.Relay/GraphQL.Relay.csproj', d => d.replace( 22 | /(.*)<\/VersionPrefix>/, 23 | `${version}<\/VersionPrefix>` 24 | )) 25 | })() 26 | 27 | 28 | const version = _.pop(); 29 | function getNextVersion() { 30 | let currentVersion = require('../package.json').version 31 | let [nextVersion] = _; 32 | 33 | if (['alpha', 'beta', 'rc'].includes(nextVersion)) 34 | nextVersion = semver.inc(currentVersion, 'pre', nextVersion) 35 | 36 | else if (['major', 'minor', 'patch'].includes(nextVersion)) 37 | nextVersion = semver.inc(currentVersion, nextVersion) 38 | 39 | return nextVersion 40 | } 41 | 42 | async function updateFile(filePath, updater) { 43 | let target = resolve(filePath) 44 | let data = await (filePath.endsWith('.json') ? 45 | readJson(target) : 46 | readFile(target, 'utf8') 47 | ) 48 | console.log(`Updating ${filePath}`) 49 | let result = updater(data) 50 | return typeof result !== 'string' ? 51 | writeJson(target, result, { spaces: 2 }) : 52 | writeFile(target, result) 53 | } 54 | 55 | --------------------------------------------------------------------------------