├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main_deploy_to_azure_function.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── api ├── .devcontainer │ ├── Dockerfile │ └── devcontainer.json ├── .funcignore ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── README.md ├── codegen.yml ├── graphql.schema.json ├── graphql │ ├── apolloContext.ts │ ├── data │ │ ├── cosmos │ │ │ ├── GameDataSource.ts │ │ │ ├── QuestionDataSource.ts │ │ │ └── UserDataSource.ts │ │ ├── inMemory │ │ │ ├── GameDataSource.ts │ │ │ ├── QuestionDataSource.ts │ │ │ └── UserDataSource.ts │ │ ├── index.ts │ │ └── types.ts │ ├── function.json │ ├── generated.ts │ ├── index.ts │ ├── resolvers.ts │ └── schema.graphql ├── host.json ├── local.settings.json ├── package-lock.json ├── package.json ├── trivia.json ├── tsconfig.json └── utils.ts ├── client ├── .editorconfig ├── .gitignore ├── .vscode │ ├── extensions.json │ └── launch.json ├── GraphQL.queries.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── operations │ │ ├── addPlayer.graphql │ │ ├── answers.graphql │ │ ├── createGame.graphql │ │ ├── endGame.graphql │ │ └── getGame.graphql │ ├── pages │ │ ├── CompleteGame.tsx │ │ ├── CreateGame.tsx │ │ ├── JoinGame.tsx │ │ └── PlayGame.tsx │ ├── react-app-env.d.ts │ ├── serviceWorker.ts │ ├── setupTests.ts │ └── useInterval.ts └── tsconfig.json └── package-lock.json /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.github/workflows/main_deploy_to_azure_function.yml: -------------------------------------------------------------------------------- 1 | # Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action 2 | # More GitHub Actions for Azure: https://github.com/Azure/actions 3 | 4 | name: Build and deploy Node.js project to Azure Function App - 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | env: 13 | AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root 14 | NODE_VERSION: '16.x' # set this to the node version to use (supports 8.x, 10.x, 12.x) 15 | 16 | jobs: 17 | build-and-deploy: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: 'Checkout GitHub Action' 21 | uses: actions/checkout@v2 22 | 23 | - name: Setup Node ${{ env.NODE_VERSION }} Environment 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ env.NODE_VERSION }} 27 | 28 | - name: 'Resolve Project Dependencies Using Npm' 29 | shell: bash 30 | run: | 31 | pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}' 32 | npm install 33 | npm run build --if-present 34 | npm run test --if-present 35 | popd 36 | 37 | - name: 'Run Azure Functions Action' 38 | uses: Azure/functions-action@v1 39 | id: fa 40 | with: 41 | app-name: 'YOUR-FUNCTION-NAME' 42 | slot-name: 'Production' 43 | package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} 44 | publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_123456 }} 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 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 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - javascript 5 | - typescript 6 | - nodejs 7 | name: "TypeScript E2E GraphQL to Cosmos DB Static web app" 8 | description: "This sample uses an Azure Static web app (React client and an Azure Function API) to request data with a GraphQL query to the API. The API translates the query to a Cosmos DB SQL query then returns that data to the client. This sample uses the [Apollo GraphQL](https://github.com/apollographql) open source tools." 9 | products: 10 | - azure 11 | - azure-app-service-static 12 | - azure-functions 13 | - azure-portal 14 | - vs-code 15 | - azure-cosmos-db 16 | --- 17 | 18 | # TypeScript E2E GraphQL to Cosmos DB Static web app 19 | 20 | This sample uses an Azure Static web app (React client and an Azure Function API) to request data with a GraphQL query to the API. The API translates the query to a Cosmos DB SQL query then returns that data to the client. This sample uses the [Apollo GraphQL](https://github.com/apollographql) open source tools. 21 | 22 | This sample is based on Aaron Powell's [Type-safe GraphQL apps with TypeScript workshop](https://github.com/aaronpowell/graphql-typescript-workshop.git), hosted on GitHub in a separate repository. You can also read his [blog series on GraphQL](https://www.aaron-powell.com/posts/2020-07-13-graphql-on-azure-part-1-getting-started/) or watch his YouTube video - [Type-safe GraphQL with TypeScript](https://www.youtube.com/watch?v=G2HUgV30EG4). 23 | 24 | ## Features 25 | 26 | This provides the following features: 27 | 28 | * Trivia game: game, player, questions 29 | * [Azure Function API](https://docs.microsoft.com/en-us/azure/azure-functions/) using GraphQL schema and generated types for both the API and client 30 | * [Azure Cosmos DB](https://docs.microsoft.com/en-us/azure/cosmos-db/) SQL database for storing game data 31 | * [Azure Static web app](https://docs.microsoft.com/en-us/azure/static-web-apps/) for hosting client and Azure Function API 32 | 33 | ## Getting Started 34 | 35 | ### Prerequisites 36 | 37 | - Node.js 14+ 38 | - Azure resources: Azure Static web app, Azure Cosmos DB 39 | 40 | ### Quickstart 41 | 42 | 1. In a bash terminal, run the command `git clone https://github.com/Azure-Samples/js-e2e-graphql-cosmosdb-static-web-app.git`. 43 | 2. In a bash terminal, run the command `cd js-e2e-graphql-cosmosdb-static-web-app` and open the project with VS Code with the commend `code .`. 44 | 3. In the VS Code integrated terminal, run the command `npm install && cd api && npm install` to install npm dependencies. 45 | 4. Use the Azure portal to create a new [Azure Cosmos DB](https://ms.portal.azure.com/#create/Microsoft.DocumentDB) resource, with a new database named `trivia` and a new collection named `game` with a partition key named `modelType`. 46 | 5. In the Azure portal for your new Cosmos DB resource, use the **Data Explorer** to import the `./api/trivia.json` file into the game collection. 47 | 6. In the Azure portal for your new Cosmos DB resource, use the **Keys** page to find and copy your **Primary connection string**. 48 | 7. In VS Code, configure `./api/local.settings.json` with the Cosmos DB connection string. 49 | 50 | ```json 51 | { 52 | "IsEncrypted": false, 53 | "Values": { 54 | "FUNCTIONS_WORKER_RUNTIME": "node", 55 | "AzureWebJobsStorage": "", 56 | "CosmosDB": "AccountEndpoint=https://..." 57 | } 58 | } 59 | ``` 60 | 61 | 8. In VS Code, select to run and debug `Run full stack`. 62 | 63 | Part of running the full stack is to generate the following files to support the graphql schema and typescript types: 64 | - `./api/graphql.schema.json` 65 | - `./api/graphql/generated.ts` 66 | - `./src/generated.tsx`: TypeScript source file for the API GraphQL schema 67 | 68 | 9. Open a browser to `http://localhost:3000` to view the React client. 69 | 70 | ### Running the game 71 | 72 | * The game provides a series of trivia questions for a player then displays the player's score. 73 | * A player's name doesn't have to be unique. Games across the same player are not tracked. 74 | * All trivia questions come from `./api/trivia.json` which is a subset of questions available from [Open Trivia Database](https://opentdb.com/). 75 | 76 | ### Using the in-memory database 77 | 78 | If you would rather use the in-memory database, change the `./api/index.ts` file to use the **inMemoryDataSources**: 79 | 80 | Change this: 81 | 82 | ```javascript 83 | const server = new ApolloServer({ 84 | schema: addResolversToSchema({ schema, resolvers }), 85 | dataSources: cosmosDataSources, 86 | context: {}, 87 | }); 88 | ``` 89 | 90 | To this: 91 | 92 | ```javascript 93 | const server = new ApolloServer({ 94 | schema: addResolversToSchema({ schema, resolvers }), 95 | dataSources: inMemoryDataSources, 96 | context: {}, 97 | }); 98 | ``` 99 | 100 | ## Troubleshooting 101 | 102 | |Error|Reason| 103 | |---|---| 104 | |Browser-displayed runtime error when you try to create game: `Unexpected token P in JSON at position 0`|Verify the Azure Function API is running on port 7071. If you started the Azure Function API, wait a couple of minutes and try again.| 105 | |Browser-displayed runtime error when you try to create game:`Error: Response not successful: Received status code 500 - CosmosDB connection string not found`|The Cosmos DB connection string isn't found in the running API.| 106 | |Browser-displayed runtime error when you try to create game:`Error: Owner resource does not exist`|The Cosmos DB resource doesn't have the trivia container created.| 107 | |Browser-displayed runtime error when you try to create game:`Error: Owner resource does not exist`|The Cosmos DB resource doesn't have the `trivia` container (database).| 108 | |Browser-displayed runtime error when you try to create game:`Error: Resource Not Found`|The Cosmos DB resource doesn't have the `game` collection in the `trivia` container (database).| 109 | |Browser-displayed runtime error when you play the game: `Error: No game trivia questions found`|The trivia.json file has not been loaded into the `game` collection in the Cosmos DB resource.| 110 | -------------------------------------------------------------------------------- /api/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Find the Dockerfile at the following URL: 2 | # Node 14: https://github.com/Azure/azure-functions-docker/blob/dev/host/4/bullseye/amd64/node/node14/node14-core-tools.Dockerfile 3 | # Node 16: https://github.com/Azure/azure-functions-docker/blob/dev/host/4/bullseye/amd64/node/node16/node16-core-tools.Dockerfile 4 | ARG VARIANT=14 5 | FROM mcr.microsoft.com/azure-functions/node:4-node${VARIANT}-core-tools 6 | 7 | # [Optional] Uncomment this section to install additional OS packages. 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends 10 | 11 | # [Optional] Uncomment if you want to install more global node packages 12 | #RUN sudo -u node npm install -g -------------------------------------------------------------------------------- /api/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/azure-functions-node 3 | { 4 | "name": "Azure Functions & Node.js", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node.js version: 14, 16 8 | "args": { "VARIANT": "14" } 9 | }, 10 | "forwardPorts": [ 7071 ], 11 | 12 | // Configure tool-specific properties. 13 | "customizations": { 14 | // Configure properties specific to VS Code. 15 | "vscode": { 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": [ 18 | "ms-azuretools.vscode-azurefunctions", 19 | "dbaeumer.vscode-eslint", 20 | "Azurite.azurite" 21 | ] 22 | } 23 | }, 24 | 25 | // Use 'postCreateCommand' to run commands after the container is created. 26 | // "postCreateCommand": "npm install", 27 | 28 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 29 | "remoteUser": "node" 30 | } 31 | -------------------------------------------------------------------------------- /api/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | csx 4 | .vs 5 | edge 6 | Publish 7 | 8 | *.user 9 | *.suo 10 | *.cscfg 11 | *.Cache 12 | project.lock.json 13 | 14 | /packages 15 | /TestResults 16 | 17 | /tools/NuGet.exe 18 | /App_Data 19 | /secrets 20 | /data 21 | .secrets 22 | appsettings.json 23 | local.settings.json 24 | 25 | node_modules 26 | dist 27 | 28 | # Local python packages 29 | .python_packages/ 30 | 31 | # Python Environments 32 | .env 33 | .venv 34 | env/ 35 | venv/ 36 | ENV/ 37 | env.bak/ 38 | venv.bak/ 39 | 40 | # Byte-compiled / optimized / DLL files 41 | __pycache__/ 42 | *.py[cod] 43 | *$py.class 44 | 45 | #TypeScript Build 46 | dist 47 | 48 | # Azurite 49 | azurite 50 | __azurite_* 51 | __blobstorage__ 52 | __queuestorage__ 53 | 54 | # MacOS 55 | .DS_Store 56 | 57 | # generated schema 58 | ./graphql.schema.json -------------------------------------------------------------------------------- /api/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /api/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Node Functions", 6 | "type": "node", 7 | "request": "attach", 8 | "port": 9229, 9 | "preLaunchTask": "func: host start" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /api/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": ".", 3 | "azureFunctions.postDeployTask": "npm install (functions)", 4 | "azureFunctions.projectLanguage": "TypeScript", 5 | "azureFunctions.projectRuntime": "~4", 6 | "debug.internalConsoleOptions": "neverOpen", 7 | "azureFunctions.preDeployTask": "npm prune (functions)" 8 | } -------------------------------------------------------------------------------- /api/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "command": "host start", 7 | "problemMatcher": "$func-node-watch", 8 | "isBackground": true, 9 | "dependsOn": "npm build (functions)" 10 | }, 11 | { 12 | "type": "shell", 13 | "label": "npm build (functions)", 14 | "command": "npm run build", 15 | "dependsOn": "npm install (functions)", 16 | "problemMatcher": "$tsc" 17 | }, 18 | { 19 | "type": "shell", 20 | "label": "npm install (functions)", 21 | "command": "npm install" 22 | }, 23 | { 24 | "type": "shell", 25 | "label": "npm prune (functions)", 26 | "command": "npm prune --production", 27 | "dependsOn": "npm build (functions)", 28 | "problemMatcher": [] 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # Apollo serverless API 2 | 3 | This serverless API uses the Apollo server with either an in-memory datasource or the CosmosDB datasource for Core (SQL) API. 4 | 5 | # Install 6 | 7 | 1. Install API dependencies 8 | 9 | ``` 10 | npm install 11 | ``` 12 | 13 | 1. For Windows only, [download, install, and start Azure Cosmos DB emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator?tabs=ssl-netstd21#download-the-emulator) if you intend to use Cosmos DB. 14 | 15 | 1. Start local storage: 16 | 17 | ``` 18 | npm run local-storage 19 | ``` 20 | 21 | 1. Start the local API function 22 | 23 | ``` 24 | npm start 25 | ``` 26 | 27 | ## Trivia game data 28 | 29 | * Install the [trivia.json](./trivia.json) data into your Cosmos DB for Core (SQL) API data resource. 30 | * Create a database and collection using the following details: 31 | 32 | |Setting|Value| 33 | |--|--| 34 | |Database ID|`trivia`| 35 | |Container ID|`game`| 36 | |Partition key|`modelType`| 37 | 38 | ## Cosmos DB emulator 39 | 40 | * For local development, install and use the [Cosmos DB emulator](https://aka.ms/cosmosdb-emulator). 41 | 42 | ## Sample queries 43 | 44 | These queries should work in the Apollo sandbox if run in order. 45 | 46 | ### Start the trivia game 47 | 48 | Create a game with a random gameId variable such as "xder". 49 | 50 | ``` 51 | mutation CreateGame { 52 | createGame { 53 | id 54 | state 55 | players { 56 | id 57 | name 58 | } 59 | questions { 60 | id 61 | question 62 | correctAnswer 63 | answers 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | Response 70 | 71 | ```{ 72 | "data": { 73 | "createGame": { 74 | "id": "dfvm", 75 | "state": "WaitingForPlayers", 76 | "players": [], 77 | "questions": [ 78 | { 79 | "id": "30", 80 | "question": "What is the main CPU is the Sega Mega Drive / Sega Genesis?", 81 | "correctAnswer": "Motorola 68000", 82 | "answers": [ 83 | "Intel 8088", 84 | "Yamaha YM2612", 85 | "Motorola 68000", 86 | "Zilog Z80" 87 | ] 88 | }, 89 | ... remaining questions removed for brevity 90 | ] 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | ### Get specific game 97 | 98 | Query for game with gameId such as "pcwf". 99 | 100 | ``` 101 | query Query($gameId: ID!) { 102 | game(id: $gameId) { 103 | id 104 | state 105 | players { 106 | id 107 | name 108 | 109 | } 110 | questions { 111 | id 112 | question 113 | correctAnswer 114 | answers 115 | } 116 | } 117 | } 118 | ``` 119 | 120 | Returns a response such as 121 | 122 | ``` 123 | { 124 | "data": { 125 | "game": { 126 | "id": "pcwf", 127 | "state": "Started", 128 | "players": [ 129 | { 130 | "id": "ukxh", 131 | "name": "dina" 132 | } 133 | ], 134 | "questions": [ 135 | { 136 | "id": "35", 137 | "question": "The C programming language was created by this American computer scientist. ", 138 | "correctAnswer": "Dennis Ritchie", 139 | "answers": [ 140 | "Tim Berners Lee", 141 | "Willis Ware", 142 | "al-Khwārizmī", 143 | "Dennis Ritchie" 144 | ] 145 | }, 146 | ... remaining questions removed for brevity 147 | ] 148 | } 149 | } 150 | } 151 | ``` -------------------------------------------------------------------------------- /api/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: "./graphql/schema.graphql" 3 | documents: "../client/src/operations/*.graphql" 4 | generates: 5 | graphql/generated.ts: 6 | config: 7 | contextType: "./apolloContext#ApolloContext" 8 | mappers: 9 | Question: ./data/types#QuestionModel 10 | Game: ./data/types#GameModel 11 | Player: ./data/types#UserModel 12 | plugins: 13 | - "typescript" 14 | - "typescript-resolvers" 15 | ./graphql.schema.json: 16 | plugins: 17 | - "introspection" 18 | 19 | ../client/src/generated.tsx: 20 | config: 21 | withHooks: true 22 | withHOC: false 23 | withComponent: false 24 | plugins: 25 | - "typescript" 26 | - "typescript-operations" 27 | - "typed-document-node" 28 | hooks: 29 | afterAllFileWrite: 30 | - npx prettier --write 31 | -------------------------------------------------------------------------------- /api/graphql.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "__schema": { 3 | "queryType": { 4 | "name": "Query" 5 | }, 6 | "mutationType": { 7 | "name": "Mutation" 8 | }, 9 | "subscriptionType": null, 10 | "types": [ 11 | { 12 | "kind": "SCALAR", 13 | "name": "Boolean", 14 | "description": "The `Boolean` scalar type represents `true` or `false`.", 15 | "fields": null, 16 | "inputFields": null, 17 | "interfaces": null, 18 | "enumValues": null, 19 | "possibleTypes": null 20 | }, 21 | { 22 | "kind": "OBJECT", 23 | "name": "Game", 24 | "description": null, 25 | "fields": [ 26 | { 27 | "name": "id", 28 | "description": null, 29 | "args": [], 30 | "type": { 31 | "kind": "NON_NULL", 32 | "name": null, 33 | "ofType": { 34 | "kind": "SCALAR", 35 | "name": "ID", 36 | "ofType": null 37 | } 38 | }, 39 | "isDeprecated": false, 40 | "deprecationReason": null 41 | }, 42 | { 43 | "name": "players", 44 | "description": null, 45 | "args": [], 46 | "type": { 47 | "kind": "NON_NULL", 48 | "name": null, 49 | "ofType": { 50 | "kind": "LIST", 51 | "name": null, 52 | "ofType": { 53 | "kind": "NON_NULL", 54 | "name": null, 55 | "ofType": { 56 | "kind": "OBJECT", 57 | "name": "Player", 58 | "ofType": null 59 | } 60 | } 61 | } 62 | }, 63 | "isDeprecated": false, 64 | "deprecationReason": null 65 | }, 66 | { 67 | "name": "questions", 68 | "description": null, 69 | "args": [], 70 | "type": { 71 | "kind": "NON_NULL", 72 | "name": null, 73 | "ofType": { 74 | "kind": "LIST", 75 | "name": null, 76 | "ofType": { 77 | "kind": "NON_NULL", 78 | "name": null, 79 | "ofType": { 80 | "kind": "OBJECT", 81 | "name": "Question", 82 | "ofType": null 83 | } 84 | } 85 | } 86 | }, 87 | "isDeprecated": false, 88 | "deprecationReason": null 89 | }, 90 | { 91 | "name": "state", 92 | "description": null, 93 | "args": [], 94 | "type": { 95 | "kind": "ENUM", 96 | "name": "GameState", 97 | "ofType": null 98 | }, 99 | "isDeprecated": false, 100 | "deprecationReason": null 101 | } 102 | ], 103 | "inputFields": null, 104 | "interfaces": [], 105 | "enumValues": null, 106 | "possibleTypes": null 107 | }, 108 | { 109 | "kind": "ENUM", 110 | "name": "GameState", 111 | "description": null, 112 | "fields": null, 113 | "inputFields": null, 114 | "interfaces": null, 115 | "enumValues": [ 116 | { 117 | "name": "Completed", 118 | "description": null, 119 | "isDeprecated": false, 120 | "deprecationReason": null 121 | }, 122 | { 123 | "name": "Started", 124 | "description": null, 125 | "isDeprecated": false, 126 | "deprecationReason": null 127 | }, 128 | { 129 | "name": "WaitingForPlayers", 130 | "description": null, 131 | "isDeprecated": false, 132 | "deprecationReason": null 133 | } 134 | ], 135 | "possibleTypes": null 136 | }, 137 | { 138 | "kind": "SCALAR", 139 | "name": "ID", 140 | "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", 141 | "fields": null, 142 | "inputFields": null, 143 | "interfaces": null, 144 | "enumValues": null, 145 | "possibleTypes": null 146 | }, 147 | { 148 | "kind": "OBJECT", 149 | "name": "Mutation", 150 | "description": null, 151 | "fields": [ 152 | { 153 | "name": "addPlayerToGame", 154 | "description": null, 155 | "args": [ 156 | { 157 | "name": "id", 158 | "description": null, 159 | "type": { 160 | "kind": "NON_NULL", 161 | "name": null, 162 | "ofType": { 163 | "kind": "SCALAR", 164 | "name": "ID", 165 | "ofType": null 166 | } 167 | }, 168 | "defaultValue": null, 169 | "isDeprecated": false, 170 | "deprecationReason": null 171 | }, 172 | { 173 | "name": "name", 174 | "description": null, 175 | "type": { 176 | "kind": "NON_NULL", 177 | "name": null, 178 | "ofType": { 179 | "kind": "SCALAR", 180 | "name": "String", 181 | "ofType": null 182 | } 183 | }, 184 | "defaultValue": null, 185 | "isDeprecated": false, 186 | "deprecationReason": null 187 | } 188 | ], 189 | "type": { 190 | "kind": "NON_NULL", 191 | "name": null, 192 | "ofType": { 193 | "kind": "OBJECT", 194 | "name": "Player", 195 | "ofType": null 196 | } 197 | }, 198 | "isDeprecated": false, 199 | "deprecationReason": null 200 | }, 201 | { 202 | "name": "createGame", 203 | "description": null, 204 | "args": [], 205 | "type": { 206 | "kind": "OBJECT", 207 | "name": "Game", 208 | "ofType": null 209 | }, 210 | "isDeprecated": false, 211 | "deprecationReason": null 212 | }, 213 | { 214 | "name": "startGame", 215 | "description": null, 216 | "args": [ 217 | { 218 | "name": "id", 219 | "description": null, 220 | "type": { 221 | "kind": "NON_NULL", 222 | "name": null, 223 | "ofType": { 224 | "kind": "SCALAR", 225 | "name": "ID", 226 | "ofType": null 227 | } 228 | }, 229 | "defaultValue": null, 230 | "isDeprecated": false, 231 | "deprecationReason": null 232 | } 233 | ], 234 | "type": { 235 | "kind": "OBJECT", 236 | "name": "Game", 237 | "ofType": null 238 | }, 239 | "isDeprecated": false, 240 | "deprecationReason": null 241 | }, 242 | { 243 | "name": "submitAnswer", 244 | "description": null, 245 | "args": [ 246 | { 247 | "name": "answer", 248 | "description": null, 249 | "type": { 250 | "kind": "NON_NULL", 251 | "name": null, 252 | "ofType": { 253 | "kind": "SCALAR", 254 | "name": "String", 255 | "ofType": null 256 | } 257 | }, 258 | "defaultValue": null, 259 | "isDeprecated": false, 260 | "deprecationReason": null 261 | }, 262 | { 263 | "name": "gameId", 264 | "description": null, 265 | "type": { 266 | "kind": "NON_NULL", 267 | "name": null, 268 | "ofType": { 269 | "kind": "SCALAR", 270 | "name": "ID", 271 | "ofType": null 272 | } 273 | }, 274 | "defaultValue": null, 275 | "isDeprecated": false, 276 | "deprecationReason": null 277 | }, 278 | { 279 | "name": "playerId", 280 | "description": null, 281 | "type": { 282 | "kind": "NON_NULL", 283 | "name": null, 284 | "ofType": { 285 | "kind": "SCALAR", 286 | "name": "ID", 287 | "ofType": null 288 | } 289 | }, 290 | "defaultValue": null, 291 | "isDeprecated": false, 292 | "deprecationReason": null 293 | }, 294 | { 295 | "name": "questionId", 296 | "description": null, 297 | "type": { 298 | "kind": "NON_NULL", 299 | "name": null, 300 | "ofType": { 301 | "kind": "SCALAR", 302 | "name": "ID", 303 | "ofType": null 304 | } 305 | }, 306 | "defaultValue": null, 307 | "isDeprecated": false, 308 | "deprecationReason": null 309 | } 310 | ], 311 | "type": { 312 | "kind": "OBJECT", 313 | "name": "Player", 314 | "ofType": null 315 | }, 316 | "isDeprecated": false, 317 | "deprecationReason": null 318 | } 319 | ], 320 | "inputFields": null, 321 | "interfaces": [], 322 | "enumValues": null, 323 | "possibleTypes": null 324 | }, 325 | { 326 | "kind": "OBJECT", 327 | "name": "Player", 328 | "description": null, 329 | "fields": [ 330 | { 331 | "name": "game", 332 | "description": null, 333 | "args": [ 334 | { 335 | "name": "gameId", 336 | "description": null, 337 | "type": { 338 | "kind": "SCALAR", 339 | "name": "ID", 340 | "ofType": null 341 | }, 342 | "defaultValue": null, 343 | "isDeprecated": false, 344 | "deprecationReason": null 345 | } 346 | ], 347 | "type": { 348 | "kind": "NON_NULL", 349 | "name": null, 350 | "ofType": { 351 | "kind": "OBJECT", 352 | "name": "Game", 353 | "ofType": null 354 | } 355 | }, 356 | "isDeprecated": false, 357 | "deprecationReason": null 358 | }, 359 | { 360 | "name": "games", 361 | "description": null, 362 | "args": [], 363 | "type": { 364 | "kind": "NON_NULL", 365 | "name": null, 366 | "ofType": { 367 | "kind": "LIST", 368 | "name": null, 369 | "ofType": { 370 | "kind": "NON_NULL", 371 | "name": null, 372 | "ofType": { 373 | "kind": "OBJECT", 374 | "name": "Game", 375 | "ofType": null 376 | } 377 | } 378 | } 379 | }, 380 | "isDeprecated": false, 381 | "deprecationReason": null 382 | }, 383 | { 384 | "name": "id", 385 | "description": null, 386 | "args": [], 387 | "type": { 388 | "kind": "NON_NULL", 389 | "name": null, 390 | "ofType": { 391 | "kind": "SCALAR", 392 | "name": "ID", 393 | "ofType": null 394 | } 395 | }, 396 | "isDeprecated": false, 397 | "deprecationReason": null 398 | }, 399 | { 400 | "name": "name", 401 | "description": null, 402 | "args": [], 403 | "type": { 404 | "kind": "NON_NULL", 405 | "name": null, 406 | "ofType": { 407 | "kind": "SCALAR", 408 | "name": "String", 409 | "ofType": null 410 | } 411 | }, 412 | "isDeprecated": false, 413 | "deprecationReason": null 414 | } 415 | ], 416 | "inputFields": null, 417 | "interfaces": [], 418 | "enumValues": null, 419 | "possibleTypes": null 420 | }, 421 | { 422 | "kind": "OBJECT", 423 | "name": "PlayerResult", 424 | "description": null, 425 | "fields": [ 426 | { 427 | "name": "answers", 428 | "description": null, 429 | "args": [], 430 | "type": { 431 | "kind": "NON_NULL", 432 | "name": null, 433 | "ofType": { 434 | "kind": "LIST", 435 | "name": null, 436 | "ofType": { 437 | "kind": "NON_NULL", 438 | "name": null, 439 | "ofType": { 440 | "kind": "SCALAR", 441 | "name": "String", 442 | "ofType": null 443 | } 444 | } 445 | } 446 | }, 447 | "isDeprecated": false, 448 | "deprecationReason": null 449 | }, 450 | { 451 | "name": "correct", 452 | "description": null, 453 | "args": [], 454 | "type": { 455 | "kind": "SCALAR", 456 | "name": "Boolean", 457 | "ofType": null 458 | }, 459 | "isDeprecated": false, 460 | "deprecationReason": null 461 | }, 462 | { 463 | "name": "correctAnswer", 464 | "description": null, 465 | "args": [], 466 | "type": { 467 | "kind": "NON_NULL", 468 | "name": null, 469 | "ofType": { 470 | "kind": "SCALAR", 471 | "name": "String", 472 | "ofType": null 473 | } 474 | }, 475 | "isDeprecated": false, 476 | "deprecationReason": null 477 | }, 478 | { 479 | "name": "name", 480 | "description": null, 481 | "args": [], 482 | "type": { 483 | "kind": "NON_NULL", 484 | "name": null, 485 | "ofType": { 486 | "kind": "SCALAR", 487 | "name": "String", 488 | "ofType": null 489 | } 490 | }, 491 | "isDeprecated": false, 492 | "deprecationReason": null 493 | }, 494 | { 495 | "name": "question", 496 | "description": null, 497 | "args": [], 498 | "type": { 499 | "kind": "NON_NULL", 500 | "name": null, 501 | "ofType": { 502 | "kind": "SCALAR", 503 | "name": "String", 504 | "ofType": null 505 | } 506 | }, 507 | "isDeprecated": false, 508 | "deprecationReason": null 509 | }, 510 | { 511 | "name": "submittedAnswer", 512 | "description": null, 513 | "args": [], 514 | "type": { 515 | "kind": "NON_NULL", 516 | "name": null, 517 | "ofType": { 518 | "kind": "SCALAR", 519 | "name": "String", 520 | "ofType": null 521 | } 522 | }, 523 | "isDeprecated": false, 524 | "deprecationReason": null 525 | } 526 | ], 527 | "inputFields": null, 528 | "interfaces": [], 529 | "enumValues": null, 530 | "possibleTypes": null 531 | }, 532 | { 533 | "kind": "OBJECT", 534 | "name": "Query", 535 | "description": null, 536 | "fields": [ 537 | { 538 | "name": "game", 539 | "description": null, 540 | "args": [ 541 | { 542 | "name": "id", 543 | "description": null, 544 | "type": { 545 | "kind": "NON_NULL", 546 | "name": null, 547 | "ofType": { 548 | "kind": "SCALAR", 549 | "name": "ID", 550 | "ofType": null 551 | } 552 | }, 553 | "defaultValue": null, 554 | "isDeprecated": false, 555 | "deprecationReason": null 556 | } 557 | ], 558 | "type": { 559 | "kind": "OBJECT", 560 | "name": "Game", 561 | "ofType": null 562 | }, 563 | "isDeprecated": false, 564 | "deprecationReason": null 565 | }, 566 | { 567 | "name": "games", 568 | "description": null, 569 | "args": [], 570 | "type": { 571 | "kind": "NON_NULL", 572 | "name": null, 573 | "ofType": { 574 | "kind": "LIST", 575 | "name": null, 576 | "ofType": { 577 | "kind": "NON_NULL", 578 | "name": null, 579 | "ofType": { 580 | "kind": "OBJECT", 581 | "name": "Game", 582 | "ofType": null 583 | } 584 | } 585 | } 586 | }, 587 | "isDeprecated": false, 588 | "deprecationReason": null 589 | }, 590 | { 591 | "name": "playerResults", 592 | "description": null, 593 | "args": [ 594 | { 595 | "name": "gameId", 596 | "description": null, 597 | "type": { 598 | "kind": "NON_NULL", 599 | "name": null, 600 | "ofType": { 601 | "kind": "SCALAR", 602 | "name": "ID", 603 | "ofType": null 604 | } 605 | }, 606 | "defaultValue": null, 607 | "isDeprecated": false, 608 | "deprecationReason": null 609 | }, 610 | { 611 | "name": "playerId", 612 | "description": null, 613 | "type": { 614 | "kind": "NON_NULL", 615 | "name": null, 616 | "ofType": { 617 | "kind": "SCALAR", 618 | "name": "ID", 619 | "ofType": null 620 | } 621 | }, 622 | "defaultValue": null, 623 | "isDeprecated": false, 624 | "deprecationReason": null 625 | } 626 | ], 627 | "type": { 628 | "kind": "NON_NULL", 629 | "name": null, 630 | "ofType": { 631 | "kind": "LIST", 632 | "name": null, 633 | "ofType": { 634 | "kind": "NON_NULL", 635 | "name": null, 636 | "ofType": { 637 | "kind": "OBJECT", 638 | "name": "PlayerResult", 639 | "ofType": null 640 | } 641 | } 642 | } 643 | }, 644 | "isDeprecated": false, 645 | "deprecationReason": null 646 | } 647 | ], 648 | "inputFields": null, 649 | "interfaces": [], 650 | "enumValues": null, 651 | "possibleTypes": null 652 | }, 653 | { 654 | "kind": "OBJECT", 655 | "name": "Question", 656 | "description": null, 657 | "fields": [ 658 | { 659 | "name": "answers", 660 | "description": null, 661 | "args": [], 662 | "type": { 663 | "kind": "NON_NULL", 664 | "name": null, 665 | "ofType": { 666 | "kind": "LIST", 667 | "name": null, 668 | "ofType": { 669 | "kind": "NON_NULL", 670 | "name": null, 671 | "ofType": { 672 | "kind": "SCALAR", 673 | "name": "String", 674 | "ofType": null 675 | } 676 | } 677 | } 678 | }, 679 | "isDeprecated": false, 680 | "deprecationReason": null 681 | }, 682 | { 683 | "name": "correctAnswer", 684 | "description": null, 685 | "args": [], 686 | "type": { 687 | "kind": "NON_NULL", 688 | "name": null, 689 | "ofType": { 690 | "kind": "SCALAR", 691 | "name": "String", 692 | "ofType": null 693 | } 694 | }, 695 | "isDeprecated": false, 696 | "deprecationReason": null 697 | }, 698 | { 699 | "name": "id", 700 | "description": null, 701 | "args": [], 702 | "type": { 703 | "kind": "NON_NULL", 704 | "name": null, 705 | "ofType": { 706 | "kind": "SCALAR", 707 | "name": "ID", 708 | "ofType": null 709 | } 710 | }, 711 | "isDeprecated": false, 712 | "deprecationReason": null 713 | }, 714 | { 715 | "name": "question", 716 | "description": null, 717 | "args": [], 718 | "type": { 719 | "kind": "NON_NULL", 720 | "name": null, 721 | "ofType": { 722 | "kind": "SCALAR", 723 | "name": "String", 724 | "ofType": null 725 | } 726 | }, 727 | "isDeprecated": false, 728 | "deprecationReason": null 729 | } 730 | ], 731 | "inputFields": null, 732 | "interfaces": [], 733 | "enumValues": null, 734 | "possibleTypes": null 735 | }, 736 | { 737 | "kind": "SCALAR", 738 | "name": "String", 739 | "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", 740 | "fields": null, 741 | "inputFields": null, 742 | "interfaces": null, 743 | "enumValues": null, 744 | "possibleTypes": null 745 | }, 746 | { 747 | "kind": "OBJECT", 748 | "name": "__Directive", 749 | "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", 750 | "fields": [ 751 | { 752 | "name": "name", 753 | "description": null, 754 | "args": [], 755 | "type": { 756 | "kind": "NON_NULL", 757 | "name": null, 758 | "ofType": { 759 | "kind": "SCALAR", 760 | "name": "String", 761 | "ofType": null 762 | } 763 | }, 764 | "isDeprecated": false, 765 | "deprecationReason": null 766 | }, 767 | { 768 | "name": "description", 769 | "description": null, 770 | "args": [], 771 | "type": { 772 | "kind": "SCALAR", 773 | "name": "String", 774 | "ofType": null 775 | }, 776 | "isDeprecated": false, 777 | "deprecationReason": null 778 | }, 779 | { 780 | "name": "isRepeatable", 781 | "description": null, 782 | "args": [], 783 | "type": { 784 | "kind": "NON_NULL", 785 | "name": null, 786 | "ofType": { 787 | "kind": "SCALAR", 788 | "name": "Boolean", 789 | "ofType": null 790 | } 791 | }, 792 | "isDeprecated": false, 793 | "deprecationReason": null 794 | }, 795 | { 796 | "name": "locations", 797 | "description": null, 798 | "args": [], 799 | "type": { 800 | "kind": "NON_NULL", 801 | "name": null, 802 | "ofType": { 803 | "kind": "LIST", 804 | "name": null, 805 | "ofType": { 806 | "kind": "NON_NULL", 807 | "name": null, 808 | "ofType": { 809 | "kind": "ENUM", 810 | "name": "__DirectiveLocation", 811 | "ofType": null 812 | } 813 | } 814 | } 815 | }, 816 | "isDeprecated": false, 817 | "deprecationReason": null 818 | }, 819 | { 820 | "name": "args", 821 | "description": null, 822 | "args": [ 823 | { 824 | "name": "includeDeprecated", 825 | "description": null, 826 | "type": { 827 | "kind": "SCALAR", 828 | "name": "Boolean", 829 | "ofType": null 830 | }, 831 | "defaultValue": "false", 832 | "isDeprecated": false, 833 | "deprecationReason": null 834 | } 835 | ], 836 | "type": { 837 | "kind": "NON_NULL", 838 | "name": null, 839 | "ofType": { 840 | "kind": "LIST", 841 | "name": null, 842 | "ofType": { 843 | "kind": "NON_NULL", 844 | "name": null, 845 | "ofType": { 846 | "kind": "OBJECT", 847 | "name": "__InputValue", 848 | "ofType": null 849 | } 850 | } 851 | } 852 | }, 853 | "isDeprecated": false, 854 | "deprecationReason": null 855 | } 856 | ], 857 | "inputFields": null, 858 | "interfaces": [], 859 | "enumValues": null, 860 | "possibleTypes": null 861 | }, 862 | { 863 | "kind": "ENUM", 864 | "name": "__DirectiveLocation", 865 | "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", 866 | "fields": null, 867 | "inputFields": null, 868 | "interfaces": null, 869 | "enumValues": [ 870 | { 871 | "name": "QUERY", 872 | "description": "Location adjacent to a query operation.", 873 | "isDeprecated": false, 874 | "deprecationReason": null 875 | }, 876 | { 877 | "name": "MUTATION", 878 | "description": "Location adjacent to a mutation operation.", 879 | "isDeprecated": false, 880 | "deprecationReason": null 881 | }, 882 | { 883 | "name": "SUBSCRIPTION", 884 | "description": "Location adjacent to a subscription operation.", 885 | "isDeprecated": false, 886 | "deprecationReason": null 887 | }, 888 | { 889 | "name": "FIELD", 890 | "description": "Location adjacent to a field.", 891 | "isDeprecated": false, 892 | "deprecationReason": null 893 | }, 894 | { 895 | "name": "FRAGMENT_DEFINITION", 896 | "description": "Location adjacent to a fragment definition.", 897 | "isDeprecated": false, 898 | "deprecationReason": null 899 | }, 900 | { 901 | "name": "FRAGMENT_SPREAD", 902 | "description": "Location adjacent to a fragment spread.", 903 | "isDeprecated": false, 904 | "deprecationReason": null 905 | }, 906 | { 907 | "name": "INLINE_FRAGMENT", 908 | "description": "Location adjacent to an inline fragment.", 909 | "isDeprecated": false, 910 | "deprecationReason": null 911 | }, 912 | { 913 | "name": "VARIABLE_DEFINITION", 914 | "description": "Location adjacent to a variable definition.", 915 | "isDeprecated": false, 916 | "deprecationReason": null 917 | }, 918 | { 919 | "name": "SCHEMA", 920 | "description": "Location adjacent to a schema definition.", 921 | "isDeprecated": false, 922 | "deprecationReason": null 923 | }, 924 | { 925 | "name": "SCALAR", 926 | "description": "Location adjacent to a scalar definition.", 927 | "isDeprecated": false, 928 | "deprecationReason": null 929 | }, 930 | { 931 | "name": "OBJECT", 932 | "description": "Location adjacent to an object type definition.", 933 | "isDeprecated": false, 934 | "deprecationReason": null 935 | }, 936 | { 937 | "name": "FIELD_DEFINITION", 938 | "description": "Location adjacent to a field definition.", 939 | "isDeprecated": false, 940 | "deprecationReason": null 941 | }, 942 | { 943 | "name": "ARGUMENT_DEFINITION", 944 | "description": "Location adjacent to an argument definition.", 945 | "isDeprecated": false, 946 | "deprecationReason": null 947 | }, 948 | { 949 | "name": "INTERFACE", 950 | "description": "Location adjacent to an interface definition.", 951 | "isDeprecated": false, 952 | "deprecationReason": null 953 | }, 954 | { 955 | "name": "UNION", 956 | "description": "Location adjacent to a union definition.", 957 | "isDeprecated": false, 958 | "deprecationReason": null 959 | }, 960 | { 961 | "name": "ENUM", 962 | "description": "Location adjacent to an enum definition.", 963 | "isDeprecated": false, 964 | "deprecationReason": null 965 | }, 966 | { 967 | "name": "ENUM_VALUE", 968 | "description": "Location adjacent to an enum value definition.", 969 | "isDeprecated": false, 970 | "deprecationReason": null 971 | }, 972 | { 973 | "name": "INPUT_OBJECT", 974 | "description": "Location adjacent to an input object type definition.", 975 | "isDeprecated": false, 976 | "deprecationReason": null 977 | }, 978 | { 979 | "name": "INPUT_FIELD_DEFINITION", 980 | "description": "Location adjacent to an input object field definition.", 981 | "isDeprecated": false, 982 | "deprecationReason": null 983 | } 984 | ], 985 | "possibleTypes": null 986 | }, 987 | { 988 | "kind": "OBJECT", 989 | "name": "__EnumValue", 990 | "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", 991 | "fields": [ 992 | { 993 | "name": "name", 994 | "description": null, 995 | "args": [], 996 | "type": { 997 | "kind": "NON_NULL", 998 | "name": null, 999 | "ofType": { 1000 | "kind": "SCALAR", 1001 | "name": "String", 1002 | "ofType": null 1003 | } 1004 | }, 1005 | "isDeprecated": false, 1006 | "deprecationReason": null 1007 | }, 1008 | { 1009 | "name": "description", 1010 | "description": null, 1011 | "args": [], 1012 | "type": { 1013 | "kind": "SCALAR", 1014 | "name": "String", 1015 | "ofType": null 1016 | }, 1017 | "isDeprecated": false, 1018 | "deprecationReason": null 1019 | }, 1020 | { 1021 | "name": "isDeprecated", 1022 | "description": null, 1023 | "args": [], 1024 | "type": { 1025 | "kind": "NON_NULL", 1026 | "name": null, 1027 | "ofType": { 1028 | "kind": "SCALAR", 1029 | "name": "Boolean", 1030 | "ofType": null 1031 | } 1032 | }, 1033 | "isDeprecated": false, 1034 | "deprecationReason": null 1035 | }, 1036 | { 1037 | "name": "deprecationReason", 1038 | "description": null, 1039 | "args": [], 1040 | "type": { 1041 | "kind": "SCALAR", 1042 | "name": "String", 1043 | "ofType": null 1044 | }, 1045 | "isDeprecated": false, 1046 | "deprecationReason": null 1047 | } 1048 | ], 1049 | "inputFields": null, 1050 | "interfaces": [], 1051 | "enumValues": null, 1052 | "possibleTypes": null 1053 | }, 1054 | { 1055 | "kind": "OBJECT", 1056 | "name": "__Field", 1057 | "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", 1058 | "fields": [ 1059 | { 1060 | "name": "name", 1061 | "description": null, 1062 | "args": [], 1063 | "type": { 1064 | "kind": "NON_NULL", 1065 | "name": null, 1066 | "ofType": { 1067 | "kind": "SCALAR", 1068 | "name": "String", 1069 | "ofType": null 1070 | } 1071 | }, 1072 | "isDeprecated": false, 1073 | "deprecationReason": null 1074 | }, 1075 | { 1076 | "name": "description", 1077 | "description": null, 1078 | "args": [], 1079 | "type": { 1080 | "kind": "SCALAR", 1081 | "name": "String", 1082 | "ofType": null 1083 | }, 1084 | "isDeprecated": false, 1085 | "deprecationReason": null 1086 | }, 1087 | { 1088 | "name": "args", 1089 | "description": null, 1090 | "args": [ 1091 | { 1092 | "name": "includeDeprecated", 1093 | "description": null, 1094 | "type": { 1095 | "kind": "SCALAR", 1096 | "name": "Boolean", 1097 | "ofType": null 1098 | }, 1099 | "defaultValue": "false", 1100 | "isDeprecated": false, 1101 | "deprecationReason": null 1102 | } 1103 | ], 1104 | "type": { 1105 | "kind": "NON_NULL", 1106 | "name": null, 1107 | "ofType": { 1108 | "kind": "LIST", 1109 | "name": null, 1110 | "ofType": { 1111 | "kind": "NON_NULL", 1112 | "name": null, 1113 | "ofType": { 1114 | "kind": "OBJECT", 1115 | "name": "__InputValue", 1116 | "ofType": null 1117 | } 1118 | } 1119 | } 1120 | }, 1121 | "isDeprecated": false, 1122 | "deprecationReason": null 1123 | }, 1124 | { 1125 | "name": "type", 1126 | "description": null, 1127 | "args": [], 1128 | "type": { 1129 | "kind": "NON_NULL", 1130 | "name": null, 1131 | "ofType": { 1132 | "kind": "OBJECT", 1133 | "name": "__Type", 1134 | "ofType": null 1135 | } 1136 | }, 1137 | "isDeprecated": false, 1138 | "deprecationReason": null 1139 | }, 1140 | { 1141 | "name": "isDeprecated", 1142 | "description": null, 1143 | "args": [], 1144 | "type": { 1145 | "kind": "NON_NULL", 1146 | "name": null, 1147 | "ofType": { 1148 | "kind": "SCALAR", 1149 | "name": "Boolean", 1150 | "ofType": null 1151 | } 1152 | }, 1153 | "isDeprecated": false, 1154 | "deprecationReason": null 1155 | }, 1156 | { 1157 | "name": "deprecationReason", 1158 | "description": null, 1159 | "args": [], 1160 | "type": { 1161 | "kind": "SCALAR", 1162 | "name": "String", 1163 | "ofType": null 1164 | }, 1165 | "isDeprecated": false, 1166 | "deprecationReason": null 1167 | } 1168 | ], 1169 | "inputFields": null, 1170 | "interfaces": [], 1171 | "enumValues": null, 1172 | "possibleTypes": null 1173 | }, 1174 | { 1175 | "kind": "OBJECT", 1176 | "name": "__InputValue", 1177 | "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", 1178 | "fields": [ 1179 | { 1180 | "name": "name", 1181 | "description": null, 1182 | "args": [], 1183 | "type": { 1184 | "kind": "NON_NULL", 1185 | "name": null, 1186 | "ofType": { 1187 | "kind": "SCALAR", 1188 | "name": "String", 1189 | "ofType": null 1190 | } 1191 | }, 1192 | "isDeprecated": false, 1193 | "deprecationReason": null 1194 | }, 1195 | { 1196 | "name": "description", 1197 | "description": null, 1198 | "args": [], 1199 | "type": { 1200 | "kind": "SCALAR", 1201 | "name": "String", 1202 | "ofType": null 1203 | }, 1204 | "isDeprecated": false, 1205 | "deprecationReason": null 1206 | }, 1207 | { 1208 | "name": "type", 1209 | "description": null, 1210 | "args": [], 1211 | "type": { 1212 | "kind": "NON_NULL", 1213 | "name": null, 1214 | "ofType": { 1215 | "kind": "OBJECT", 1216 | "name": "__Type", 1217 | "ofType": null 1218 | } 1219 | }, 1220 | "isDeprecated": false, 1221 | "deprecationReason": null 1222 | }, 1223 | { 1224 | "name": "defaultValue", 1225 | "description": "A GraphQL-formatted string representing the default value for this input value.", 1226 | "args": [], 1227 | "type": { 1228 | "kind": "SCALAR", 1229 | "name": "String", 1230 | "ofType": null 1231 | }, 1232 | "isDeprecated": false, 1233 | "deprecationReason": null 1234 | }, 1235 | { 1236 | "name": "isDeprecated", 1237 | "description": null, 1238 | "args": [], 1239 | "type": { 1240 | "kind": "NON_NULL", 1241 | "name": null, 1242 | "ofType": { 1243 | "kind": "SCALAR", 1244 | "name": "Boolean", 1245 | "ofType": null 1246 | } 1247 | }, 1248 | "isDeprecated": false, 1249 | "deprecationReason": null 1250 | }, 1251 | { 1252 | "name": "deprecationReason", 1253 | "description": null, 1254 | "args": [], 1255 | "type": { 1256 | "kind": "SCALAR", 1257 | "name": "String", 1258 | "ofType": null 1259 | }, 1260 | "isDeprecated": false, 1261 | "deprecationReason": null 1262 | } 1263 | ], 1264 | "inputFields": null, 1265 | "interfaces": [], 1266 | "enumValues": null, 1267 | "possibleTypes": null 1268 | }, 1269 | { 1270 | "kind": "OBJECT", 1271 | "name": "__Schema", 1272 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", 1273 | "fields": [ 1274 | { 1275 | "name": "description", 1276 | "description": null, 1277 | "args": [], 1278 | "type": { 1279 | "kind": "SCALAR", 1280 | "name": "String", 1281 | "ofType": null 1282 | }, 1283 | "isDeprecated": false, 1284 | "deprecationReason": null 1285 | }, 1286 | { 1287 | "name": "types", 1288 | "description": "A list of all types supported by this server.", 1289 | "args": [], 1290 | "type": { 1291 | "kind": "NON_NULL", 1292 | "name": null, 1293 | "ofType": { 1294 | "kind": "LIST", 1295 | "name": null, 1296 | "ofType": { 1297 | "kind": "NON_NULL", 1298 | "name": null, 1299 | "ofType": { 1300 | "kind": "OBJECT", 1301 | "name": "__Type", 1302 | "ofType": null 1303 | } 1304 | } 1305 | } 1306 | }, 1307 | "isDeprecated": false, 1308 | "deprecationReason": null 1309 | }, 1310 | { 1311 | "name": "queryType", 1312 | "description": "The type that query operations will be rooted at.", 1313 | "args": [], 1314 | "type": { 1315 | "kind": "NON_NULL", 1316 | "name": null, 1317 | "ofType": { 1318 | "kind": "OBJECT", 1319 | "name": "__Type", 1320 | "ofType": null 1321 | } 1322 | }, 1323 | "isDeprecated": false, 1324 | "deprecationReason": null 1325 | }, 1326 | { 1327 | "name": "mutationType", 1328 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 1329 | "args": [], 1330 | "type": { 1331 | "kind": "OBJECT", 1332 | "name": "__Type", 1333 | "ofType": null 1334 | }, 1335 | "isDeprecated": false, 1336 | "deprecationReason": null 1337 | }, 1338 | { 1339 | "name": "subscriptionType", 1340 | "description": "If this server support subscription, the type that subscription operations will be rooted at.", 1341 | "args": [], 1342 | "type": { 1343 | "kind": "OBJECT", 1344 | "name": "__Type", 1345 | "ofType": null 1346 | }, 1347 | "isDeprecated": false, 1348 | "deprecationReason": null 1349 | }, 1350 | { 1351 | "name": "directives", 1352 | "description": "A list of all directives supported by this server.", 1353 | "args": [], 1354 | "type": { 1355 | "kind": "NON_NULL", 1356 | "name": null, 1357 | "ofType": { 1358 | "kind": "LIST", 1359 | "name": null, 1360 | "ofType": { 1361 | "kind": "NON_NULL", 1362 | "name": null, 1363 | "ofType": { 1364 | "kind": "OBJECT", 1365 | "name": "__Directive", 1366 | "ofType": null 1367 | } 1368 | } 1369 | } 1370 | }, 1371 | "isDeprecated": false, 1372 | "deprecationReason": null 1373 | } 1374 | ], 1375 | "inputFields": null, 1376 | "interfaces": [], 1377 | "enumValues": null, 1378 | "possibleTypes": null 1379 | }, 1380 | { 1381 | "kind": "OBJECT", 1382 | "name": "__Type", 1383 | "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", 1384 | "fields": [ 1385 | { 1386 | "name": "kind", 1387 | "description": null, 1388 | "args": [], 1389 | "type": { 1390 | "kind": "NON_NULL", 1391 | "name": null, 1392 | "ofType": { 1393 | "kind": "ENUM", 1394 | "name": "__TypeKind", 1395 | "ofType": null 1396 | } 1397 | }, 1398 | "isDeprecated": false, 1399 | "deprecationReason": null 1400 | }, 1401 | { 1402 | "name": "name", 1403 | "description": null, 1404 | "args": [], 1405 | "type": { 1406 | "kind": "SCALAR", 1407 | "name": "String", 1408 | "ofType": null 1409 | }, 1410 | "isDeprecated": false, 1411 | "deprecationReason": null 1412 | }, 1413 | { 1414 | "name": "description", 1415 | "description": null, 1416 | "args": [], 1417 | "type": { 1418 | "kind": "SCALAR", 1419 | "name": "String", 1420 | "ofType": null 1421 | }, 1422 | "isDeprecated": false, 1423 | "deprecationReason": null 1424 | }, 1425 | { 1426 | "name": "specifiedByURL", 1427 | "description": null, 1428 | "args": [], 1429 | "type": { 1430 | "kind": "SCALAR", 1431 | "name": "String", 1432 | "ofType": null 1433 | }, 1434 | "isDeprecated": false, 1435 | "deprecationReason": null 1436 | }, 1437 | { 1438 | "name": "fields", 1439 | "description": null, 1440 | "args": [ 1441 | { 1442 | "name": "includeDeprecated", 1443 | "description": null, 1444 | "type": { 1445 | "kind": "SCALAR", 1446 | "name": "Boolean", 1447 | "ofType": null 1448 | }, 1449 | "defaultValue": "false", 1450 | "isDeprecated": false, 1451 | "deprecationReason": null 1452 | } 1453 | ], 1454 | "type": { 1455 | "kind": "LIST", 1456 | "name": null, 1457 | "ofType": { 1458 | "kind": "NON_NULL", 1459 | "name": null, 1460 | "ofType": { 1461 | "kind": "OBJECT", 1462 | "name": "__Field", 1463 | "ofType": null 1464 | } 1465 | } 1466 | }, 1467 | "isDeprecated": false, 1468 | "deprecationReason": null 1469 | }, 1470 | { 1471 | "name": "interfaces", 1472 | "description": null, 1473 | "args": [], 1474 | "type": { 1475 | "kind": "LIST", 1476 | "name": null, 1477 | "ofType": { 1478 | "kind": "NON_NULL", 1479 | "name": null, 1480 | "ofType": { 1481 | "kind": "OBJECT", 1482 | "name": "__Type", 1483 | "ofType": null 1484 | } 1485 | } 1486 | }, 1487 | "isDeprecated": false, 1488 | "deprecationReason": null 1489 | }, 1490 | { 1491 | "name": "possibleTypes", 1492 | "description": null, 1493 | "args": [], 1494 | "type": { 1495 | "kind": "LIST", 1496 | "name": null, 1497 | "ofType": { 1498 | "kind": "NON_NULL", 1499 | "name": null, 1500 | "ofType": { 1501 | "kind": "OBJECT", 1502 | "name": "__Type", 1503 | "ofType": null 1504 | } 1505 | } 1506 | }, 1507 | "isDeprecated": false, 1508 | "deprecationReason": null 1509 | }, 1510 | { 1511 | "name": "enumValues", 1512 | "description": null, 1513 | "args": [ 1514 | { 1515 | "name": "includeDeprecated", 1516 | "description": null, 1517 | "type": { 1518 | "kind": "SCALAR", 1519 | "name": "Boolean", 1520 | "ofType": null 1521 | }, 1522 | "defaultValue": "false", 1523 | "isDeprecated": false, 1524 | "deprecationReason": null 1525 | } 1526 | ], 1527 | "type": { 1528 | "kind": "LIST", 1529 | "name": null, 1530 | "ofType": { 1531 | "kind": "NON_NULL", 1532 | "name": null, 1533 | "ofType": { 1534 | "kind": "OBJECT", 1535 | "name": "__EnumValue", 1536 | "ofType": null 1537 | } 1538 | } 1539 | }, 1540 | "isDeprecated": false, 1541 | "deprecationReason": null 1542 | }, 1543 | { 1544 | "name": "inputFields", 1545 | "description": null, 1546 | "args": [ 1547 | { 1548 | "name": "includeDeprecated", 1549 | "description": null, 1550 | "type": { 1551 | "kind": "SCALAR", 1552 | "name": "Boolean", 1553 | "ofType": null 1554 | }, 1555 | "defaultValue": "false", 1556 | "isDeprecated": false, 1557 | "deprecationReason": null 1558 | } 1559 | ], 1560 | "type": { 1561 | "kind": "LIST", 1562 | "name": null, 1563 | "ofType": { 1564 | "kind": "NON_NULL", 1565 | "name": null, 1566 | "ofType": { 1567 | "kind": "OBJECT", 1568 | "name": "__InputValue", 1569 | "ofType": null 1570 | } 1571 | } 1572 | }, 1573 | "isDeprecated": false, 1574 | "deprecationReason": null 1575 | }, 1576 | { 1577 | "name": "ofType", 1578 | "description": null, 1579 | "args": [], 1580 | "type": { 1581 | "kind": "OBJECT", 1582 | "name": "__Type", 1583 | "ofType": null 1584 | }, 1585 | "isDeprecated": false, 1586 | "deprecationReason": null 1587 | } 1588 | ], 1589 | "inputFields": null, 1590 | "interfaces": [], 1591 | "enumValues": null, 1592 | "possibleTypes": null 1593 | }, 1594 | { 1595 | "kind": "ENUM", 1596 | "name": "__TypeKind", 1597 | "description": "An enum describing what kind of type a given `__Type` is.", 1598 | "fields": null, 1599 | "inputFields": null, 1600 | "interfaces": null, 1601 | "enumValues": [ 1602 | { 1603 | "name": "SCALAR", 1604 | "description": "Indicates this type is a scalar.", 1605 | "isDeprecated": false, 1606 | "deprecationReason": null 1607 | }, 1608 | { 1609 | "name": "OBJECT", 1610 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 1611 | "isDeprecated": false, 1612 | "deprecationReason": null 1613 | }, 1614 | { 1615 | "name": "INTERFACE", 1616 | "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", 1617 | "isDeprecated": false, 1618 | "deprecationReason": null 1619 | }, 1620 | { 1621 | "name": "UNION", 1622 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.", 1623 | "isDeprecated": false, 1624 | "deprecationReason": null 1625 | }, 1626 | { 1627 | "name": "ENUM", 1628 | "description": "Indicates this type is an enum. `enumValues` is a valid field.", 1629 | "isDeprecated": false, 1630 | "deprecationReason": null 1631 | }, 1632 | { 1633 | "name": "INPUT_OBJECT", 1634 | "description": "Indicates this type is an input object. `inputFields` is a valid field.", 1635 | "isDeprecated": false, 1636 | "deprecationReason": null 1637 | }, 1638 | { 1639 | "name": "LIST", 1640 | "description": "Indicates this type is a list. `ofType` is a valid field.", 1641 | "isDeprecated": false, 1642 | "deprecationReason": null 1643 | }, 1644 | { 1645 | "name": "NON_NULL", 1646 | "description": "Indicates this type is a non-null. `ofType` is a valid field.", 1647 | "isDeprecated": false, 1648 | "deprecationReason": null 1649 | } 1650 | ], 1651 | "possibleTypes": null 1652 | } 1653 | ], 1654 | "directives": [ 1655 | { 1656 | "name": "deprecated", 1657 | "description": "Marks an element of a GraphQL schema as no longer supported.", 1658 | "isRepeatable": false, 1659 | "locations": [ 1660 | "ARGUMENT_DEFINITION", 1661 | "ENUM_VALUE", 1662 | "FIELD_DEFINITION", 1663 | "INPUT_FIELD_DEFINITION" 1664 | ], 1665 | "args": [ 1666 | { 1667 | "name": "reason", 1668 | "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).", 1669 | "type": { 1670 | "kind": "SCALAR", 1671 | "name": "String", 1672 | "ofType": null 1673 | }, 1674 | "defaultValue": "\"No longer supported\"", 1675 | "isDeprecated": false, 1676 | "deprecationReason": null 1677 | } 1678 | ] 1679 | }, 1680 | { 1681 | "name": "include", 1682 | "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", 1683 | "isRepeatable": false, 1684 | "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], 1685 | "args": [ 1686 | { 1687 | "name": "if", 1688 | "description": "Included when true.", 1689 | "type": { 1690 | "kind": "NON_NULL", 1691 | "name": null, 1692 | "ofType": { 1693 | "kind": "SCALAR", 1694 | "name": "Boolean", 1695 | "ofType": null 1696 | } 1697 | }, 1698 | "defaultValue": null, 1699 | "isDeprecated": false, 1700 | "deprecationReason": null 1701 | } 1702 | ] 1703 | }, 1704 | { 1705 | "name": "skip", 1706 | "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", 1707 | "isRepeatable": false, 1708 | "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], 1709 | "args": [ 1710 | { 1711 | "name": "if", 1712 | "description": "Skipped when true.", 1713 | "type": { 1714 | "kind": "NON_NULL", 1715 | "name": null, 1716 | "ofType": { 1717 | "kind": "SCALAR", 1718 | "name": "Boolean", 1719 | "ofType": null 1720 | } 1721 | }, 1722 | "defaultValue": null, 1723 | "isDeprecated": false, 1724 | "deprecationReason": null 1725 | } 1726 | ] 1727 | }, 1728 | { 1729 | "name": "specifiedBy", 1730 | "description": "Exposes a URL that specifies the behavior of this scalar.", 1731 | "isRepeatable": false, 1732 | "locations": ["SCALAR"], 1733 | "args": [ 1734 | { 1735 | "name": "url", 1736 | "description": "The URL that specifies the behavior of this scalar.", 1737 | "type": { 1738 | "kind": "NON_NULL", 1739 | "name": null, 1740 | "ofType": { 1741 | "kind": "SCALAR", 1742 | "name": "String", 1743 | "ofType": null 1744 | } 1745 | }, 1746 | "defaultValue": null, 1747 | "isDeprecated": false, 1748 | "deprecationReason": null 1749 | } 1750 | ] 1751 | } 1752 | ] 1753 | } 1754 | } 1755 | -------------------------------------------------------------------------------- /api/graphql/apolloContext.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IGameDataSource, 3 | IQuestionDataSource, 4 | IUserDataSource, 5 | } from "./data/types"; 6 | 7 | export type ApolloContext = { 8 | dataSources: { 9 | user: IUserDataSource; 10 | game: IGameDataSource; 11 | question: IQuestionDataSource; 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /api/graphql/data/cosmos/GameDataSource.ts: -------------------------------------------------------------------------------- 1 | import { CosmosDataSource } from "apollo-datasource-cosmosdb"; 2 | import { arrayRandomiser, idGenerator } from "../../../utils"; 3 | import { GameState } from "../../generated"; 4 | import { IGameDataSource, GameModel, ModelType, QuestionModel } from "../types"; 5 | 6 | export class GameDataSource 7 | extends CosmosDataSource 8 | implements IGameDataSource 9 | { 10 | async getUserGames(userId: string) { 11 | const response = await this.findManyByQuery({ 12 | query: ` 13 | SELECT * 14 | FROM c 15 | WHERE c.modelType = @type 16 | AND EXISTS(SELECT p.id FROM p IN c.players WHERE p.id = @id)`, 17 | parameters: [ 18 | { name: "@id", value: userId }, 19 | { name: "@type", value: ModelType.Game }, 20 | ], 21 | }); 22 | 23 | return response.resources; 24 | } 25 | 26 | async getGames() { 27 | const games = await this.findManyByQuery({ 28 | query: "SELECT * FROM c WHERE c.modelType = @type", 29 | parameters: [{ name: "@type", value: ModelType.Game }], 30 | }); 31 | 32 | return games.resources; 33 | } 34 | 35 | async getGame(id: string) { 36 | const game = await this.findManyByQuery({ 37 | query: "SELECT TOP 1 * FROM c WHERE c.id = @id AND c.modelType = @type", 38 | parameters: [ 39 | { name: "@id", value: id }, 40 | { name: "@type", value: ModelType.Game }, 41 | ], 42 | }); 43 | 44 | return game.resources[0]; 45 | } 46 | 47 | async createGame(questions: QuestionModel[]) { 48 | const newGame: GameModel = { 49 | id: idGenerator(), 50 | modelType: ModelType.Game, 51 | state: GameState.WaitingForPlayers, 52 | players: [], 53 | answers: [], 54 | questions: arrayRandomiser(questions).slice(0, 10), 55 | }; 56 | 57 | const savedGame = await this.createOne(newGame); 58 | 59 | return savedGame.resource; 60 | } 61 | 62 | async updateGame(game: GameModel) { 63 | const response = await this.container 64 | .item(game.id, game.modelType) 65 | .replace(game); 66 | 67 | return response.resource; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /api/graphql/data/cosmos/QuestionDataSource.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from "apollo-datasource"; 2 | import { CosmosDataSource } from "apollo-datasource-cosmosdb"; 3 | import { arrayRandomiser } from "../../../utils"; 4 | import { ModelType, IQuestionDataSource, QuestionModel } from "../types"; 5 | 6 | export class QuestionDataSource 7 | extends CosmosDataSource 8 | implements IQuestionDataSource { 9 | async getQuestions(): Promise { 10 | const questions = await this.findManyByQuery({ 11 | query: "SELECT * FROM c WHERE c.modelType = @type", 12 | parameters: [{ name: "@type", value: ModelType.Question }], 13 | }); 14 | 15 | return arrayRandomiser(questions.resources); 16 | } 17 | async getQuestion(id: string) { 18 | const question = await this.findOneById(id); 19 | 20 | return question; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /api/graphql/data/cosmos/UserDataSource.ts: -------------------------------------------------------------------------------- 1 | import { CosmosDataSource } from "apollo-datasource-cosmosdb"; 2 | import { idGenerator } from "../../../utils"; 3 | import { ModelType, IUserDataSource, UserModel } from "../types"; 4 | 5 | export class UserDataSource 6 | extends CosmosDataSource 7 | implements IUserDataSource { 8 | async getUser(id: string) { 9 | return await this.findOneById(id); 10 | } 11 | 12 | async createUser(name: string) { 13 | // without doing a proper auth solution we'll pretend that names are unique 14 | const existingUser = await this.findManyByQuery({ 15 | query: 16 | "SELECT TOP 1 * FROM c WHERE c.name = @name AND c.modelType = @type", 17 | parameters: [ 18 | { name: "@name", value: name }, 19 | { name: "@type", value: ModelType.User }, 20 | ], 21 | }); 22 | 23 | if (existingUser.resources[0]) { 24 | return existingUser.resources[0]; 25 | } 26 | 27 | const user: UserModel = { 28 | id: idGenerator(), 29 | modelType: ModelType.User, 30 | name, 31 | // fields used with Azure Static Web Apps auth 32 | identityProvider: "not defined", 33 | userDetails: "not defined", 34 | userRoles: ["anonymous", "authenticated"], 35 | }; 36 | 37 | const savedUser = await this.createOne(user); 38 | return savedUser.resource; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /api/graphql/data/inMemory/GameDataSource.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from "apollo-datasource"; 2 | import { idGenerator } from "../../../utils"; 3 | import { GameState } from "../../generated"; 4 | import { IGameDataSource, GameModel, ModelType, QuestionModel } from "../types"; 5 | 6 | export class GameDataSource extends DataSource implements IGameDataSource { 7 | constructor(private games: GameModel[]) { 8 | super(); 9 | } 10 | getUserGames(userId: string): Promise { 11 | return Promise.resolve( 12 | this.games.filter((g) => g.players.some((p) => p.id === userId)) 13 | ); 14 | } 15 | 16 | updateGame(game: GameModel): Promise { 17 | return Promise.resolve(game); 18 | } 19 | 20 | getGames() { 21 | return Promise.resolve(this.games); 22 | } 23 | 24 | getGame(id: string): Promise { 25 | return Promise.resolve(this.games.find((g) => g.id === id)); 26 | } 27 | 28 | createGame(questions: QuestionModel[]): Promise { 29 | const game: GameModel = { 30 | id: idGenerator(), 31 | modelType: ModelType.Game, 32 | state: GameState.WaitingForPlayers, 33 | answers: [], 34 | questions, 35 | players: [], 36 | }; 37 | 38 | this.games.push(game); 39 | 40 | return Promise.resolve(game); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /api/graphql/data/inMemory/QuestionDataSource.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from "apollo-datasource"; 2 | import { arrayRandomiser } from "../../../utils"; 3 | import { IQuestionDataSource, QuestionModel } from "../types"; 4 | 5 | export class QuestionDataSource 6 | extends DataSource 7 | implements IQuestionDataSource { 8 | #questions: QuestionModel[]; 9 | constructor() { 10 | super(); 11 | this.#questions = require("../../../../trivia.json"); 12 | } 13 | getQuestion(id: string): Promise { 14 | return Promise.resolve(this.#questions.find((q) => q.id === id)); 15 | } 16 | 17 | getQuestions(): Promise { 18 | return Promise.resolve(arrayRandomiser(this.#questions).slice(0, 10)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /api/graphql/data/inMemory/UserDataSource.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from "apollo-datasource"; 2 | import { idGenerator } from "../../../utils"; 3 | import { ModelType, IUserDataSource, UserModel } from "../types"; 4 | 5 | export class UserDataSource extends DataSource implements IUserDataSource { 6 | constructor(private users: UserModel[]) { 7 | super(); 8 | } 9 | 10 | getUser(id: string): Promise { 11 | return Promise.resolve(this.users.find((u) => u.id === id)); 12 | } 13 | createUser(name: string): Promise { 14 | const existingUser = this.users.find((u) => u.name === name); 15 | 16 | if (existingUser) { 17 | return Promise.resolve(existingUser); 18 | } 19 | 20 | const user: UserModel = { 21 | id: idGenerator(), 22 | modelType: ModelType.User, 23 | name, 24 | // fields used with Azure Static Web Apps auth 25 | identityProvider: "not defined", 26 | userDetails: "not defined", 27 | userRoles: ["anonymous", "authenticated"], 28 | }; 29 | 30 | this.users.push(user); 31 | 32 | return Promise.resolve(user); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /api/graphql/data/index.ts: -------------------------------------------------------------------------------- 1 | import { CosmosClient } from "@azure/cosmos"; 2 | import { GameDataSource as CosmosGameDataSource } from "./cosmos/GameDataSource"; 3 | import { QuestionDataSource as CosmosQuestionDataSource } from "./cosmos/QuestionDataSource"; 4 | import { UserDataSource as CosmosUserDataSource } from "./cosmos/UserDataSource"; 5 | import { GameDataSource as InMemoryGameDataSource } from "./inMemory/GameDataSource"; 6 | import { QuestionDataSource as InMemoryQuestionDataSource } from "./inMemory/QuestionDataSource"; 7 | import { UserDataSource as InMemoryUserDataSource } from "./inMemory/UserDataSource"; 8 | import { GameModel, UserModel } from "./types"; 9 | 10 | export const cosmosDataSources = () => { 11 | 12 | if(!process.env.CosmosDB || process.env.CosmosDB.length === 0) throw new Error("CosmosDB connection string not found."); 13 | 14 | const client = new CosmosClient(process.env.CosmosDB); 15 | const container = client.database("trivia").container("game"); 16 | 17 | return { 18 | user: new CosmosUserDataSource(container), 19 | question: new CosmosQuestionDataSource(container), 20 | game: new CosmosGameDataSource(container), 21 | }; 22 | }; 23 | 24 | const games: GameModel[] = []; 25 | const users: UserModel[] = []; 26 | export const inMemoryDataSources = () => ({ 27 | user: new InMemoryUserDataSource(users), 28 | question: new InMemoryQuestionDataSource(), 29 | game: new InMemoryGameDataSource(games), 30 | }); 31 | -------------------------------------------------------------------------------- /api/graphql/data/types.ts: -------------------------------------------------------------------------------- 1 | import { GameState } from "../generated"; 2 | 3 | export enum ModelType { 4 | Question = "Question", 5 | User = "User", 6 | UserAnswer = "UserAnswer", 7 | Game = "Game", 8 | } 9 | 10 | type Model = { 11 | id: string; 12 | modelType: ModelType; 13 | }; 14 | 15 | export type QuestionModel = { 16 | question: string; 17 | category: string; 18 | incorrect_answers: string[]; 19 | correct_answer: string; 20 | type: string; 21 | difficulty: "easy" | "medium" | "hard"; 22 | } & Model; 23 | 24 | export type UserModel = { 25 | name: string; 26 | identityProvider: string; 27 | userDetails: string; 28 | userRoles: string[]; 29 | } & Model; 30 | 31 | export type UserAnswerModel = { 32 | user: UserModel; 33 | question: QuestionModel; 34 | answer: string; 35 | } & Model; 36 | 37 | export type GameModel = { 38 | state: GameState; 39 | players: UserModel[]; 40 | questions: QuestionModel[]; 41 | answers: UserAnswerModel[]; 42 | } & Model; 43 | 44 | export interface IGameDataSource { 45 | getGames(): Promise; 46 | getGame(id: string): Promise; 47 | createGame(questions: QuestionModel[]): Promise; 48 | updateGame(game: GameModel): Promise; 49 | getUserGames(userId: string): Promise; 50 | } 51 | 52 | export interface IUserDataSource { 53 | getUser(id: string): Promise; 54 | createUser(name: string): Promise; 55 | } 56 | 57 | export interface IQuestionDataSource { 58 | getQuestion(id: string): Promise; 59 | getQuestions(): Promise; 60 | } 61 | -------------------------------------------------------------------------------- /api/graphql/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": ["get", "post"] 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "$return" 14 | } 15 | ], 16 | "scriptFile": "../dist/graphql/index.js" 17 | } 18 | -------------------------------------------------------------------------------- /api/graphql/generated.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from "graphql"; 2 | import { QuestionModel, GameModel, UserModel } from "./data/types"; 3 | import { ApolloContext } from "./apolloContext"; 4 | export type Maybe = T | null; 5 | export type InputMaybe = Maybe; 6 | export type Exact = { 7 | [K in keyof T]: T[K]; 8 | }; 9 | export type MakeOptional = Omit & { 10 | [SubKey in K]?: Maybe; 11 | }; 12 | export type MakeMaybe = Omit & { 13 | [SubKey in K]: Maybe; 14 | }; 15 | export type RequireFields = Omit & { 16 | [P in K]-?: NonNullable; 17 | }; 18 | /** All built-in and custom scalars, mapped to their actual values */ 19 | export type Scalars = { 20 | ID: string; 21 | String: string; 22 | Boolean: boolean; 23 | Int: number; 24 | Float: number; 25 | }; 26 | 27 | export type Game = { 28 | __typename?: "Game"; 29 | id: Scalars["ID"]; 30 | players: Array; 31 | questions: Array; 32 | state?: Maybe; 33 | }; 34 | 35 | export enum GameState { 36 | Completed = "Completed", 37 | Started = "Started", 38 | WaitingForPlayers = "WaitingForPlayers", 39 | } 40 | 41 | export type Mutation = { 42 | __typename?: "Mutation"; 43 | addPlayerToGame: Player; 44 | createGame?: Maybe; 45 | startGame?: Maybe; 46 | submitAnswer?: Maybe; 47 | }; 48 | 49 | export type MutationAddPlayerToGameArgs = { 50 | id: Scalars["ID"]; 51 | name: Scalars["String"]; 52 | }; 53 | 54 | export type MutationStartGameArgs = { 55 | id: Scalars["ID"]; 56 | }; 57 | 58 | export type MutationSubmitAnswerArgs = { 59 | answer: Scalars["String"]; 60 | gameId: Scalars["ID"]; 61 | playerId: Scalars["ID"]; 62 | questionId: Scalars["ID"]; 63 | }; 64 | 65 | export type Player = { 66 | __typename?: "Player"; 67 | game: Game; 68 | games: Array; 69 | id: Scalars["ID"]; 70 | name: Scalars["String"]; 71 | }; 72 | 73 | export type PlayerGameArgs = { 74 | gameId?: InputMaybe; 75 | }; 76 | 77 | export type PlayerResult = { 78 | __typename?: "PlayerResult"; 79 | answers: Array; 80 | correct?: Maybe; 81 | correctAnswer: Scalars["String"]; 82 | name: Scalars["String"]; 83 | question: Scalars["String"]; 84 | submittedAnswer: Scalars["String"]; 85 | }; 86 | 87 | export type Query = { 88 | __typename?: "Query"; 89 | game?: Maybe; 90 | games: Array; 91 | playerResults: Array; 92 | }; 93 | 94 | export type QueryGameArgs = { 95 | id: Scalars["ID"]; 96 | }; 97 | 98 | export type QueryPlayerResultsArgs = { 99 | gameId: Scalars["ID"]; 100 | playerId: Scalars["ID"]; 101 | }; 102 | 103 | export type Question = { 104 | __typename?: "Question"; 105 | answers: Array; 106 | correctAnswer: Scalars["String"]; 107 | id: Scalars["ID"]; 108 | question: Scalars["String"]; 109 | }; 110 | 111 | export type ResolverTypeWrapper = Promise | T; 112 | 113 | export type ResolverWithResolve = { 114 | resolve: ResolverFn; 115 | }; 116 | export type Resolver = 117 | | ResolverFn 118 | | ResolverWithResolve; 119 | 120 | export type ResolverFn = ( 121 | parent: TParent, 122 | args: TArgs, 123 | context: TContext, 124 | info: GraphQLResolveInfo 125 | ) => Promise | TResult; 126 | 127 | export type SubscriptionSubscribeFn = ( 128 | parent: TParent, 129 | args: TArgs, 130 | context: TContext, 131 | info: GraphQLResolveInfo 132 | ) => AsyncIterable | Promise>; 133 | 134 | export type SubscriptionResolveFn = ( 135 | parent: TParent, 136 | args: TArgs, 137 | context: TContext, 138 | info: GraphQLResolveInfo 139 | ) => TResult | Promise; 140 | 141 | export interface SubscriptionSubscriberObject< 142 | TResult, 143 | TKey extends string, 144 | TParent, 145 | TContext, 146 | TArgs 147 | > { 148 | subscribe: SubscriptionSubscribeFn< 149 | { [key in TKey]: TResult }, 150 | TParent, 151 | TContext, 152 | TArgs 153 | >; 154 | resolve?: SubscriptionResolveFn< 155 | TResult, 156 | { [key in TKey]: TResult }, 157 | TContext, 158 | TArgs 159 | >; 160 | } 161 | 162 | export interface SubscriptionResolverObject { 163 | subscribe: SubscriptionSubscribeFn; 164 | resolve: SubscriptionResolveFn; 165 | } 166 | 167 | export type SubscriptionObject< 168 | TResult, 169 | TKey extends string, 170 | TParent, 171 | TContext, 172 | TArgs 173 | > = 174 | | SubscriptionSubscriberObject 175 | | SubscriptionResolverObject; 176 | 177 | export type SubscriptionResolver< 178 | TResult, 179 | TKey extends string, 180 | TParent = {}, 181 | TContext = {}, 182 | TArgs = {} 183 | > = 184 | | (( 185 | ...args: any[] 186 | ) => SubscriptionObject) 187 | | SubscriptionObject; 188 | 189 | export type TypeResolveFn = ( 190 | parent: TParent, 191 | context: TContext, 192 | info: GraphQLResolveInfo 193 | ) => Maybe | Promise>; 194 | 195 | export type IsTypeOfResolverFn = ( 196 | obj: T, 197 | context: TContext, 198 | info: GraphQLResolveInfo 199 | ) => boolean | Promise; 200 | 201 | export type NextResolverFn = () => Promise; 202 | 203 | export type DirectiveResolverFn< 204 | TResult = {}, 205 | TParent = {}, 206 | TContext = {}, 207 | TArgs = {} 208 | > = ( 209 | next: NextResolverFn, 210 | parent: TParent, 211 | args: TArgs, 212 | context: TContext, 213 | info: GraphQLResolveInfo 214 | ) => TResult | Promise; 215 | 216 | /** Mapping between all available schema types and the resolvers types */ 217 | export type ResolversTypes = { 218 | Boolean: ResolverTypeWrapper; 219 | Game: ResolverTypeWrapper; 220 | GameState: GameState; 221 | ID: ResolverTypeWrapper; 222 | Mutation: ResolverTypeWrapper<{}>; 223 | Player: ResolverTypeWrapper; 224 | PlayerResult: ResolverTypeWrapper; 225 | Query: ResolverTypeWrapper<{}>; 226 | Question: ResolverTypeWrapper; 227 | String: ResolverTypeWrapper; 228 | }; 229 | 230 | /** Mapping between all available schema types and the resolvers parents */ 231 | export type ResolversParentTypes = { 232 | Boolean: Scalars["Boolean"]; 233 | Game: GameModel; 234 | ID: Scalars["ID"]; 235 | Mutation: {}; 236 | Player: UserModel; 237 | PlayerResult: PlayerResult; 238 | Query: {}; 239 | Question: QuestionModel; 240 | String: Scalars["String"]; 241 | }; 242 | 243 | export type GameResolvers< 244 | ContextType = ApolloContext, 245 | ParentType extends ResolversParentTypes["Game"] = ResolversParentTypes["Game"] 246 | > = { 247 | id?: Resolver; 248 | players?: Resolver, ParentType, ContextType>; 249 | questions?: Resolver< 250 | Array, 251 | ParentType, 252 | ContextType 253 | >; 254 | state?: Resolver, ParentType, ContextType>; 255 | __isTypeOf?: IsTypeOfResolverFn; 256 | }; 257 | 258 | export type MutationResolvers< 259 | ContextType = ApolloContext, 260 | ParentType extends ResolversParentTypes["Mutation"] = ResolversParentTypes["Mutation"] 261 | > = { 262 | addPlayerToGame?: Resolver< 263 | ResolversTypes["Player"], 264 | ParentType, 265 | ContextType, 266 | RequireFields 267 | >; 268 | createGame?: Resolver, ParentType, ContextType>; 269 | startGame?: Resolver< 270 | Maybe, 271 | ParentType, 272 | ContextType, 273 | RequireFields 274 | >; 275 | submitAnswer?: Resolver< 276 | Maybe, 277 | ParentType, 278 | ContextType, 279 | RequireFields< 280 | MutationSubmitAnswerArgs, 281 | "answer" | "gameId" | "playerId" | "questionId" 282 | > 283 | >; 284 | }; 285 | 286 | export type PlayerResolvers< 287 | ContextType = ApolloContext, 288 | ParentType extends ResolversParentTypes["Player"] = ResolversParentTypes["Player"] 289 | > = { 290 | game?: Resolver< 291 | ResolversTypes["Game"], 292 | ParentType, 293 | ContextType, 294 | Partial 295 | >; 296 | games?: Resolver, ParentType, ContextType>; 297 | id?: Resolver; 298 | name?: Resolver; 299 | __isTypeOf?: IsTypeOfResolverFn; 300 | }; 301 | 302 | export type PlayerResultResolvers< 303 | ContextType = ApolloContext, 304 | ParentType extends ResolversParentTypes["PlayerResult"] = ResolversParentTypes["PlayerResult"] 305 | > = { 306 | answers?: Resolver, ParentType, ContextType>; 307 | correct?: Resolver, ParentType, ContextType>; 308 | correctAnswer?: Resolver; 309 | name?: Resolver; 310 | question?: Resolver; 311 | submittedAnswer?: Resolver; 312 | __isTypeOf?: IsTypeOfResolverFn; 313 | }; 314 | 315 | export type QueryResolvers< 316 | ContextType = ApolloContext, 317 | ParentType extends ResolversParentTypes["Query"] = ResolversParentTypes["Query"] 318 | > = { 319 | game?: Resolver< 320 | Maybe, 321 | ParentType, 322 | ContextType, 323 | RequireFields 324 | >; 325 | games?: Resolver, ParentType, ContextType>; 326 | playerResults?: Resolver< 327 | Array, 328 | ParentType, 329 | ContextType, 330 | RequireFields 331 | >; 332 | }; 333 | 334 | export type QuestionResolvers< 335 | ContextType = ApolloContext, 336 | ParentType extends ResolversParentTypes["Question"] = ResolversParentTypes["Question"] 337 | > = { 338 | answers?: Resolver, ParentType, ContextType>; 339 | correctAnswer?: Resolver; 340 | id?: Resolver; 341 | question?: Resolver; 342 | __isTypeOf?: IsTypeOfResolverFn; 343 | }; 344 | 345 | export type Resolvers = { 346 | Game?: GameResolvers; 347 | Mutation?: MutationResolvers; 348 | Player?: PlayerResolvers; 349 | PlayerResult?: PlayerResultResolvers; 350 | Query?: QueryResolvers; 351 | Question?: QuestionResolvers; 352 | }; 353 | -------------------------------------------------------------------------------- /api/graphql/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader"; 2 | import { loadSchemaSync } from "@graphql-tools/load"; 3 | import { addResolversToSchema } from "@graphql-tools/schema"; 4 | import { ApolloServer } from "apollo-server-azure-functions"; 5 | import { join } from "path"; 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import { cosmosDataSources, inMemoryDataSources } from "./data/index"; 8 | import resolvers from "./resolvers"; 9 | 10 | let dataSources:any = inMemoryDataSources; 11 | 12 | if(process.env.CosmosDB){ 13 | console.log("using Cosmos DB"); 14 | dataSources = cosmosDataSources 15 | } else { 16 | console.log("using in-memory DB"); 17 | } 18 | 19 | const schema = loadSchemaSync( 20 | join(__dirname, "..", "..", "graphql", "schema.graphql"), 21 | { 22 | loaders: [new GraphQLFileLoader()], 23 | } 24 | ); 25 | 26 | const server = new ApolloServer({ 27 | schema: addResolversToSchema({ schema, resolvers }), 28 | dataSources, 29 | context: {}, 30 | }); 31 | 32 | export default server.createHandler({ 33 | cors: { 34 | origin: ['*', "https://studio.apollographql.com"], 35 | methods: ["GET", "POST", "OPTIONS"], 36 | allowedHeaders: ["access-control-allow-credentials", "access-control-allow-origin", "content-type"] 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /api/graphql/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { arrayRandomiser } from "../utils"; 2 | import { ModelType, UserAnswerModel } from "./data/types"; 3 | import { GameState, Resolvers } from "./generated"; 4 | 5 | const resolvers: Resolvers = { 6 | Query: { 7 | game(_, { id }, { dataSources }) { 8 | return dataSources.game.getGame(id); 9 | }, 10 | games(_, __, { dataSources }) { 11 | return dataSources.game.getGames(); 12 | }, 13 | async playerResults(_, { gameId, playerId }, { dataSources }) { 14 | const game = await dataSources.game.getGame(gameId); 15 | 16 | const playerAnswers = game.answers.filter((a) => a.user.id === playerId); 17 | 18 | return playerAnswers.map((answer) => { 19 | const question = answer.question; 20 | return { 21 | name: answer.user.name, 22 | answers: arrayRandomiser( 23 | question.incorrect_answers.concat(question.correct_answer) 24 | ), 25 | question: question.question, 26 | correctAnswer: question.correct_answer, 27 | submittedAnswer: answer.answer, 28 | correct: answer.answer === question.correct_answer, 29 | }; 30 | }); 31 | }, 32 | }, 33 | Question: { 34 | answers(question) { 35 | const answers = arrayRandomiser( 36 | question.incorrect_answers.concat([question.correct_answer]) 37 | ); 38 | 39 | return answers; 40 | }, 41 | correctAnswer(question) { 42 | return question.correct_answer; 43 | }, 44 | id(question) { 45 | return question.id; 46 | }, 47 | question(question) { 48 | return question.question; 49 | }, 50 | }, 51 | Player: { 52 | async game(user, { gameId }, { dataSources }) { 53 | const game = await dataSources.game.getGame(gameId); 54 | 55 | if (!game.players.some((player) => player.id === user.id)) { 56 | throw Error("Player not part of the game"); 57 | } 58 | 59 | return game; 60 | }, 61 | async games(user, _, { dataSources }) { 62 | const games = await dataSources.game.getUserGames(user.id); 63 | 64 | return games; 65 | }, 66 | }, 67 | Mutation: { 68 | async createGame(_, __, { dataSources }) { 69 | const questions = await dataSources.question.getQuestions(); 70 | const game = await dataSources.game.createGame(questions); 71 | 72 | return game; 73 | }, 74 | async addPlayerToGame(_, { id, name }, { dataSources }) { 75 | const user = await dataSources.user.createUser(name); 76 | const game = await dataSources.game.getGame(id); 77 | game.players.push(user); 78 | await dataSources.game.updateGame(game); 79 | 80 | return user; 81 | }, 82 | async startGame(_, { id }, { dataSources }) { 83 | const game = await dataSources.game.getGame(id); 84 | game.state = GameState.Started; 85 | return await dataSources.game.updateGame(game); 86 | }, 87 | async submitAnswer( 88 | _, 89 | { answer, gameId, playerId, questionId }, 90 | { dataSources } 91 | ) { 92 | const [game, user, question] = await Promise.all([ 93 | dataSources.game.getGame(gameId), 94 | dataSources.user.getUser(playerId), 95 | dataSources.question.getQuestion(questionId), 96 | ]); 97 | 98 | const answerModel: UserAnswerModel = { 99 | id: `${gameId}-${questionId}-${playerId}`, 100 | modelType: ModelType.UserAnswer, 101 | answer, 102 | question, 103 | user, 104 | }; 105 | 106 | game.answers.push(answerModel); 107 | 108 | await dataSources.game.updateGame(game); 109 | return user; 110 | }, 111 | }, 112 | }; 113 | 114 | export default resolvers; 115 | -------------------------------------------------------------------------------- /api/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | enum GameState { 2 | WaitingForPlayers 3 | Started 4 | Completed 5 | } 6 | 7 | type Question { 8 | id: ID! 9 | question: String! 10 | correctAnswer: String! 11 | answers: [String!]! 12 | } 13 | 14 | type Player { 15 | id: ID! 16 | name: String! 17 | game(gameId: ID): Game! 18 | games: [Game!]! 19 | } 20 | 21 | type Game { 22 | id: ID! 23 | state: GameState 24 | players: [Player!]! 25 | questions: [Question!]! 26 | } 27 | 28 | type PlayerResult { 29 | name: String! 30 | question: String! 31 | submittedAnswer: String! 32 | correctAnswer: String! 33 | answers: [String!]! 34 | correct: Boolean 35 | } 36 | 37 | type Query { 38 | game(id: ID!): Game 39 | games: [Game!]! 40 | playerResults(gameId: ID!, playerId: ID!): [PlayerResult!]! 41 | } 42 | 43 | type Mutation { 44 | createGame: Game 45 | addPlayerToGame(id: ID!, name: String!): Player! 46 | startGame(id: ID!): Game 47 | submitAnswer( 48 | gameId: ID! 49 | playerId: ID! 50 | questionId: ID! 51 | answer: String! 52 | ): Player 53 | } 54 | 55 | schema { 56 | query: Query 57 | mutation: Mutation 58 | } 59 | -------------------------------------------------------------------------------- /api/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[2.*, 3.0.0)" 14 | } 15 | } -------------------------------------------------------------------------------- /api/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "node", 5 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 6 | "NODE_TLS_REJECT_UNAUTHORIZED":0, 7 | "CosmosDB": "" 8 | }, 9 | "Host": { 10 | "CORS": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "version": "", 4 | "scripts": { 5 | "build": "tsc", 6 | "build:production": "npm run prestart && npm prune --production", 7 | "watch": "tsc --w", 8 | "local-storage": "azurite --silent --location azurite --debug azurite/debug.log", 9 | "prestart": "npm run build && func extensions install && npm run gen", 10 | "start:host": "func start", 11 | "start": "concurrently \"npm run start:host\" \"npm run watch\" \"npm run gen:watch\" ", 12 | "test": "echo \"No tests yet...\"", 13 | "gen": "graphql-codegen --config codegen.yml", 14 | "gen:watch": "graphql-codegen --config codegen.yml --watch" 15 | }, 16 | "description": "", 17 | "devDependencies": { 18 | "@graphql-codegen/cli": "^2.11.6", 19 | "@graphql-codegen/introspection": "^2.2.1", 20 | "@graphql-codegen/typed-document-node": "^2.3.3", 21 | "@graphql-codegen/typescript": "^2.7.3", 22 | "@graphql-codegen/typescript-operations": "^2.5.3", 23 | "@graphql-codegen/typescript-resolvers": "^2.7.3", 24 | "azurite": "^3.18.0", 25 | "concurrently": "^7.3.0", 26 | "typescript": "^4.7.4" 27 | }, 28 | "dependencies": { 29 | "@azure/cosmos": "^3.16.4", 30 | "@azure/functions": "^3.2.0", 31 | "@graphql-tools/graphql-file-loader": "^7.5.2", 32 | "@graphql-tools/load": "^7.7.4", 33 | "@graphql-tools/schema": "^9.0.1", 34 | "apollo-datasource": "^3.3.2", 35 | "apollo-datasource-cosmosdb": "^0.1.0", 36 | "apollo-server-azure-functions": "^3.10.1", 37 | "graphql": "^16.6.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /api/trivia.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "0", 4 | "category": "Science: Computers", 5 | "type": "multiple", 6 | "difficulty": "easy", 7 | "question": "What does CPU stand for?", 8 | "correct_answer": "Central Processing Unit", 9 | "incorrect_answers": [ 10 | "Central Process Unit", 11 | "Computer Personal Unit", 12 | "Central Processor Unit" 13 | ], 14 | "modelType": "Question" 15 | }, 16 | { 17 | "id": "1", 18 | "category": "Science: Computers", 19 | "type": "multiple", 20 | "difficulty": "medium", 21 | "question": "What does AD stand for in relation to Windows Operating Systems? ", 22 | "correct_answer": "Active Directory", 23 | "incorrect_answers": [ 24 | "Alternative Drive", 25 | "Automated Database", 26 | "Active Department" 27 | ], 28 | "modelType": "Question" 29 | }, 30 | { 31 | "id": "2", 32 | "category": "Science: Computers", 33 | "type": "multiple", 34 | "difficulty": "medium", 35 | "question": "When was the programming language "C#" released?", 36 | "correct_answer": "2000", 37 | "incorrect_answers": ["1998", "1999", "2001"], 38 | "modelType": "Question" 39 | }, 40 | { 41 | "id": "3", 42 | "category": "Science: Computers", 43 | "type": "multiple", 44 | "difficulty": "easy", 45 | "question": "What amount of bits commonly equals one byte?", 46 | "correct_answer": "8", 47 | "incorrect_answers": ["1", "2", "64"], 48 | "modelType": "Question" 49 | }, 50 | { 51 | "id": "4", 52 | "category": "Science: Computers", 53 | "type": "multiple", 54 | "difficulty": "medium", 55 | "question": "Which of the following languages is used as a scripting language in the Unity 3D game engine?", 56 | "correct_answer": "C#", 57 | "incorrect_answers": ["Java", "C++", "Objective-C"], 58 | "modelType": "Question" 59 | }, 60 | { 61 | "id": "5", 62 | "category": "Science: Computers", 63 | "type": "multiple", 64 | "difficulty": "easy", 65 | "question": "If you were to code software in this language you'd only be able to type 0's and 1's.", 66 | "correct_answer": "Binary", 67 | "incorrect_answers": ["JavaScript", "C++", "Python"], 68 | "modelType": "Question" 69 | }, 70 | { 71 | "id": "6", 72 | "category": "Science: Computers", 73 | "type": "multiple", 74 | "difficulty": "easy", 75 | "question": "What is the most preferred image format used for logos in the Wikimedia database?", 76 | "correct_answer": ".svg", 77 | "incorrect_answers": [".png", ".jpeg", ".gif"], 78 | "modelType": "Question" 79 | }, 80 | { 81 | "id": "7", 82 | "category": "Science: Computers", 83 | "type": "multiple", 84 | "difficulty": "medium", 85 | "question": "What was the first commerically available computer processor?", 86 | "correct_answer": "Intel 4004", 87 | "incorrect_answers": ["Intel 486SX", "TMS 1000", "AMD AM386"], 88 | "modelType": "Question" 89 | }, 90 | { 91 | "id": "8", 92 | "category": "Science: Computers", 93 | "type": "multiple", 94 | "difficulty": "easy", 95 | "question": "In web design, what does CSS stand for?", 96 | "correct_answer": "Cascading Style Sheet", 97 | "incorrect_answers": [ 98 | "Counter Strike: Source", 99 | "Corrective Style Sheet", 100 | "Computer Style Sheet" 101 | ], 102 | "modelType": "Question" 103 | }, 104 | { 105 | "id": "9", 106 | "category": "Science: Computers", 107 | "type": "multiple", 108 | "difficulty": "easy", 109 | "question": "What is the domain name for the country Tuvalu?", 110 | "correct_answer": ".tv", 111 | "incorrect_answers": [".tu", ".tt", ".tl"], 112 | "modelType": "Question" 113 | }, 114 | { 115 | "id": "10", 116 | "category": "Science: Computers", 117 | "type": "multiple", 118 | "difficulty": "medium", 119 | "question": "While Apple was formed in California, in which western state was Microsoft founded?", 120 | "correct_answer": "New Mexico", 121 | "incorrect_answers": ["Washington", "Colorado", "Arizona"], 122 | "modelType": "Question" 123 | }, 124 | { 125 | "id": "11", 126 | "category": "Science: Computers", 127 | "type": "multiple", 128 | "difficulty": "medium", 129 | "question": "What does the acronym CDN stand for in terms of networking?", 130 | "correct_answer": "Content Delivery Network", 131 | "incorrect_answers": [ 132 | "Content Distribution Network", 133 | "Computational Data Network", 134 | "Compressed Data Network" 135 | ], 136 | "modelType": "Question" 137 | }, 138 | { 139 | "id": "12", 140 | "category": "Science: Computers", 141 | "type": "multiple", 142 | "difficulty": "medium", 143 | "question": "How many cores does the Intel i7-6950X have?", 144 | "correct_answer": "10", 145 | "incorrect_answers": ["12", "8", "4"], 146 | "modelType": "Question" 147 | }, 148 | { 149 | "id": "13", 150 | "category": "Science: Computers", 151 | "type": "multiple", 152 | "difficulty": "medium", 153 | "question": "In the server hosting industry IaaS stands for...", 154 | "correct_answer": "Infrastructure as a Service", 155 | "incorrect_answers": [ 156 | "Internet as a Service", 157 | "Internet and a Server", 158 | "Infrastructure as a Server" 159 | ], 160 | "modelType": "Question" 161 | }, 162 | { 163 | "id": "14", 164 | "category": "Science: Computers", 165 | "type": "multiple", 166 | "difficulty": "medium", 167 | "question": "Which coding language was the #1 programming language in terms of usage on GitHub in 2015?", 168 | "correct_answer": "JavaScript", 169 | "incorrect_answers": ["C#", "Python", "PHP"], 170 | "modelType": "Question" 171 | }, 172 | { 173 | "id": "15", 174 | "category": "Science: Computers", 175 | "type": "multiple", 176 | "difficulty": "hard", 177 | "question": "What port does HTTP run on?", 178 | "correct_answer": "80", 179 | "incorrect_answers": ["53", "443", "23"], 180 | "modelType": "Question" 181 | }, 182 | { 183 | "id": "16", 184 | "category": "Science: Computers", 185 | "type": "multiple", 186 | "difficulty": "medium", 187 | "question": "In HTML, which non-standard tag used to be be used to make elements scroll across the viewport?", 188 | "correct_answer": "<marquee></marquee>", 189 | "incorrect_answers": [ 190 | "<scroll></scroll>", 191 | "<move></move>", 192 | "<slide></slide>" 193 | ], 194 | "modelType": "Question" 195 | }, 196 | { 197 | "id": "17", 198 | "category": "Science: Computers", 199 | "type": "multiple", 200 | "difficulty": "hard", 201 | "question": "Which of these was the name of a bug found in April 2014 in the publicly available OpenSSL cryptography library?", 202 | "correct_answer": "Heartbleed", 203 | "incorrect_answers": ["Shellshock", "Corrupted Blood", "Shellscript"], 204 | "modelType": "Question" 205 | }, 206 | { 207 | "id": "18", 208 | "category": "Science: Computers", 209 | "type": "multiple", 210 | "difficulty": "easy", 211 | "question": "In "Hexadecimal", what color would be displayed from the color code? "#00FF00"?", 212 | "correct_answer": "Green", 213 | "incorrect_answers": ["Red", "Blue", "Yellow"], 214 | "modelType": "Question" 215 | }, 216 | { 217 | "id": "19", 218 | "category": "Science: Computers", 219 | "type": "multiple", 220 | "difficulty": "medium", 221 | "question": "Which of these programming languages is a low-level language?", 222 | "correct_answer": "Assembly", 223 | "incorrect_answers": ["Python", "C#", "Pascal"], 224 | "modelType": "Question" 225 | }, 226 | { 227 | "id": "20", 228 | "category": "Science: Computers", 229 | "type": "multiple", 230 | "difficulty": "medium", 231 | "question": "In computing terms, typically what does CLI stand for?", 232 | "correct_answer": "Command Line Interface", 233 | "incorrect_answers": [ 234 | "Common Language Input", 235 | "Control Line Interface", 236 | "Common Language Interface" 237 | ], 238 | "modelType": "Question" 239 | }, 240 | { 241 | "id": "21", 242 | "category": "Science: Computers", 243 | "type": "multiple", 244 | "difficulty": "medium", 245 | "question": "What does "LCD" stand for?", 246 | "correct_answer": "Liquid Crystal Display", 247 | "incorrect_answers": [ 248 | "Language Control Design", 249 | "Last Common Difference", 250 | "Long Continuous Design" 251 | ], 252 | "modelType": "Question" 253 | }, 254 | { 255 | "id": "22", 256 | "category": "Science: Computers", 257 | "type": "multiple", 258 | "difficulty": "hard", 259 | "question": "Who is the original author of the realtime physics engine called PhysX?", 260 | "correct_answer": "NovodeX", 261 | "incorrect_answers": ["Ageia", "Nvidia", "AMD"], 262 | "modelType": "Question" 263 | }, 264 | { 265 | "id": "23", 266 | "category": "Science: Computers", 267 | "type": "multiple", 268 | "difficulty": "medium", 269 | "question": "Which of the following is a personal computer made by the Japanese company Fujitsu?", 270 | "correct_answer": "FM-7", 271 | "incorrect_answers": ["PC-9801", "Xmillennium ", "MSX"], 272 | "modelType": "Question" 273 | }, 274 | { 275 | "id": "24", 276 | "category": "Science: Computers", 277 | "type": "multiple", 278 | "difficulty": "medium", 279 | "question": ".rs is the top-level domain for what country?", 280 | "correct_answer": "Serbia", 281 | "incorrect_answers": ["Romania", "Russia", "Rwanda"], 282 | "modelType": "Question" 283 | }, 284 | { 285 | "id": "25", 286 | "category": "Science: Computers", 287 | "type": "multiple", 288 | "difficulty": "medium", 289 | "question": "What is known as "the brain" of the Computer?", 290 | "correct_answer": "Central Processing Unit", 291 | "incorrect_answers": [ 292 | "Motherboard", 293 | "Graphics Processing Unit", 294 | "Keyboard" 295 | ], 296 | "modelType": "Question" 297 | }, 298 | { 299 | "id": "26", 300 | "category": "Science: Computers", 301 | "type": "multiple", 302 | "difficulty": "hard", 303 | "question": "Which data structure does FILO apply to?", 304 | "correct_answer": "Stack", 305 | "incorrect_answers": ["Queue", "Heap", "Tree"], 306 | "modelType": "Question" 307 | }, 308 | { 309 | "id": "27", 310 | "category": "Science: Computers", 311 | "type": "multiple", 312 | "difficulty": "medium", 313 | "question": "What does the term GPU stand for?", 314 | "correct_answer": "Graphics Processing Unit", 315 | "incorrect_answers": [ 316 | "Gaming Processor Unit", 317 | "Graphite Producing Unit", 318 | "Graphical Proprietary Unit" 319 | ], 320 | "modelType": "Question" 321 | }, 322 | { 323 | "id": "28", 324 | "category": "Science: Computers", 325 | "type": "multiple", 326 | "difficulty": "hard", 327 | "question": "What is the name of the process that sends one qubit of information using two bits of classical information?", 328 | "correct_answer": "Quantum Teleportation", 329 | "incorrect_answers": [ 330 | "Super Dense Coding", 331 | "Quantum Entanglement", 332 | "Quantum Programming" 333 | ], 334 | "modelType": "Question" 335 | }, 336 | { 337 | "id": "29", 338 | "category": "Science: Computers", 339 | "type": "multiple", 340 | "difficulty": "hard", 341 | "question": "Which of these is not a key value of Agile software development?", 342 | "correct_answer": "Comprehensive documentation", 343 | "incorrect_answers": [ 344 | "Individuals and interactions", 345 | "Customer collaboration", 346 | "Responding to change" 347 | ], 348 | "modelType": "Question" 349 | }, 350 | { 351 | "id": "30", 352 | "category": "Science: Computers", 353 | "type": "multiple", 354 | "difficulty": "medium", 355 | "question": "What is the main CPU is the Sega Mega Drive / Sega Genesis?", 356 | "correct_answer": "Motorola 68000", 357 | "incorrect_answers": ["Zilog Z80", "Yamaha YM2612", "Intel 8088"], 358 | "modelType": "Question" 359 | }, 360 | { 361 | "id": "31", 362 | "category": "Science: Computers", 363 | "type": "multiple", 364 | "difficulty": "easy", 365 | "question": "This mobile OS held the largest market share in 2012.", 366 | "correct_answer": "iOS", 367 | "incorrect_answers": ["Android", "BlackBerry", "Symbian"], 368 | "modelType": "Question" 369 | }, 370 | { 371 | "id": "32", 372 | "category": "Science: Computers", 373 | "type": "multiple", 374 | "difficulty": "medium", 375 | "question": "Which of these people was NOT a founder of Apple Inc?", 376 | "correct_answer": "Jonathan Ive", 377 | "incorrect_answers": ["Steve Jobs", "Ronald Wayne", "Steve Wozniak"], 378 | "modelType": "Question" 379 | }, 380 | { 381 | "id": "33", 382 | "category": "Science: Computers", 383 | "type": "multiple", 384 | "difficulty": "hard", 385 | "question": "Which RAID array type is associated with data mirroring?", 386 | "correct_answer": "RAID 1", 387 | "incorrect_answers": ["RAID 0", "RAID 10", "RAID 5"], 388 | "modelType": "Question" 389 | }, 390 | { 391 | "id": "34", 392 | "category": "Science: Computers", 393 | "type": "multiple", 394 | "difficulty": "easy", 395 | "question": "How many values can a single byte represent?", 396 | "correct_answer": "256", 397 | "incorrect_answers": ["8", "1", "1024"], 398 | "modelType": "Question" 399 | }, 400 | { 401 | "id": "35", 402 | "category": "Science: Computers", 403 | "type": "multiple", 404 | "difficulty": "easy", 405 | "question": "The C programming language was created by this American computer scientist. ", 406 | "correct_answer": "Dennis Ritchie", 407 | "incorrect_answers": ["Tim Berners Lee", "al-Khwārizmī", "Willis Ware"], 408 | "modelType": "Question" 409 | }, 410 | { 411 | "id": "36", 412 | "category": "Science: Computers", 413 | "type": "multiple", 414 | "difficulty": "medium", 415 | "question": "Which programming language was developed by Sun Microsystems in 1995?", 416 | "correct_answer": "Java", 417 | "incorrect_answers": ["Python", "Solaris OS", "C++"], 418 | "modelType": "Question" 419 | }, 420 | { 421 | "id": "37", 422 | "category": "Science: Computers", 423 | "type": "multiple", 424 | "difficulty": "medium", 425 | "question": "The name of technology company HP stands for what?", 426 | "correct_answer": "Hewlett-Packard", 427 | "incorrect_answers": ["Howard Packmann", "Husker-Pollosk", "Hellman-Pohl"], 428 | "modelType": "Question" 429 | }, 430 | { 431 | "id": "38", 432 | "category": "Science: Computers", 433 | "type": "multiple", 434 | "difficulty": "hard", 435 | "question": "Who invented the "Spanning Tree Protocol"?", 436 | "correct_answer": "Radia Perlman", 437 | "incorrect_answers": ["Paul Vixie", "Vint Cerf", "Michael Roberts"], 438 | "modelType": "Question" 439 | }, 440 | { 441 | "id": "39", 442 | "category": "Science: Computers", 443 | "type": "multiple", 444 | "difficulty": "medium", 445 | "question": "In programming, what do you call functions with the same name but different implementations?", 446 | "correct_answer": "Overloading", 447 | "incorrect_answers": ["Overriding", "Abstracting", "Inheriting"], 448 | "modelType": "Question" 449 | }, 450 | { 451 | "id": "40", 452 | "category": "Science: Computers", 453 | "type": "multiple", 454 | "difficulty": "medium", 455 | "question": "What does RAID stand for?", 456 | "correct_answer": "Redundant Array of Independent Disks", 457 | "incorrect_answers": [ 458 | "Rapid Access for Indexed Devices", 459 | "Range of Applications with Identical Designs", 460 | "Randomized Abstract Identification Description" 461 | ], 462 | "modelType": "Question" 463 | }, 464 | { 465 | "id": "41", 466 | "category": "Science: Computers", 467 | "type": "multiple", 468 | "difficulty": "easy", 469 | "question": "How long is an IPv6 address?", 470 | "correct_answer": "128 bits", 471 | "incorrect_answers": ["32 bits", "64 bits", "128 bytes"], 472 | "modelType": "Question" 473 | }, 474 | { 475 | "id": "42", 476 | "category": "Science: Computers", 477 | "type": "multiple", 478 | "difficulty": "easy", 479 | "question": "In computing, what does MIDI stand for?", 480 | "correct_answer": "Musical Instrument Digital Interface", 481 | "incorrect_answers": [ 482 | "Musical Interface of Digital Instruments", 483 | "Modular Interface of Digital Instruments", 484 | "Musical Instrument Data Interface" 485 | ], 486 | "modelType": "Question" 487 | }, 488 | { 489 | "id": "43", 490 | "category": "Science: Computers", 491 | "type": "multiple", 492 | "difficulty": "easy", 493 | "question": "In computing, what does LAN stand for?", 494 | "correct_answer": "Local Area Network", 495 | "incorrect_answers": [ 496 | "Long Antenna Node", 497 | "Light Access Node", 498 | "Land Address Navigation" 499 | ], 500 | "modelType": "Question" 501 | }, 502 | { 503 | "id": "44", 504 | "category": "Science: Computers", 505 | "type": "multiple", 506 | "difficulty": "medium", 507 | "question": "How many bytes are in a single Kibibyte?", 508 | "correct_answer": "1024", 509 | "incorrect_answers": ["2400", "1000", "1240"], 510 | "modelType": "Question" 511 | }, 512 | { 513 | "id": "45", 514 | "category": "Science: Computers", 515 | "type": "multiple", 516 | "difficulty": "hard", 517 | "question": "According to DeMorgan's Theorem, the Boolean expression (AB)' is equivalent to:", 518 | "correct_answer": "A' + B'", 519 | "incorrect_answers": [ 520 | "A'B + B'A", 521 | "A'B'", 522 | "AB' + AB" 523 | ], 524 | "modelType": "Question" 525 | }, 526 | { 527 | "id": "46", 528 | "category": "Science: Computers", 529 | "type": "multiple", 530 | "difficulty": "hard", 531 | "question": "The acronym "RIP" stands for which of these?", 532 | "correct_answer": "Routing Information Protocol", 533 | "incorrect_answers": [ 534 | "Runtime Instance Processes", 535 | "Regular Interval Processes", 536 | "Routine Inspection Protocol" 537 | ], 538 | "modelType": "Question" 539 | }, 540 | { 541 | "id": "47", 542 | "category": "Science: Computers", 543 | "type": "multiple", 544 | "difficulty": "hard", 545 | "question": "Which of these is not a layer in the OSI model for data communications?", 546 | "correct_answer": "Connection Layer", 547 | "incorrect_answers": [ 548 | "Application Layer", 549 | "Transport Layer", 550 | "Physical Layer" 551 | ], 552 | "modelType": "Question" 553 | }, 554 | { 555 | "id": "48", 556 | "category": "Science: Computers", 557 | "type": "multiple", 558 | "difficulty": "medium", 559 | "question": "What is the number of keys on a standard Windows Keyboard?", 560 | "correct_answer": "104", 561 | "incorrect_answers": ["64", "94", "76"], 562 | "modelType": "Question" 563 | }, 564 | { 565 | "id": "49", 566 | "category": "Science: Computers", 567 | "type": "multiple", 568 | "difficulty": "easy", 569 | "question": "What does the computer software acronym JVM stand for?", 570 | "correct_answer": "Java Virtual Machine", 571 | "incorrect_answers": [ 572 | "Java Vendor Machine", 573 | "Java Visual Machine", 574 | "Just Virtual Machine" 575 | ], 576 | "modelType": "Question" 577 | }, 578 | { 579 | "id": "50", 580 | "category": "Entertainment: Video Games", 581 | "type": "multiple", 582 | "difficulty": "easy", 583 | "question": "What is the name of "Team Fortress 2" update, in which it became Free-to-play?", 584 | "correct_answer": "Über Update", 585 | "incorrect_answers": [ 586 | "Pyromania Update", 587 | "Mann-Conomy Update", 588 | "Engineer Update" 589 | ], 590 | "modelType": "Question" 591 | }, 592 | { 593 | "id": "51", 594 | "category": "Entertainment: Video Games", 595 | "type": "multiple", 596 | "difficulty": "medium", 597 | "question": "Who's the creator of Geometry Dash?", 598 | "correct_answer": "Robert Topala", 599 | "incorrect_answers": ["Scott Cawthon", "Adam Engels", "Andrew Spinks"], 600 | "modelType": "Question" 601 | }, 602 | { 603 | "id": "52", 604 | "category": "Entertainment: Video Games", 605 | "type": "multiple", 606 | "difficulty": "easy", 607 | "question": "Who created the digital distribution platform Steam?", 608 | "correct_answer": "Valve", 609 | "incorrect_answers": ["Pixeltail Games", "Ubisoft", "Electronic Arts"], 610 | "modelType": "Question" 611 | }, 612 | { 613 | "id": "53", 614 | "category": "Entertainment: Video Games", 615 | "type": "multiple", 616 | "difficulty": "easy", 617 | "question": "What year was the game Team Fortress 2 released?", 618 | "correct_answer": "2007", 619 | "incorrect_answers": ["2009", "2005", "2010"], 620 | "modelType": "Question" 621 | }, 622 | { 623 | "id": "54", 624 | "category": "Entertainment: Video Games", 625 | "type": "multiple", 626 | "difficulty": "hard", 627 | "question": "What was the code name given to Sonic the Hedgehog 4 during its development?", 628 | "correct_answer": "Project Needlemouse", 629 | "incorrect_answers": [ 630 | "Project Bluespike", 631 | "Project Roboegg", 632 | "Project Darksphere" 633 | ], 634 | "modelType": "Question" 635 | }, 636 | { 637 | "id": "55", 638 | "category": "Entertainment: Video Games", 639 | "type": "multiple", 640 | "difficulty": "medium", 641 | "question": "What is Solid Snake's real name?", 642 | "correct_answer": "David", 643 | "incorrect_answers": ["Solid Snake", "John", "Huey"], 644 | "modelType": "Question" 645 | }, 646 | { 647 | "id": "56", 648 | "category": "Entertainment: Video Games", 649 | "type": "multiple", 650 | "difficulty": "medium", 651 | "question": "Who is the villain company in "Stardew Valley"?", 652 | "correct_answer": "Joja Co ", 653 | "incorrect_answers": ["Ronin", "Empire", "Robotnik Industry's "], 654 | "modelType": "Question" 655 | }, 656 | { 657 | "id": "57", 658 | "category": "Entertainment: Video Games", 659 | "type": "multiple", 660 | "difficulty": "hard", 661 | "question": "In the original "Super Mario Bros.", what is the acceleration of Mario if he was in free fall?", 662 | "correct_answer": "91.28 m/s^2", 663 | "incorrect_answers": ["110 m/s^2", "9.42 m/s^2", "4.4 m/s^2"], 664 | "modelType": "Question" 665 | }, 666 | { 667 | "id": "58", 668 | "category": "Entertainment: Video Games", 669 | "type": "multiple", 670 | "difficulty": "medium", 671 | "question": "How much does the 'AWP' cost in Counter-Strike: Global Offensive?", 672 | "correct_answer": "$4750", 673 | "incorrect_answers": ["$4500", "$4650", "$5000"], 674 | "modelType": "Question" 675 | }, 676 | { 677 | "id": "59", 678 | "category": "Entertainment: Video Games", 679 | "type": "multiple", 680 | "difficulty": "medium", 681 | "question": "What was the first interactive movie video game?", 682 | "correct_answer": "Astron Belt", 683 | "incorrect_answers": ["Dragon's Lair", "Cube Quest", "M.A.C.H. 3"], 684 | "modelType": "Question" 685 | }, 686 | { 687 | "id": "60", 688 | "category": "Entertainment: Video Games", 689 | "type": "multiple", 690 | "difficulty": "easy", 691 | "question": "Which of these Starbound races has a Wild West culture?", 692 | "correct_answer": "Novakid", 693 | "incorrect_answers": ["Avian", "Human", "Hylotl"], 694 | "modelType": "Question" 695 | }, 696 | { 697 | "id": "61", 698 | "category": "Entertainment: Video Games", 699 | "type": "multiple", 700 | "difficulty": "medium", 701 | "question": "In Need For Speed Most Wanted (2005), what do you drive at the beginning of the career mode?", 702 | "correct_answer": "BMW M3 GTR", 703 | "incorrect_answers": ["Porsche 911 Turbo", "Nissan 240SX", "Ford Mustang"], 704 | "modelType": "Question" 705 | }, 706 | { 707 | "id": "62", 708 | "category": "Entertainment: Video Games", 709 | "type": "multiple", 710 | "difficulty": "medium", 711 | "question": "What happened to Half-Life 2 prior to its release, which resulted in Valve starting over the development of the game?", 712 | "correct_answer": "The source code got leaked", 713 | "incorrect_answers": [ 714 | "They weren't satisfied with the result", 715 | "The story was not good enough", 716 | "Way too many bugs to be fixed" 717 | ], 718 | "modelType": "Question" 719 | }, 720 | { 721 | "id": "63", 722 | "category": "Entertainment: Video Games", 723 | "type": "multiple", 724 | "difficulty": "easy", 725 | "question": "TF2: What code does Soldier put into the door keypad in "Meet the Spy"?", 726 | "correct_answer": "1111", 727 | "incorrect_answers": ["1432", "1337", "No code"], 728 | "modelType": "Question" 729 | }, 730 | { 731 | "id": "64", 732 | "category": "Entertainment: Video Games", 733 | "type": "multiple", 734 | "difficulty": "easy", 735 | "question": "What was Frank West's job in "Dead Rising"?", 736 | "correct_answer": "Photojournalist", 737 | "incorrect_answers": ["Janitor", "Chef", "Taxi Driver"], 738 | "modelType": "Question" 739 | }, 740 | { 741 | "id": "65", 742 | "category": "Entertainment: Video Games", 743 | "type": "multiple", 744 | "difficulty": "medium", 745 | "question": "In "Overwatch", what is the name of Mercy's "ultimate ability"?", 746 | "correct_answer": "Valkyrie", 747 | "incorrect_answers": ["Earthshatter", "Rocket Barrage", "Molten Core"], 748 | "modelType": "Question" 749 | }, 750 | { 751 | "id": "66", 752 | "category": "Entertainment: Video Games", 753 | "type": "multiple", 754 | "difficulty": "easy", 755 | "question": "What video game sparked controversy because of its hidden "Hot Coffee" minigame?", 756 | "correct_answer": "Grand Theft Auto: San Andreas", 757 | "incorrect_answers": [ 758 | "Grand Theft Auto: Vice City", 759 | "Hitman: Blood Money", 760 | "Cooking Mama" 761 | ], 762 | "modelType": "Question" 763 | }, 764 | { 765 | "id": "67", 766 | "category": "Entertainment: Video Games", 767 | "type": "multiple", 768 | "difficulty": "easy", 769 | "question": "How many flagship monsters appear in Monster Hunter Gernerations?", 770 | "correct_answer": "4", 771 | "incorrect_answers": ["1", "2", "3"], 772 | "modelType": "Question" 773 | }, 774 | { 775 | "id": "68", 776 | "category": "Entertainment: Video Games", 777 | "type": "multiple", 778 | "difficulty": "hard", 779 | "question": "What's the name of the halloween-related Sims 4 Stuff Pack released September 29th, 2015?", 780 | "correct_answer": "Spooky Stuff", 781 | "incorrect_answers": [ 782 | "Ghosts n' Ghouls", 783 | "Nerving Nights", 784 | "Fearful Frights" 785 | ], 786 | "modelType": "Question" 787 | }, 788 | { 789 | "id": "69", 790 | "category": "Entertainment: Video Games", 791 | "type": "multiple", 792 | "difficulty": "hard", 793 | "question": "In Xenoblade Chronicles X, which class has a sniper rifle as it's primary weapon?", 794 | "correct_answer": "Partisan Eagle", 795 | "incorrect_answers": ["Blast Fencer", "Winged Viper", "Bastion Warrior"], 796 | "modelType": "Question" 797 | }, 798 | { 799 | "id": "70", 800 | "category": "Entertainment: Video Games", 801 | "type": "multiple", 802 | "difficulty": "easy", 803 | "question": "In what year was Hearthstone released?", 804 | "correct_answer": "2014", 805 | "incorrect_answers": ["2011", "2013", "2012"], 806 | "modelType": "Question" 807 | }, 808 | { 809 | "id": "71", 810 | "category": "Entertainment: Video Games", 811 | "type": "multiple", 812 | "difficulty": "medium", 813 | "question": "In the game "Terraria", which of these bosses are pre-hardmode bosses?", 814 | "correct_answer": "Eye of Cthulhu", 815 | "incorrect_answers": ["Plantera", "Skeletron Prime", "The Destroyer"], 816 | "modelType": "Question" 817 | }, 818 | { 819 | "id": "72", 820 | "category": "Entertainment: Video Games", 821 | "type": "multiple", 822 | "difficulty": "medium", 823 | "question": "This weapon in Counter-Strike: Global Offensive does not exist in real life.", 824 | "correct_answer": "M4A4", 825 | "incorrect_answers": ["AWP", "M4A1", "MP9"], 826 | "modelType": "Question" 827 | }, 828 | { 829 | "id": "73", 830 | "category": "Entertainment: Video Games", 831 | "type": "multiple", 832 | "difficulty": "easy", 833 | "question": "In what year was the game "FTL: Faster Than Light" released?", 834 | "correct_answer": "2012", 835 | "incorrect_answers": ["2014", "2013", "2011"], 836 | "modelType": "Question" 837 | }, 838 | { 839 | "id": "74", 840 | "category": "Entertainment: Video Games", 841 | "type": "multiple", 842 | "difficulty": "medium", 843 | "question": "What was the first .hack game?", 844 | "correct_answer": ".hack//Infection", 845 | "incorrect_answers": [".hack//Zero", ".hack//Sign", ".hack//Liminality"], 846 | "modelType": "Question" 847 | }, 848 | { 849 | "id": "75", 850 | "category": "Entertainment: Video Games", 851 | "type": "multiple", 852 | "difficulty": "medium", 853 | "question": "In Terraria, what does the Wall of Flesh not drop upon defeat?", 854 | "correct_answer": "Picksaw", 855 | "incorrect_answers": ["Pwnhammer", "Breaker Blade", "Laser Rifle"], 856 | "modelType": "Question" 857 | }, 858 | { 859 | "id": "76", 860 | "category": "Entertainment: Video Games", 861 | "type": "multiple", 862 | "difficulty": "easy", 863 | "question": ""Minecraft" was released from beta in 2011 during a convention held in which city?", 864 | "correct_answer": "Las Vegas", 865 | "incorrect_answers": ["Paris", "Bellevue", "London"], 866 | "modelType": "Question" 867 | }, 868 | { 869 | "id": "77", 870 | "category": "Entertainment: Video Games", 871 | "type": "multiple", 872 | "difficulty": "medium", 873 | "question": "How many regular Sunken Sea Scrolls are there in "Splatoon"?", 874 | "correct_answer": "27", 875 | "incorrect_answers": ["32", "30", "5"], 876 | "modelType": "Question" 877 | }, 878 | { 879 | "id": "78", 880 | "category": "Entertainment: Video Games", 881 | "type": "multiple", 882 | "difficulty": "medium", 883 | "question": "Danganronpa Another Episode: Ultra Despair Girls is set after which Danganronpa game?", 884 | "correct_answer": "Danganronpa: Trigger Happy Havoc", 885 | "incorrect_answers": [ 886 | "Danganronpa 2: Goodbye Despair", 887 | "Danganronpa V3: Killing Harmony", 888 | "Danganronpa 3: The End of Hope's Peak High School" 889 | ], 890 | "modelType": "Question" 891 | }, 892 | { 893 | "id": "79", 894 | "category": "Entertainment: Video Games", 895 | "type": "multiple", 896 | "difficulty": "medium", 897 | "question": "What is not a playable race in "Final Fantasy XIV: A Realm Reborn"?", 898 | "correct_answer": "Hume", 899 | "incorrect_answers": ["Miqo'te", "Lalafell", "Roegadyn"], 900 | "modelType": "Question" 901 | }, 902 | { 903 | "id": "80", 904 | "category": "Entertainment: Video Games", 905 | "type": "multiple", 906 | "difficulty": "hard", 907 | "question": "Which game in the "Dark Souls" series does the player play as the "Ashen One"?", 908 | "correct_answer": "Dark Souls III", 909 | "incorrect_answers": ["Dark Souls I", "Bloodborne", "Demon Souls"], 910 | "modelType": "Question" 911 | }, 912 | { 913 | "id": "81", 914 | "category": "Entertainment: Video Games", 915 | "type": "multiple", 916 | "difficulty": "medium", 917 | "question": "What is the name of the first level in "Call of Duty: World at War"?", 918 | "correct_answer": "Semper Fi", 919 | "incorrect_answers": ["Ring of Steel", "Vendetta", "Eviction"], 920 | "modelType": "Question" 921 | }, 922 | { 923 | "id": "82", 924 | "category": "Entertainment: Video Games", 925 | "type": "multiple", 926 | "difficulty": "medium", 927 | "question": "In the "Call Of Duty: Zombies" map "Moon", there is a secondary called the QED. What does QED stand for?", 928 | "correct_answer": "Quantum Entanglement Device", 929 | "incorrect_answers": [ 930 | "Quad Ectoplasmic Driver", 931 | "Question Every Dog", 932 | "Quality Edward Device" 933 | ], 934 | "modelType": "Question" 935 | }, 936 | { 937 | "id": "83", 938 | "category": "Entertainment: Video Games", 939 | "type": "multiple", 940 | "difficulty": "easy", 941 | "question": "In "Call Of Duty: Zombies", what is the name of the Pack-A-Punched Crossbow?", 942 | "correct_answer": "Awful Lawton", 943 | "incorrect_answers": ["Longinus", "V-R11", "Predator"], 944 | "modelType": "Question" 945 | }, 946 | { 947 | "id": "84", 948 | "category": "Entertainment: Video Games", 949 | "type": "multiple", 950 | "difficulty": "medium", 951 | "question": "In "Call Of Duty: Zombies", completing which map's main easter egg will reward you with the achievement, "Little Lost Girl"?", 952 | "correct_answer": "Origins", 953 | "incorrect_answers": ["Revelations", "Moon", "Tranzit"], 954 | "modelType": "Question" 955 | }, 956 | { 957 | "id": "85", 958 | "category": "Entertainment: Video Games", 959 | "type": "multiple", 960 | "difficulty": "medium", 961 | "question": "Which company developed and published Game Dev Tycoon?", 962 | "correct_answer": "Greenheart Games", 963 | "incorrect_answers": ["Greenland Games", "The Tycoonists", "MomCorp"], 964 | "modelType": "Question" 965 | }, 966 | { 967 | "id": "86", 968 | "category": "Entertainment: Video Games", 969 | "type": "multiple", 970 | "difficulty": "hard", 971 | "question": "Which of these characters wasn't a villian in Club Penguin?", 972 | "correct_answer": "The Director", 973 | "incorrect_answers": [ 974 | "Herbert P. Bear", 975 | "Tusk", 976 | "Ultimate Proto-Bot 10000" 977 | ], 978 | "modelType": "Question" 979 | }, 980 | { 981 | "id": "87", 982 | "category": "Entertainment: Video Games", 983 | "type": "multiple", 984 | "difficulty": "easy", 985 | "question": "In which mall does "Dead Rising" take place?", 986 | "correct_answer": "Willamette Parkview Mall", 987 | "incorrect_answers": [ 988 | "Liberty Mall", 989 | "Twin Pines Mall", 990 | "Central Square Shopping Center" 991 | ], 992 | "modelType": "Question" 993 | }, 994 | { 995 | "id": "88", 996 | "category": "Entertainment: Video Games", 997 | "type": "multiple", 998 | "difficulty": "medium", 999 | "question": "In what year was Pokémon Diamond & Pearl released in Japan?", 1000 | "correct_answer": "2006", 1001 | "incorrect_answers": ["2009", "2007", "2008"], 1002 | "modelType": "Question" 1003 | }, 1004 | { 1005 | "id": "89", 1006 | "category": "Entertainment: Video Games", 1007 | "type": "multiple", 1008 | "difficulty": "medium", 1009 | "question": "Which of these is NOT a name of a city in the main island of PLAYERUNKNOWN'S BATTLEGROUNDS?", 1010 | "correct_answer": "Belushya Guba", 1011 | "incorrect_answers": ["Yasnaya Polyana", "Pochinki", "Georgopol"], 1012 | "modelType": "Question" 1013 | }, 1014 | { 1015 | "id": "90", 1016 | "category": "Entertainment: Video Games", 1017 | "type": "multiple", 1018 | "difficulty": "medium", 1019 | "question": "In Telltale Games' "The Walking Dead: Season One" what is the name of Clementine's father?", 1020 | "correct_answer": "Ed", 1021 | "incorrect_answers": ["Charles", "Lee", "Walter"], 1022 | "modelType": "Question" 1023 | }, 1024 | { 1025 | "id": "91", 1026 | "category": "Entertainment: Video Games", 1027 | "type": "multiple", 1028 | "difficulty": "medium", 1029 | "question": "How many controllers could a Nintendo GameCube have plugged in at one time?", 1030 | "correct_answer": "4", 1031 | "incorrect_answers": ["8", "6", "2"], 1032 | "modelType": "Question" 1033 | }, 1034 | { 1035 | "id": "92", 1036 | "category": "Entertainment: Video Games", 1037 | "type": "multiple", 1038 | "difficulty": "medium", 1039 | "question": "Which Crypt of the NecroDancer (2015) character has a soundtrack by Jake "Virt" Kaufman?", 1040 | "correct_answer": "Uncle Eli", 1041 | "incorrect_answers": ["Cadence", "Nocturna", "Octavian (Bard)"], 1042 | "modelType": "Question" 1043 | }, 1044 | { 1045 | "id": "93", 1046 | "category": "Entertainment: Video Games", 1047 | "type": "multiple", 1048 | "difficulty": "medium", 1049 | "question": "What is the only Generation III Pokemon whose name begins with the letter I?", 1050 | "correct_answer": "Illumise", 1051 | "incorrect_answers": ["Infernape", "Ivysaur", "Igglybuff"], 1052 | "modelType": "Question" 1053 | }, 1054 | { 1055 | "id": "94", 1056 | "category": "Entertainment: Video Games", 1057 | "type": "multiple", 1058 | "difficulty": "hard", 1059 | "question": "In World of Warcraft, which raid instance features a chess event?", 1060 | "correct_answer": "Karazhan", 1061 | "incorrect_answers": [ 1062 | "Zul'Aman", 1063 | "Blackwing Lair", 1064 | "Temple of Ahn'Qiraj" 1065 | ], 1066 | "modelType": "Question" 1067 | }, 1068 | { 1069 | "id": "95", 1070 | "category": "Entertainment: Video Games", 1071 | "type": "multiple", 1072 | "difficulty": "medium", 1073 | "question": "In the 1980s, a service called Gameline allowed users to download games to what console?", 1074 | "correct_answer": "Atari 2600", 1075 | "incorrect_answers": [ 1076 | "Sega Genesis", 1077 | "Nintendo Entertainment System", 1078 | "ColecoVision" 1079 | ], 1080 | "modelType": "Question" 1081 | }, 1082 | { 1083 | "id": "96", 1084 | "category": "Entertainment: Video Games", 1085 | "type": "multiple", 1086 | "difficulty": "medium", 1087 | "question": "What is the subtitle for Gran Turismo 3?", 1088 | "correct_answer": "A-Spec", 1089 | "incorrect_answers": ["Championship", "Drive", "Nitro"], 1090 | "modelType": "Question" 1091 | }, 1092 | { 1093 | "id": "97", 1094 | "category": "Entertainment: Video Games", 1095 | "type": "multiple", 1096 | "difficulty": "medium", 1097 | "question": "Who is credited with having created the world's first video game Easter Egg?", 1098 | "correct_answer": "Warren Robinett", 1099 | "incorrect_answers": ["Julius Smith", "Will Crowther", "Don Woods"], 1100 | "modelType": "Question" 1101 | }, 1102 | { 1103 | "id": "98", 1104 | "category": "Entertainment: Video Games", 1105 | "type": "multiple", 1106 | "difficulty": "medium", 1107 | "question": "What character is NOT apart of the Grand Theft Auto series?", 1108 | "correct_answer": "Michael Cardenas", 1109 | "incorrect_answers": ["Packie McReary", "Tommy Vercetti", "Lester Crest"], 1110 | "modelType": "Question" 1111 | }, 1112 | { 1113 | "id": "99", 1114 | "category": "Entertainment: Video Games", 1115 | "type": "multiple", 1116 | "difficulty": "medium", 1117 | "question": "Which of the following characters is NOT playable in "Resident Evil 6"?", 1118 | "correct_answer": "Jill Valentine", 1119 | "incorrect_answers": ["Chris Redfield", "Sherry Birkin", "Helena Harper"], 1120 | "modelType": "Question" 1121 | } 1122 | ] 1123 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "dist", 6 | "rootDir": ".", 7 | "sourceMap": true, 8 | "strict": false, 9 | "esModuleInterop": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /api/utils.ts: -------------------------------------------------------------------------------- 1 | export const arrayRandomiser = (array: T[]) => 2 | array.sort(() => 0.5 - Math.random()); 3 | 4 | export const idGenerator = () => { 5 | const chars = "qwertyuioplkjhgfdsazxcvbnm"; 6 | 7 | let code = ""; 8 | 9 | for (let i = 0; i < 4; i++) { 10 | const random = Math.floor(Math.random() * chars.length); 11 | code += chars[random]; 12 | } 13 | 14 | return code; 15 | }; 16 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.*] 2 | indent_size = 2 3 | indent_style = space 4 | end_of_line = lf 5 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | generated.ts* 26 | graphql.schema.json 27 | -------------------------------------------------------------------------------- /client/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-azuretools.vscode-azurestaticwebapps", 5 | "dbaeumer.vscode-eslint", 6 | "apollographql.vscode-apollo", 7 | "esbenp.prettier-vscode" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /client/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "npm start", 6 | "name": "Run frontend", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | }, 10 | { 11 | "command": "npm start", 12 | "name": "Run backend", 13 | "request": "launch", 14 | "type": "node-terminal", 15 | "cwd": "${workspaceFolder}/api" 16 | } 17 | ], 18 | "compounds": [ 19 | { 20 | "name": "Run full stack", 21 | "configurations": ["Run frontend", "Run backend"] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /client/GraphQL.queries.md: -------------------------------------------------------------------------------- 1 | # List of games 2 | 3 | ``` 4 | query { 5 | games { 6 | id, 7 | state, 8 | players { 9 | id, 10 | name 11 | } 12 | } 13 | } 14 | ``` 15 | 16 | # List of questions 17 | 18 | 1. Update `./api/api/graphql/schema.graphql` with new query to Query type: 19 | 20 | ``` 21 | type Query { 22 | game(id: ID!): Game 23 | games: [Game!]! 24 | playerResults(gameId: ID!, playerId: ID!): [PlayerResult!]! 25 | questions: [Question] 26 | } 27 | ``` 28 | 29 | 1. Add the following code to `/api/graphql/resolvers.ts`. 30 | 31 | ```javascript 32 | questions(_, __, { dataSources }) { 33 | return dataSources.question.getQuestions(); 34 | }, 35 | ``` 36 | 37 | 1. Query for questions in playground 38 | 39 | ``` 40 | query { 41 | questions { 42 | id, 43 | question, 44 | correctAnswer, 45 | answers 46 | } 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aswa-react-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://localhost:7071", 6 | "dependencies": { 7 | "@apollo/client": "^3.6.9", 8 | "graphql": "^16.6.0", 9 | "graphql-tag": "^2.12.6", 10 | "react": "^18.2.0", 11 | "react-router-dom": "^6.3.0", 12 | "react-scripts": "5.0.1", 13 | "web-vitals": "^2.1.4" 14 | }, 15 | "devDependencies": { 16 | "@testing-library/jest-dom": "^5.11.9", 17 | "@testing-library/react": "^11.2.5", 18 | "@testing-library/user-event": "^12.6.3", 19 | "@types/jest": "^26.0.20", 20 | "@types/node": "^14.14.25", 21 | "@types/react": "^17.0.1", 22 | "@types/react-dom": "^18.0.6", 23 | "@types/react-router-dom": "^5.1.7", 24 | "react-dom": "^18.2.0" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/js-e2e-graphql-cosmosdb-static-web-app/c3f38fc66b82303399caed6d81ade6c891e05cb2/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/js-e2e-graphql-cosmosdb-static-web-app/c3f38fc66b82303399caed6d81ade6c891e05cb2/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/js-e2e-graphql-cosmosdb-static-web-app/c3f38fc66b82303399caed6d81ade6c891e05cb2/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 1024px; 3 | margin: 0 auto; 4 | } 5 | 6 | ul { 7 | list-style: none; 8 | } 9 | 10 | li.submitted { 11 | font-style: italic; 12 | } 13 | 14 | li.correct { 15 | font-weight: bold; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.css"; 3 | import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; 4 | import CreateGame from "./pages/CreateGame"; 5 | import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client"; 6 | import JoinGame from "./pages/JoinGame"; 7 | import PlayGame from "./pages/PlayGame"; 8 | import CompleteGame from "./pages/CompleteGame"; 9 | 10 | const client = new ApolloClient({ 11 | uri: "/api/graphql", 12 | cache: new InMemoryCache(), 13 | }); 14 | 15 | function App() { 16 | return ( 17 | 18 | 19 | 20 | } /> 21 | } /> 22 | } /> 23 | } /> 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import './index.css'; 5 | import App from './App'; 6 | import * as serviceWorker from './serviceWorker'; 7 | 8 | const container = document.getElementById('root'); 9 | const root = createRoot(container!); // createRoot(container!) if you use TypeScript 10 | 11 | 12 | root.render( 13 | 14 | 15 | 16 | ); 17 | 18 | // If you want your app to work offline and load faster, you can change 19 | // unregister() to register() below. Note this comes with some pitfalls. 20 | // Learn more about service workers: https://bit.ly/CRA-PWA 21 | serviceWorker.unregister(); 22 | -------------------------------------------------------------------------------- /client/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/src/operations/addPlayer.graphql: -------------------------------------------------------------------------------- 1 | mutation addPlayerScreen($id: ID!, $name: String!) { 2 | addPlayerToGame(id: $id, name: $name) { 3 | id 4 | } 5 | startGame(id: $id) { 6 | id 7 | players { 8 | id 9 | name 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/src/operations/answers.graphql: -------------------------------------------------------------------------------- 1 | mutation submitAnswer( 2 | $gameId: ID! 3 | $playerId: ID! 4 | $questionId: ID! 5 | $answer: String! 6 | ) { 7 | submitAnswer( 8 | gameId: $gameId 9 | playerId: $playerId 10 | questionId: $questionId 11 | answer: $answer 12 | ) { 13 | id 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/operations/createGame.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateGame { 2 | createGame { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /client/src/operations/endGame.graphql: -------------------------------------------------------------------------------- 1 | query playerResults($gameId: ID!, $playerId: ID!) { 2 | playerResults(gameId: $gameId, playerId: $playerId) { 3 | correct 4 | question 5 | answers 6 | correctAnswer 7 | submittedAnswer 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/src/operations/getGame.graphql: -------------------------------------------------------------------------------- 1 | query getGame($id: ID!) { 2 | game(id: $id) { 3 | questions { 4 | id 5 | question 6 | answers 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/src/pages/CompleteGame.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@apollo/client"; 2 | import React from "react"; 3 | import { useParams } from "react-router-dom"; 4 | import { PlayerResultsDocument } from "../generated"; 5 | 6 | const CompleteGame: React.FC = () => { 7 | const { id, playerId } = useParams() as { id: string; playerId: string }; 8 | 9 | const { loading, data } = useQuery(PlayerResultsDocument, { 10 | variables: { 11 | gameId: id, 12 | playerId, 13 | }, 14 | }); 15 | 16 | if (loading || !data) { 17 | return

Waiting for your answers

; 18 | } 19 | 20 | return ( 21 |
22 |

Game over man, game over!

23 | {data.playerResults.map((result) => { 24 | return ( 25 |
26 |

27 | {result.correct ? "✅" : "❌"} 28 | {" "} 31 |

32 |
    33 | {result.answers.map((a) => { 34 | return ( 35 |
  • 41 | 42 |
  • 43 | ); 44 | })} 45 |
46 |
47 | ); 48 | })} 49 |
50 | ); 51 | }; 52 | 53 | export default CompleteGame; 54 | -------------------------------------------------------------------------------- /client/src/pages/CreateGame.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@apollo/client"; 2 | import { GraphQLError } from "graphql"; 3 | import React, { useEffect, useState } from "react"; 4 | import { useNavigate } from "react-router-dom"; 5 | import { CreateGameDocument } from "../generated"; 6 | 7 | const CreateGame: React.FC = () => { 8 | const [apiError, setApiError] = useState(""); 9 | const [creating, setCreating] = useState(false); 10 | const [createGame, { loading, called, data, error }] = useMutation( 11 | CreateGameDocument 12 | ); 13 | 14 | const navigate = useNavigate(); 15 | 16 | useEffect(() => { 17 | if (creating) { 18 | createGame() 19 | .then(() => { console.log(`success`)}) 20 | .catch((error) => { 21 | console.log(error); 22 | // @ts-ignore 23 | setApiError(JSON.stringify(error)); 24 | }); 25 | } 26 | }, [creating, createGame]); 27 | 28 | useEffect(() => { 29 | if (!loading && called && !error && data && data.createGame) { 30 | navigate(`/game/join/${data.createGame.id}`); 31 | } else if (error) { 32 | console.error(error); 33 | } 34 | }, [loading, called, data, error, navigate]); 35 | 36 | return ( 37 |
38 |

Create a new game!

39 | 40 | { apiError && 41 |
42 |

Error: {apiError}

43 |
44 | } 45 | 46 | { !apiError && 47 | 50 | } 51 |
52 | ); 53 | }; 54 | 55 | export default CreateGame; 56 | -------------------------------------------------------------------------------- /client/src/pages/JoinGame.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@apollo/client"; 2 | import React, { useEffect, useState } from "react"; 3 | import { useNavigate, useParams } from "react-router-dom"; 4 | import { AddPlayerScreenDocument } from "../generated"; 5 | 6 | const JoinGame: React.FC = () => { 7 | const { id } = useParams() as { id: string }; 8 | const navigate = useNavigate(); 9 | const [name, setName] = useState(""); 10 | const [addPlayerToGame, { loading, data }] = useMutation( 11 | AddPlayerScreenDocument 12 | ); 13 | 14 | useEffect(() => { 15 | if (data) { 16 | navigate(`/game/play/${id}/${data.addPlayerToGame.id}`); 17 | } 18 | }, [data, id, navigate]); 19 | 20 | return ( 21 |
22 |

Join the game: {id}

23 |
24 | 25 | setName(e.target.value)} 30 | /> 31 |
32 |
33 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default JoinGame; 49 | -------------------------------------------------------------------------------- /client/src/pages/PlayGame.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation, useQuery } from "@apollo/client"; 2 | import React, { useEffect, useState } from "react"; 3 | import { useNavigate, useParams } from "react-router-dom"; 4 | import { GetGameDocument, Question, SubmitAnswerDocument } from "../generated"; 5 | import useInterval from "../useInterval"; 6 | 7 | const PlayGame: React.FC = () => { 8 | const [apiError, setApiError] = useState(""); 9 | const { id, playerId } = useParams() as { id: string; playerId: string }; 10 | const navigate = useNavigate(); 11 | const [timeRemaining, setTimeRemaining] = useState(30); 12 | const [answer, setAnswer] = useState(""); 13 | const { loading, data } = useQuery(GetGameDocument, { variables: { id } }); 14 | const [question, setQuestion] = useState<{ 15 | question: string; 16 | answers: string[]; 17 | id: string; 18 | }>(); 19 | const [submitAnswer, { loading: mutationLoading }] = useMutation( 20 | SubmitAnswerDocument 21 | ); 22 | const [questions, setQuestions] = useState< 23 | Pick[] 24 | >([]); 25 | 26 | useEffect(() => { 27 | if (!loading && data && data.game && data.game.questions && data.game.questions.length>0) { 28 | setApiError(""); 29 | setQuestions([...data.game.questions]) 30 | } else { 31 | // @ts-ignore 32 | setApiError("No game trivia questions found"); 33 | } 34 | }, [loading, data]); 35 | 36 | useInterval(() => { 37 | if (question) { 38 | if (timeRemaining === 0) { 39 | const q = questions.pop(); 40 | 41 | if (!q) { 42 | navigate(`/game/finish/${id}/${playerId}`); 43 | return; 44 | } else { 45 | setTimeRemaining(30); 46 | setQuestion(q); 47 | setAnswer(""); 48 | } 49 | } else { 50 | setTimeRemaining(timeRemaining - 1); 51 | } 52 | } 53 | }, 1000); 54 | 55 | useEffect(() => { 56 | if (questions.length) { 57 | const q = questions.pop(); 58 | setTimeRemaining(30); 59 | setQuestion(q); 60 | } 61 | }, [questions]); 62 | 63 | useEffect(() => { 64 | if (timeRemaining === 0 && question) { 65 | submitAnswer({ 66 | variables: { 67 | gameId: id, 68 | playerId, 69 | questionId: question.id, 70 | answer, 71 | }, 72 | }); 73 | } 74 | }, [submitAnswer, id, playerId, question, answer, timeRemaining]); 75 | 76 | if ((loading || !question) && !apiError ) { 77 | return

Just getting the game ready, please wait

; 78 | } 79 | 80 | return ( 81 |
82 |

Play game {id}!

83 | 84 | { apiError && 85 |
86 |

Error: {apiError}

87 |
88 | } 89 | 90 | { (!apiError && question) && 91 |
92 |

Time remaining: {timeRemaining}

93 |

94 |
    95 | {question.answers.map((a) => { 96 | return ( 97 |
  • 98 | 107 |
  • 108 | ); 109 | })} 110 |
111 | 114 |
115 | } 116 |
117 | ); 118 | }; 119 | 120 | export default PlayGame; 121 | -------------------------------------------------------------------------------- /client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /client/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /client/src/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | function useInterval(callback: () => void, delay: number) { 4 | const savedCallback = useRef<() => void>(); 5 | 6 | // Remember the latest callback. 7 | useEffect(() => { 8 | savedCallback.current = callback; 9 | }, [callback]); 10 | 11 | // Set up the interval. 12 | useEffect(() => { 13 | function tick() { 14 | if (savedCallback.current) { 15 | savedCallback.current(); 16 | } 17 | } 18 | if (delay !== null) { 19 | let id = setInterval(tick, delay); 20 | return () => clearInterval(id); 21 | } 22 | }, [delay]); 23 | } 24 | 25 | export default useInterval; 26 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "include": ["src"] 20 | } 21 | --------------------------------------------------------------------------------