├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── actions │ └── prepare │ │ └── action.yml └── workflows │ ├── build.yml │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .release-it.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src ├── ambient.d.ts ├── context.ts ├── index.ts ├── prismaModeller.ts ├── serviceFile.codefacts.ts ├── serviceFile.ts ├── sharedSchema.ts ├── tests │ ├── bugs │ │ ├── parentCanBeGraphQLObject.test.ts │ │ └── returnObjectCanBeGraphQLInterfaces.test.ts │ ├── features │ │ ├── generatesTypesForUnions.test.ts │ │ ├── preferPromiseFnWhenKnown.test.ts │ │ ├── returnTypePositionsWhichPreferPrisma.test.ts │ │ ├── supportGenericExtension.test.ts │ │ ├── supportReferringToEnumsOnlyInSDL.test.ts │ │ └── warnOnSuperfluousResolvers.test.ts │ ├── integration.puzzmo.test.ts │ ├── serviceFile.test.ts │ ├── testRunner.ts │ └── vendor │ │ ├── puzzmo │ │ └── one-offs │ │ │ └── userProfiles.ts │ │ ├── soccersage-output │ │ ├── games.d.ts │ │ ├── predictions.d.ts │ │ ├── seasons.d.ts │ │ ├── shared-return-types.d.ts │ │ ├── shared-schema-types.d.ts │ │ ├── teams.d.ts │ │ └── users.d.ts │ │ └── soccersage.io-main │ │ ├── .redwood │ │ └── schema.graphql │ │ ├── README.md │ │ ├── api │ │ ├── db │ │ │ └── schema.prisma │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── server.config.js │ │ ├── src │ │ │ ├── graphql │ │ │ │ ├── .keep │ │ │ │ ├── games.sdl.ts │ │ │ │ ├── predictions.sdl.ts │ │ │ │ ├── seasons.sdl.ts │ │ │ │ ├── teams.sdl.ts │ │ │ │ └── users.sdl.ts │ │ │ └── services │ │ │ │ ├── .keep │ │ │ │ ├── games │ │ │ │ ├── games.scenarios.ts │ │ │ │ └── games.ts │ │ │ │ ├── predictions │ │ │ │ ├── predictions.scenarios.ts │ │ │ │ └── predictions.ts │ │ │ │ ├── seasons │ │ │ │ ├── seasons.scenarios.ts │ │ │ │ └── seasons.ts │ │ │ │ ├── teams │ │ │ │ ├── teams.scenarios.ts │ │ │ │ └── teams.ts │ │ │ │ └── users │ │ │ │ ├── users.scenarios.ts │ │ │ │ └── users.ts │ │ └── tsconfig.json │ │ └── license.md ├── tsBuilder.ts ├── typeFacts.ts ├── typeMap.ts ├── types.ts └── utils.ts ├── tsconfig.json └── vitest.config.ts /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | git@joshuakgoldberg.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [mozilla coc]: https://github.com/mozilla/diversity 131 | [faq]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for your interest in contributing to `sdl-codegen`! 💖 4 | 5 | > After this page, see [DEVELOPMENT.md](./DEVELOPMENT.md) for local development instructions. 6 | 7 | ## Code of Conduct 8 | 9 | This project contains a [Contributor Covenant code of conduct](./CODE_OF_CONDUCT.md) all contributors are expected to follow. 10 | 11 | ## Reporting Issues 12 | 13 | Please do [report an issue on the issue tracker](https://github.com/sdl-codegen/sdl-codegen/issues/new/choose) if there's any bugfix, documentation improvement, or general enhancement you'd like to see in the repository! Please fully fill out all required fields in the most appropriate issue form. 14 | 15 | ## Sending Contributions 16 | 17 | Sending your own changes as contribution is always appreciated! 18 | There are two steps involved: 19 | 20 | 1. [Finding an Issue](#finding-an-issue) 21 | 2. [Sending a Pull Request](#sending-a-pull-request) 22 | 23 | ### Finding an Issue 24 | 25 | With the exception of very small typos, all changes to this repository generally need to correspond to an [open issue marked as `accepting prs` on the issue tracker](https://github.com/sdl-codegen/sdl-codegen/issues?q=is%3Aopen+is%3Aissue+label%3A%22accepting+prs%22). 26 | If this is your first time contributing, consider searching for [unassigned issues that also have the `good first issue` label](https://github.com/sdl-codegen/sdl-codegen/issues?q=is%3Aopen+is%3Aissue+label%3A%22accepting+prs%22+label%3A%22good+first+issue%22+no%3Aassignee). 27 | If the issue you'd like to fix isn't found on the issue, see [Reporting Issues](#reporting-issues) for filing your own (please do!). 28 | 29 | ### Sending a Pull Request 30 | 31 | Once you've identified an open issue accepting PRs that doesn't yet have a PR sent, you're free to send a pull request. 32 | Be sure to fill out the pull request template's requested information -- otherwise your PR will likely be closed. 33 | 34 | PRs are also expected to have a title that adheres to [commitlint](https://github.com/conventional-changelog/commitlint). 35 | Only PR titles need to be in that format, not individual commits. 36 | Don't worry if you get this wrong: you can always change the PR title after sending it. 37 | Check [previously merged PRs](https://github.com/sdl-codegen/sdl-codegen/pulls?q=is%3Apr+is%3Amerged+-label%3Adependencies+) for reference. 38 | 39 | #### Draft PRs 40 | 41 | If you don't think your PR is ready for review, [set it as a draft](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request#converting-a-pull-request-to-a-draft). 42 | Draft PRs won't be reviewed. 43 | 44 | #### Granular PRs 45 | 46 | Please keeep pull requests single-purpose: in other words, don't attempt to solve multiple unrelated problems in one pull request. 47 | Send one PR per area of concern. 48 | Multi-purpose pull requests are harder and slower to review, block all changes from being merged until the whole pull request is reviewed, and are difficult to name well with semantic PR titles. 49 | 50 | #### Pull Request Reviews 51 | 52 | When a PR is not in draft, it's considered ready for review. 53 | Please don't manually `@` tag anybody to request review. 54 | A maintainer will look at it when they're next able to. 55 | 56 | PRs should have passing [GitHub status checks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks) before review is requested (unless there are explicit questions asked in the PR about any failures). 57 | 58 | #### Requested Changes 59 | 60 | After a maintainer reviews your PR, they may request changes on it. 61 | Once you've made those changes, [re-request review on GitHub](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/about-pull-request-reviews#re-requesting-a-review). 62 | 63 | Please try not to force-push commits to PRs that have already been reviewed. 64 | Doing so makes it harder to review the changes. 65 | We squash merge all commits so there's no need to try to preserve Git history within a PR branch. 66 | -------------------------------------------------------------------------------- /.github/actions/prepare/action.yml: -------------------------------------------------------------------------------- 1 | description: Prepares the repo for a typical CI job 2 | 3 | name: Prepare 4 | runs: 5 | steps: 6 | - uses: pnpm/action-setup@v2 7 | - uses: actions/setup-node@v3 8 | with: 9 | cache: pnpm 10 | node-version: "18" 11 | - run: pnpm install 12 | shell: bash 13 | using: composite 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | build: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - uses: actions/checkout@v3 6 | - uses: ./.github/actions/prepare 7 | - run: pnpm build 8 | - run: node ./lib/index.js 9 | 10 | name: Build 11 | 12 | on: 13 | pull_request: ~ 14 | 15 | push: 16 | branches: 17 | - main 18 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | lint: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - uses: actions/checkout@v3 6 | - uses: ./.github/actions/prepare 7 | - run: pnpm lint 8 | 9 | name: Lint 10 | 11 | on: 12 | pull_request: ~ 13 | 14 | push: 15 | branches: 16 | - main 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | concurrency: 2 | cancel-in-progress: false 3 | group: ${{ github.workflow }} 4 | 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | with: 11 | fetch-depth: 0 12 | - uses: ./.github/actions/prepare 13 | - run: pnpm build 14 | - run: git config user.name "${GITHUB_ACTOR}" 15 | - run: git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 16 | - run: git stash 17 | - env: 18 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 19 | run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 20 | - env: 21 | GITHUB_TOKEN: ${{ github.token }} 22 | run: if pnpm run should-semantic-release ; then pnpm release-it --verbose ; fi 23 | 24 | name: Release 25 | 26 | on: 27 | push: 28 | branches: 29 | - main 30 | 31 | permissions: 32 | contents: write 33 | id-token: write 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | test: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - uses: actions/checkout@v3 6 | - uses: ./.github/actions/prepare 7 | - run: pnpm run test 8 | 9 | name: Test 10 | 11 | on: 12 | pull_request: ~ 13 | 14 | push: 15 | branches: 16 | - main 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | lib/ 3 | node_modules/ 4 | .DS_Store -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore: release v${version}", 4 | "requireCommits": true 5 | }, 6 | "github": { 7 | "autoGenerate": true, 8 | "release": true, 9 | "releaseName": "v${version}" 10 | }, 11 | "hooks": { 12 | "before:bump": "if ! pnpm run should-semantic-release --verbose ; then exit 1 ; fi" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### 3.0.0 4 | 5 | - .d.ts generation re-write using a new technique which is going to turn into its own library. The .d.ts files are not pretty. That's not a priority for me right now. 6 | 7 | ### 2.0.0 8 | 9 | - Redwood uses prettier 3, and prettier 3 removes the sync API. This means we now have to operate entirely async. This is a breaking change from the sdl-codegen API, as you need to await the exposed public fns. 10 | - There is a verbose option which provides info on the timings of the codegen. 11 | - Big watch mode improvements 12 | 13 | ### 1.1.2 14 | 15 | Pulled back this a lot because its over-reaching: 16 | 17 | - When a resolver is simply a fn to a literal, narrow to that exact type in the codegen (instead of keeping the optional promise type) 18 | 19 | ### 1.1.0 20 | 21 | - Adds a watcher function to the return value of`createWatcher` which allows tools to be able to hook in and let SDL Codegen only re-generate what's needed. 22 | 23 | - Adds a `verbose` flag which offers some timing logs, and watcher logs. 24 | 25 | - Service file d.ts, and shared schema .d.ts' do not write to the file if the content hasn't changed to avoid triggering watchers. 26 | 27 | - Better handling of modern prettier versions 28 | 29 | - Improvements to codegen when using GraphQL types and interfaces in parent or return positions 30 | 31 | - ~~When a resolver is simply a fn to a literal, narrow to that exact type in the codegen (instead of keeping the optional promise type)~~ 32 | 33 | ### 1.0.2 34 | 35 | - Better prettier detection (and fallback) for the generated files, re #14 36 | 37 | ### 1.0 38 | 39 | - No changes 40 | 41 | ### 0.0.14 42 | 43 | - Exports the types for GraphQL unions 44 | 45 | ### 0.0.13 46 | 47 | - Adds support for generating unions in the shared file 48 | - Fixes the references to enums in sdl args 49 | 50 | ### 0.0.10 51 | 52 | - Service files do not trigger eslint warnings in (my) app OOTB 53 | - Adds support for running prettier in the generated files. 54 | - Fixes paths for the graphql types which come from `@redwoodjs/graphql-server` 55 | - Does not create empty service .d.ts files 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

SDL-Codegen

2 | 3 |

GraphQL .d.ts file generation for SDL-first projects

4 | 5 | This project is for creating the `.d.ts` files for codebases where you have SDL like: 6 | 7 | ```graphql 8 | export const schema = gql` 9 | type Game { 10 | id: ID! 11 | homeTeamID: Int! 12 | awayTeamID: Int! 13 | homeTeamScore: Int! 14 | awayTeamScore: Int! 15 | } 16 | 17 | type Query { 18 | games: [Game!]! @skipAuth 19 | upcomingGames: [Game!]! @skipAuth 20 | game(id: ID!): Game @requireAuth 21 | } 22 | ` 23 | ``` 24 | 25 | Then separately, you write functions like: 26 | 27 | ```ts 28 | export const games = () => db.game.findMany({ orderBy: { startDateTime: "asc" } }) 29 | 30 | export const upcomingGames = () => db.game.findMany({ isCompleted: false, startDateTime: { gt: new Date() } }) 31 | 32 | export const game = ({ id }) => db.game.findUnique({ where: { id } }) 33 | ``` 34 | 35 | This repo will create `.d.ts` files which very accurately, and very concisely represent the runtime for these functions. It's goal is to take all of the possible logic which might happen in the TypeScript type system, and pre-bake that into the output of the .d.ts files. 36 | 37 | You could think of it as a smaller, more singular focused version of the mature and well-featured [graphql-codgen](https://the-guild.dev/graphql/codegen). 38 | 39 | ## Vision 40 | 41 | This repo provides the APIs for building a codegen for framework authors or confident tool builders, and the goal is not to provide a CLI for a generalized use-case. 42 | 43 | ## Pipeline 44 | 45 | This app is architected as a pipeline of sorts. Here's the rough stages: 46 | 47 | - Get inputs: GraphQL schema, the source files to represent, Prisma dmmf and dts output config 48 | - Parse inputs: Parse the GraphQL schema, parse source files into facts about the code, parse the Prisma dmmf 49 | - Centralize data: Keep a central store of all of these things, and have specific updaters so that a watcher can be built. 50 | - Generate outputs: Generate .d.ts files for the files we want to generate them for 51 | 52 | It's still a bit of a work in progress to have these discrete steps, but it's getting there. 53 | 54 | ## Development 55 | 56 | See [`.github/CONTRIBUTING.md`](./.github/CONTRIBUTING.md), then [`.github/DEVELOPMENT.md`](./.github/DEVELOPMENT.md). 57 | 58 | Thanks! 59 | 60 | ## Deployment 61 | 62 | Make a commit like: `git commit --allow-empty -m "feat: Prepare for release"` 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sdl-codegen/node", 3 | "version": "3.0.4", 4 | "description": "GraphQL .d.ts file generation for SDL-first projects", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/puzzmo-com/sdl-codegen" 8 | }, 9 | "license": "MIT", 10 | "author": "Orta Therox ", 11 | "type": "module", 12 | "main": "./lib/index.js", 13 | "files": [ 14 | "lib/", 15 | "package.json", 16 | "LICENSE.md", 17 | "README.md" 18 | ], 19 | "scripts": { 20 | "build": "tsc", 21 | "build:watch": "tsc --watch", 22 | "format": "prettier \"**/*\" --ignore-unknown", 23 | "format:write": "pnpm format --write", 24 | "jest": "vitest", 25 | "lint": "eslint . --report-unused-disable-directives", 26 | "lint:knip": "knip", 27 | "lint:md": "markdownlint \"**/*.md\" \".github/**/*.md\" --rules sentences-per-line", 28 | "lint:package": "npmPkgJsonLint .", 29 | "lint:packages": "pnpm-deduplicate --list", 30 | "lint:spelling": "cspell \"**\" \".github/**/*\"", 31 | "lint:tsc": "tsc --noEmit", 32 | "prepare": "husky install", 33 | "should-semantic-release": "should-semantic-release --verbose", 34 | "test": "vitest", 35 | "type-check": "tsc --noEmit" 36 | }, 37 | "lint-staged": { 38 | "*": "prettier --ignore-unknown --write", 39 | "*.ts": "eslint --fix" 40 | }, 41 | "dependencies": { 42 | "@babel/generator": "7.26.0", 43 | "@babel/parser": "^7.26.2", 44 | "@babel/traverse": "7.25.9", 45 | "@babel/types": "7.26.0", 46 | "@mrleebo/prisma-ast": "^0.12.0", 47 | "ts-morph": "^22.0.0" 48 | }, 49 | "devDependencies": { 50 | "@babel/core": "^7.20.12", 51 | "@types/babel__generator": "^7.6.0", 52 | "@types/babel__traverse": "^7.14.0", 53 | "@types/eslint": "^8.21.1", 54 | "@types/node": "^16.16.0", 55 | "@typescript-eslint/eslint-plugin": "^5.48.2", 56 | "@typescript-eslint/parser": "^5.48.2", 57 | "@typescript/vfs": "1.4.0", 58 | "cspell": "^6.19.2", 59 | "esbuild": "^0.17.18", 60 | "eslint": "^8.32.0", 61 | "eslint-config-prettier": "^8.6.0", 62 | "eslint-plugin-deprecation": "^1.4.1", 63 | "eslint-plugin-eslint-comments": "^3.2.0", 64 | "eslint-plugin-import": "^2.27.5", 65 | "eslint-plugin-jsonc": "^2.6.0", 66 | "eslint-plugin-markdown": "^3.0.0", 67 | "eslint-plugin-no-only-tests": "^3.1.0", 68 | "eslint-plugin-regexp": "^1.12.0", 69 | "eslint-plugin-simple-import-sort": "^10.0.0", 70 | "eslint-plugin-typescript-sort-keys": "^2.3.0", 71 | "eslint-plugin-yml": "^1.5.0", 72 | "graphql": "^16.0.0", 73 | "husky": "^8.0.3", 74 | "jsonc-eslint-parser": "^2.1.0", 75 | "knip": "2.9.0", 76 | "lint-staged": "^13.1.0", 77 | "markdownlint": "^0.28.0", 78 | "markdownlint-cli": "^0.33.0", 79 | "npm-package-json-lint": "^6.4.0", 80 | "npm-package-json-lint-config-default": "^5.0.0", 81 | "pnpm-deduplicate": "^0.4.1", 82 | "prettier": "^2.8.3", 83 | "prettier-plugin-packagejson": "^2.4.2", 84 | "release-it": "^15.6.0", 85 | "sentences-per-line": "^0.2.1", 86 | "should-semantic-release": "^0.1.0", 87 | "typescript": "^5.6.3", 88 | "vitest": "^0.31.1", 89 | "yaml-eslint-parser": "^1.2.0" 90 | }, 91 | "peerDependencies": { 92 | "graphql": "*", 93 | "prettier": "^2", 94 | "typescript": "*" 95 | }, 96 | "peerDependenciesMeta": { 97 | "prettier": { 98 | "optional": true 99 | } 100 | }, 101 | "packageManager": "pnpm@7.32.2", 102 | "engines": { 103 | "node": ">=16" 104 | }, 105 | "publishConfig": { 106 | "access": "public", 107 | "provenance": true 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | declare module "prettier" 2 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import * as graphql from "graphql" 2 | import * as tsMorph from "ts-morph" 3 | import { FormatCodeSettings, System } from "typescript" 4 | 5 | import { PrismaMap } from "./prismaModeller.js" 6 | import { CodeFacts, FieldFacts } from "./typeFacts.js" 7 | 8 | export interface AppContext { 9 | /** POSIX-y fn not built into System */ 10 | basename: (path: string) => string 11 | /** "service" should be code here */ 12 | codeFacts: Map 13 | /** A global set of facts about resolvers focused from the GQL side */ 14 | fieldFacts: Map 15 | /** When we emit .d.ts files, it runs the ts formatter over the file first - you can override the default settings */ 16 | formatCodeSettings?: FormatCodeSettings 17 | /** So you can override the formatter */ 18 | gql: graphql.GraphQLSchema 19 | /** POSXIY- fn not built into System */ 20 | join: (...paths: string[]) => string 21 | /** Where to find particular files */ 22 | pathSettings: { 23 | apiServicesPath: string 24 | graphQLSchemaPath: string 25 | prismaDSLPath: string 26 | root: string 27 | sharedFilename: string 28 | sharedInternalFilename: string 29 | typesFolderRoot: string 30 | } 31 | 32 | /** A map of prisma models */ 33 | prisma: PrismaMap 34 | /** An implementation of the TypeScript system, this can be grabbed pretty 35 | * easily from the typescript import, or you can use your own like tsvfs in browsers. 36 | */ 37 | sys: System 38 | /** ts-morph is used to abstract over the typescript compiler API, this project file 39 | * is a slightly augmented version of the typescript Project api. 40 | */ 41 | tsProject: tsMorph.Project 42 | } 43 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { getSchema as getPrismaSchema } from "@mrleebo/prisma-ast" 2 | import * as graphql from "graphql" 3 | import { Project } from "ts-morph" 4 | import typescript from "typescript" 5 | 6 | import { AppContext } from "./context.js" 7 | import { PrismaMap, prismaModeller } from "./prismaModeller.js" 8 | import { lookAtServiceFile } from "./serviceFile.js" 9 | import { createSharedSchemaFiles } from "./sharedSchema.js" 10 | import { CodeFacts, FieldFacts } from "./typeFacts.js" 11 | import { RedwoodPaths } from "./types.js" 12 | 13 | export * from "./types.js" 14 | 15 | import { basename, join } from "node:path" 16 | 17 | import { makeStep } from "./utils.js" 18 | 19 | export interface SDLCodeGenReturn { 20 | // Optional way to start up a watcher mode for the codegen 21 | createWatcher: () => { fileChanged: (path: string) => Promise } 22 | // Paths which were added/changed during the run 23 | paths: string[] 24 | } 25 | 26 | /** The API specifically for the Redwood preset */ 27 | export async function runFullCodegen( 28 | preset: "redwood", 29 | config: { paths: RedwoodPaths; sys?: typescript.System; verbose?: true } 30 | ): Promise 31 | 32 | export async function runFullCodegen(preset: string, config: unknown): Promise 33 | 34 | export async function runFullCodegen(preset: string, config: unknown): Promise { 35 | if (preset !== "redwood") throw new Error("Only Redwood codegen is supported at this time") 36 | const verbose = !!(config as { verbose?: true }).verbose 37 | const startTime = Date.now() 38 | const step = makeStep(verbose) 39 | 40 | const paths = (config as { paths: RedwoodPaths }).paths 41 | const sys = typescript.sys 42 | 43 | const pathSettings: AppContext["pathSettings"] = { 44 | root: paths.base, 45 | apiServicesPath: paths.api.services, 46 | prismaDSLPath: paths.api.dbSchema, 47 | graphQLSchemaPath: paths.generated.schema, 48 | sharedFilename: "shared-schema-types.d.ts", 49 | sharedInternalFilename: "shared-return-types.d.ts", 50 | typesFolderRoot: paths.api.types, 51 | } 52 | 53 | const project = new Project({ useInMemoryFileSystem: true }) 54 | 55 | let gqlSchema: graphql.GraphQLSchema | undefined 56 | const getGraphQLSDLFromFile = (settings: AppContext["pathSettings"]) => { 57 | const schema = sys.readFile(settings.graphQLSchemaPath) 58 | if (!schema) throw new Error("No schema found at " + settings.graphQLSchemaPath) 59 | gqlSchema = graphql.buildSchema(schema) 60 | } 61 | 62 | let prismaSchema: PrismaMap = new Map() 63 | const getPrismaSchemaFromFile = (settings: AppContext["pathSettings"]) => { 64 | const prismaSchemaText = sys.readFile(settings.prismaDSLPath) 65 | if (!prismaSchemaText) throw new Error("No prisma file found at " + settings.prismaDSLPath) 66 | const prismaSchemaBlocks = getPrismaSchema(prismaSchemaText) 67 | prismaSchema = prismaModeller(prismaSchemaBlocks) 68 | } 69 | 70 | await step("Read the GraphQL schema", () => getGraphQLSDLFromFile(pathSettings)) 71 | await step("Read the Prisma schema", () => getPrismaSchemaFromFile(pathSettings)) 72 | 73 | if (!gqlSchema) throw new Error("No GraphQL Schema was created during setup") 74 | 75 | const appContext: AppContext = { 76 | gql: gqlSchema, 77 | prisma: prismaSchema, 78 | tsProject: project, 79 | codeFacts: new Map(), 80 | fieldFacts: new Map(), 81 | pathSettings, 82 | sys, 83 | join, 84 | basename, 85 | } 86 | 87 | // All changed files 88 | const filepaths = [] as string[] 89 | 90 | // Create the two shared schema files 91 | await step("Create shared schema files", async () => { 92 | const sharedDTSes = await createSharedSchemaFiles(appContext, verbose) 93 | filepaths.push(...sharedDTSes) 94 | }) 95 | 96 | let knownServiceFiles: string[] = [] 97 | const createDTSFilesForAllServices = async () => { 98 | const serviceFiles = appContext.sys.readDirectory(appContext.pathSettings.apiServicesPath) 99 | knownServiceFiles = serviceFiles.filter(isRedwoodServiceFile) 100 | 101 | for (const path of knownServiceFiles) { 102 | const dts = await lookAtServiceFile(path, appContext) 103 | if (dts) filepaths.push(dts) 104 | } 105 | } 106 | 107 | // Initial run 108 | await step("Create DTS files for all services", createDTSFilesForAllServices) 109 | 110 | const endTime = Date.now() 111 | const timeTaken = endTime - startTime 112 | if (verbose) console.log(`[sdl-codegen]: Full run took ${timeTaken}ms`) 113 | 114 | const createWatcher = () => { 115 | const oldSDL = "" 116 | 117 | return { 118 | fileChanged: async (path: string) => { 119 | if (isTypesFile(path)) return 120 | if (path === appContext.pathSettings.graphQLSchemaPath) { 121 | const newSDL = appContext.sys.readFile(path) 122 | if (newSDL === oldSDL) return 123 | 124 | if (verbose) console.log("[sdl-codegen] SDL Schema changed") 125 | await step("GraphQL schema changed", () => getGraphQLSDLFromFile(appContext.pathSettings)) 126 | await step("Create all shared schema files", () => createSharedSchemaFiles(appContext, verbose)) 127 | await step("Create all service files", createDTSFilesForAllServices) 128 | } else if (path === appContext.pathSettings.prismaDSLPath) { 129 | await step("Prisma schema changed", () => getPrismaSchemaFromFile(appContext.pathSettings)) 130 | await step("Create all shared schema files", createDTSFilesForAllServices) 131 | } else if (isRedwoodServiceFile(path)) { 132 | if (!knownServiceFiles.includes(path)) { 133 | await step("Create all shared schema files", createDTSFilesForAllServices) 134 | } else { 135 | await step("Create known service files", () => lookAtServiceFile(path, appContext)) 136 | } 137 | } 138 | }, 139 | } 140 | } 141 | 142 | return { 143 | paths: filepaths, 144 | createWatcher, 145 | } 146 | } 147 | 148 | const isTypesFile = (file: string) => file.endsWith(".d.ts") 149 | 150 | const isRedwoodServiceFile = (file: string) => { 151 | if (!file.includes("services")) return false 152 | if (file.endsWith(".d.ts")) return false 153 | if (file.endsWith(".test.ts") || file.endsWith(".test.js")) return false 154 | if (file.endsWith("scenarios.ts") || file.endsWith("scenarios.js")) return false 155 | return file.endsWith(".ts") || file.endsWith(".tsx") || file.endsWith(".js") 156 | } 157 | -------------------------------------------------------------------------------- /src/prismaModeller.ts: -------------------------------------------------------------------------------- 1 | import { Property as PrismaProperty, Schema as PrismaSchemaBlocks } from "@mrleebo/prisma-ast" 2 | 3 | interface Model { 4 | leadingComments: string 5 | properties: Map< 6 | string, 7 | { 8 | leadingComments: string 9 | property: PrismaProperty 10 | } 11 | > 12 | } 13 | 14 | export type PrismaMap = ReadonlyMap 15 | 16 | export const prismaModeller = (schema: PrismaSchemaBlocks) => { 17 | const types = new Map() 18 | 19 | let leadingComments: string[] = [] 20 | schema.list.forEach((b) => { 21 | if (b.type === "comment") { 22 | leadingComments.push(b.text.replace("/// ", "").replace("// ", "")) 23 | } 24 | 25 | if (b.type === "model") { 26 | const properties = new Map< 27 | string, 28 | { 29 | leadingComments: string 30 | property: PrismaProperty 31 | } 32 | >() 33 | 34 | let leadingFieldComments: string[] = [] 35 | // Loop through all the properties and keep track of the 36 | // comments before them 37 | b.properties.forEach((p) => { 38 | if (p.type === "comment") { 39 | leadingFieldComments.push(p.text.replace("/// ", "").replace("// ", "")) 40 | } else if (p.type === "break") { 41 | leadingFieldComments.push("") 42 | } else { 43 | properties.set(p.name, { 44 | leadingComments: leadingFieldComments.join("\n"), 45 | property: p, 46 | }) 47 | leadingFieldComments = [] 48 | } 49 | }) 50 | 51 | types.set(b.name, { 52 | properties, 53 | leadingComments: leadingComments.join("\n"), 54 | }) 55 | 56 | leadingComments = [] 57 | } 58 | }) 59 | 60 | return types 61 | } 62 | -------------------------------------------------------------------------------- /src/serviceFile.codefacts.ts: -------------------------------------------------------------------------------- 1 | import * as tsMorph from "ts-morph" 2 | 3 | import { AppContext } from "./context.js" 4 | import { CodeFacts, ModelResolverFacts, ResolverFuncFact } from "./typeFacts.js" 5 | import { varStartsWithUppercase } from "./utils.js" 6 | 7 | export const getCodeFactsForJSTSFileAtPath = (file: string, context: AppContext) => { 8 | const { pathSettings: settings } = context 9 | const fileKey = file.replace(settings.apiServicesPath, "") 10 | 11 | // const priorFacts = serviceInfo.get(fileKey) 12 | const fileFact: CodeFacts = {} 13 | 14 | const fileContents = context.sys.readFile(file) 15 | const referenceFileSourceFile = context.tsProject.createSourceFile(`/source/${fileKey}`, fileContents, { overwrite: true }) 16 | const vars = referenceFileSourceFile.getVariableDeclarations().filter((v) => v.isExported()) 17 | 18 | const resolverContainers = vars.filter(varStartsWithUppercase) 19 | 20 | const queryOrMutationResolvers = vars.filter((v) => !varStartsWithUppercase(v)) 21 | queryOrMutationResolvers.forEach((v) => { 22 | const parent = "maybe_query_mutation" 23 | const facts = getResolverInformationForDeclaration(v.getInitializer()) 24 | 25 | // Start making facts about the services 26 | const fact: ModelResolverFacts = fileFact[parent] ?? { 27 | typeName: parent, 28 | resolvers: new Map(), 29 | hasGenericArg: false, 30 | } 31 | fact.resolvers.set(v.getName(), { name: v.getName(), ...facts }) 32 | fileFact[parent] = fact 33 | }) 34 | 35 | // Next all the capital consts 36 | resolverContainers.forEach((c) => { 37 | addCustomTypeResolvers(c) 38 | }) 39 | 40 | return fileFact 41 | 42 | function addCustomTypeResolvers(variableDeclaration: tsMorph.VariableDeclaration) { 43 | const declarations = variableDeclaration.getVariableStatementOrThrow().getDeclarations() 44 | 45 | declarations.forEach((d) => { 46 | const name = d.getName() 47 | // only do it if the first letter is a capital 48 | if (!name.match(/^[A-Z]/)) return 49 | 50 | const type = d.getType() 51 | const hasGenericArg = type.getText().includes("<") 52 | 53 | // Start making facts about the services 54 | const fact: ModelResolverFacts = fileFact[name] ?? { 55 | typeName: name, 56 | resolvers: new Map(), 57 | hasGenericArg, 58 | } 59 | 60 | // Grab the const Thing = { ... } 61 | const obj = d.getFirstDescendantByKind(tsMorph.SyntaxKind.ObjectLiteralExpression) 62 | if (!obj) { 63 | throw new Error(`Could not find an object literal ( e.g. a { } ) in ${d.getName()}`) 64 | } 65 | 66 | obj.getProperties().forEach((p) => { 67 | if (p.isKind(tsMorph.SyntaxKind.SpreadAssignment)) { 68 | return 69 | } 70 | 71 | if (p.isKind(tsMorph.SyntaxKind.PropertyAssignment) && p.hasInitializer()) { 72 | const name = p.getName() 73 | fact.resolvers.set(name, { name, ...getResolverInformationForDeclaration(p.getInitializerOrThrow()) }) 74 | } 75 | 76 | if (p.isKind(tsMorph.SyntaxKind.FunctionDeclaration) && p.getName()) { 77 | const name = p.getName() 78 | // @ts-expect-error - lets let this go for now 79 | fact.resolvers.set(name, { name, ...getResolverInformationForDeclaration(p) }) 80 | } 81 | }) 82 | 83 | fileFact[d.getName()] = fact 84 | }) 85 | } 86 | } 87 | 88 | const getResolverInformationForDeclaration = (initialiser: tsMorph.Expression | undefined): Omit => { 89 | // Who knows what folks could do, lets not crash 90 | if (!initialiser) { 91 | return { 92 | funcArgCount: 0, 93 | isFunc: false, 94 | isAsync: false, 95 | isUnknown: true, 96 | isObjLiteral: false, 97 | } 98 | } 99 | 100 | // resolver is a fn 101 | if (initialiser.isKind(tsMorph.SyntaxKind.ArrowFunction) || initialiser.isKind(tsMorph.SyntaxKind.FunctionExpression)) { 102 | // Look to see if the 2nd param is just `{ root }` - which is a super common pattern in the Puzzmo codebase 103 | const params = initialiser.getParameters() 104 | let infoParamType: "all" | "just_root_destructured" = "all" 105 | 106 | if (params[1]?.getNameNode().isKind(tsMorph.SyntaxKind.ObjectBindingPattern)) { 107 | const extractsInParams = params[1].getNameNode().getChildrenOfKind(tsMorph.SyntaxKind.BindingElement) 108 | const extracted = extractsInParams.map((e) => e.getName()) 109 | if (extracted.length === 1 && extracted[0] === "root") { 110 | infoParamType = "just_root_destructured" 111 | } 112 | } 113 | 114 | let isObjLiteral = false 115 | if (initialiser.isKind(tsMorph.SyntaxKind.ArrowFunction)) { 116 | const isSingleLiner = initialiser.getStatements().length === 0 117 | if (isSingleLiner) isObjLiteral = isLiteral(initialiser.getBody()) 118 | } 119 | 120 | return { 121 | funcArgCount: params.length, 122 | isFunc: true, 123 | isAsync: initialiser.isAsync(), 124 | isUnknown: false, 125 | isObjLiteral, 126 | infoParamType, 127 | } 128 | } 129 | 130 | // resolver is a raw obj 131 | if (isLiteral(initialiser)) { 132 | return { 133 | funcArgCount: 0, 134 | isFunc: false, 135 | isAsync: false, 136 | isUnknown: false, 137 | isObjLiteral: true, 138 | } 139 | } 140 | 141 | // who knows 142 | return { 143 | funcArgCount: 0, 144 | isFunc: false, 145 | isAsync: false, 146 | isUnknown: true, 147 | isObjLiteral: false, 148 | } 149 | } 150 | 151 | const isLiteral = (node: tsMorph.Node) => 152 | node.isKind(tsMorph.SyntaxKind.ObjectLiteralExpression) || 153 | node.isKind(tsMorph.SyntaxKind.StringLiteral) || 154 | node.isKind(tsMorph.SyntaxKind.TemplateExpression) || 155 | node.isKind(tsMorph.SyntaxKind.NumericLiteral) || 156 | node.isKind(tsMorph.SyntaxKind.TrueKeyword) || 157 | node.isKind(tsMorph.SyntaxKind.FalseKeyword) || 158 | node.isKind(tsMorph.SyntaxKind.NullKeyword) || 159 | node.isKind(tsMorph.SyntaxKind.UndefinedKeyword) 160 | -------------------------------------------------------------------------------- /src/serviceFile.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unnecessary-condition */ 2 | 3 | import * as _t from "@babel/types" 4 | import * as graphql from "graphql" 5 | 6 | import { AppContext } from "./context.js" 7 | import { getCodeFactsForJSTSFileAtPath } from "./serviceFile.codefacts.js" 8 | import { builder, TSBuilder } from "./tsBuilder.js" 9 | import { CodeFacts, ModelResolverFacts, ResolverFuncFact } from "./typeFacts.js" 10 | import { TypeMapper, typeMapper } from "./typeMap.js" 11 | import { capitalizeFirstLetter, createAndReferOrInlineArgsForField, inlineArgsForField } from "./utils.js" 12 | 13 | const t = (_t as any).default || _t 14 | 15 | export const lookAtServiceFile = async (file: string, context: AppContext) => { 16 | const { gql, prisma, pathSettings: settings, codeFacts: serviceFacts, fieldFacts } = context 17 | 18 | if (!gql) throw new Error(`No schema when wanting to look at service file: ${file}`) 19 | if (!prisma) throw new Error(`No prisma schema when wanting to look at service file: ${file}`) 20 | 21 | // This isn't good enough, needs to be relative to api/src/services 22 | const fileKey = file.replace(settings.apiServicesPath, "") 23 | 24 | const thisFact: CodeFacts = {} 25 | 26 | const filename = context.basename(file) 27 | const dts = builder("", {}) 28 | 29 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 30 | const queryType = gql.getQueryType()! 31 | if (!queryType) throw new Error("No query type") 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 34 | const mutationType = gql.getMutationType()! 35 | if (!mutationType) throw new Error("No mutation type") 36 | 37 | const externalMapper = typeMapper(context, { preferPrismaModels: true }) 38 | const returnTypeMapper = typeMapper(context, {}) 39 | 40 | // The description of the source file 41 | const fileFacts = getCodeFactsForJSTSFileAtPath(file, context) 42 | if (Object.keys(fileFacts).length === 0) return 43 | 44 | // Tracks prospective prisma models which are used in the file 45 | const extraPrismaReferences = new Set() 46 | const extraSharedFileImportReferences = new Set<{ import: string; name?: string }>() 47 | 48 | // Basically if a top level resolver reference Query or Mutation 49 | const knownSpecialCasesForGraphQL = new Set() 50 | 51 | // Add the root function declarations 52 | const rootResolvers = fileFacts.maybe_query_mutation?.resolvers 53 | if (rootResolvers) 54 | rootResolvers.forEach((v) => { 55 | const isQuery = v.name in queryType.getFields() 56 | const isMutation = v.name in mutationType.getFields() 57 | const parentName = isQuery ? queryType.name : isMutation ? mutationType.name : undefined 58 | if (parentName) { 59 | addDefinitionsForTopLevelResolvers(parentName, v, dts) 60 | } else { 61 | // Add warning about unused resolver 62 | dts.rootScope.addInterface(v.name, [], { exported: true, docs: "This resolver does not exist on Query or Mutation" }) 63 | } 64 | }) 65 | 66 | // Add the root function declarations 67 | Object.values(fileFacts).forEach((model) => { 68 | if (!model) return 69 | const skip = ["maybe_query_mutation", queryType.name, mutationType.name] 70 | if (skip.includes(model.typeName)) return 71 | 72 | addCustomTypeModel(model) 73 | }) 74 | 75 | // Set up the module imports at the top 76 | const sharedGraphQLObjectsReferenced = externalMapper.getReferencedGraphQLThingsInMapping() 77 | const sharedGraphQLObjectsReferencedTypes = [...sharedGraphQLObjectsReferenced.types, ...knownSpecialCasesForGraphQL] 78 | const sharedInternalGraphQLObjectsReferenced = returnTypeMapper.getReferencedGraphQLThingsInMapping() 79 | 80 | const aliases = [...new Set([...sharedGraphQLObjectsReferenced.scalars, ...sharedInternalGraphQLObjectsReferenced.scalars])] 81 | 82 | for (const alias of aliases) { 83 | dts.rootScope.addTypeAlias(alias, "any") 84 | } 85 | 86 | const prismases = [ 87 | ...new Set([ 88 | ...sharedGraphQLObjectsReferenced.prisma, 89 | ...sharedInternalGraphQLObjectsReferenced.prisma, 90 | ...extraPrismaReferences.values(), 91 | ]), 92 | ] 93 | 94 | const validPrismaObjs = prismases.filter((p) => prisma.has(p)) 95 | if (validPrismaObjs.length) { 96 | dts.setImport("@prisma/client", { subImports: validPrismaObjs.map((p) => `${p} as P${p}`) }) 97 | } 98 | 99 | const initialResult = dts.getResult() 100 | if (initialResult.includes("GraphQLResolveInfo")) { 101 | dts.setImport("graphql", { subImports: ["GraphQLResolveInfo"] }) 102 | } 103 | 104 | if (initialResult.includes("RedwoodGraphQLContext")) { 105 | dts.setImport("@redwoodjs/graphql-server/dist/types", { subImports: ["RedwoodGraphQLContext"] }) 106 | } 107 | 108 | if (sharedInternalGraphQLObjectsReferenced.types.length || extraSharedFileImportReferences.size) { 109 | const source = `./${settings.sharedInternalFilename.replace(".d.ts", "")}` 110 | dts.setImport(source, { 111 | subImports: [ 112 | ...sharedInternalGraphQLObjectsReferenced.types.map((t) => `${t} as RT${t}`), 113 | ...[...extraSharedFileImportReferences.values()].map((t) => ("name" in t && t.name ? `${t.import} as ${t.name}` : t.import)), 114 | ], 115 | }) 116 | } 117 | 118 | if (sharedGraphQLObjectsReferencedTypes.length) { 119 | const source = `./${settings.sharedFilename.replace(".d.ts", "")}` 120 | dts.setImport(source, { subImports: sharedGraphQLObjectsReferencedTypes }) 121 | } 122 | 123 | serviceFacts.set(fileKey, thisFact) 124 | 125 | const dtsFilename = filename.endsWith(".ts") ? filename.replace(".ts", ".d.ts") : filename.replace(".js", ".d.ts") 126 | const dtsFilepath = context.join(context.pathSettings.typesFolderRoot, dtsFilename) 127 | 128 | // Some manual formatting tweaks so we align with Redwood's setup more 129 | const final = dts.getResult() 130 | 131 | const shouldWriteDTS = !!final.trim().length 132 | if (!shouldWriteDTS) return 133 | 134 | const formatted = final // await formatDTS(dtsFilepath, dts) 135 | 136 | // Don't make a file write if the content is the same 137 | const priorContent = context.sys.readFile(dtsFilename) 138 | if (priorContent === formatted) return 139 | 140 | context.sys.writeFile(dtsFilepath, formatted) 141 | return dtsFilepath 142 | 143 | function addDefinitionsForTopLevelResolvers(parentName: string, config: ResolverFuncFact, dts: TSBuilder) { 144 | const { name } = config 145 | let field = queryType.getFields()[name] 146 | if (!field) { 147 | field = mutationType.getFields()[name] 148 | } 149 | 150 | const nodeDocs = field.astNode 151 | ? ["SDL: " + graphql.print(field.astNode)] 152 | : ["@deprecated: Could not find this field in the schema for Mutation or Query"] 153 | const interfaceName = `${capitalizeFirstLetter(config.name)}Resolver` 154 | 155 | const args = createAndReferOrInlineArgsForField(field, { 156 | name: interfaceName, 157 | dts, 158 | mapper: externalMapper.map, 159 | }) 160 | 161 | if (parentName === queryType.name) knownSpecialCasesForGraphQL.add(queryType.name) 162 | if (parentName === mutationType.name) knownSpecialCasesForGraphQL.add(mutationType.name) 163 | 164 | const argsParam = args ?? "object" 165 | const qForInfos = config.infoParamType === "just_root_destructured" ? "?" : "" 166 | const returnType = returnTypeForResolver(returnTypeMapper, field, config) 167 | 168 | dts.rootScope.addInterface( 169 | interfaceName, 170 | [ 171 | { 172 | type: "call-signature", 173 | optional: config.funcArgCount < 1, 174 | returnType, 175 | params: [ 176 | { name: "args", type: argsParam, optional: config.funcArgCount < 1 }, 177 | { 178 | name: "obj", 179 | type: `{ root: ${parentName}, context${qForInfos}: RedwoodGraphQLContext, info${qForInfos}: GraphQLResolveInfo }`, 180 | optional: config.funcArgCount < 2, 181 | }, 182 | ], 183 | }, 184 | ], 185 | { 186 | exported: true, 187 | docs: nodeDocs.join(" "), 188 | } 189 | ) 190 | } 191 | 192 | /** Ideally, we want to be able to write the type for just the object */ 193 | function addCustomTypeModel(modelFacts: ModelResolverFacts) { 194 | const modelName = modelFacts.typeName 195 | extraPrismaReferences.add(modelName) 196 | 197 | // Make an interface, this is the version we are replacing from graphql-codegen: 198 | // Account: MergePrismaWithSdlTypes, AllMappedModels>; 199 | const gqlType = gql.getType(modelName) 200 | if (!gqlType) { 201 | // throw new Error(`Could not find a GraphQL type named ${d.getName()}`); 202 | // fileDTS.addStatements(`\n// ${modelName} does not exist in the schema`) 203 | return 204 | } 205 | 206 | if (!graphql.isObjectType(gqlType)) { 207 | throw new Error(`In your schema ${modelName} is not an object, which we can only make resolver types for`) 208 | } 209 | 210 | const fields = gqlType.getFields() 211 | 212 | // See: https://github.com/redwoodjs/redwood/pull/6228#issue-1342966511 213 | // For more ideas 214 | 215 | const hasGenerics = modelFacts.hasGenericArg 216 | 217 | const resolverInterface = dts.rootScope.addInterface(`${modelName}TypeResolvers`, [], { 218 | exported: true, 219 | generics: hasGenerics ? [{ name: "Extended" }] : [], 220 | }) 221 | 222 | // Handle extending classes in the runtime which only exist in SDL 223 | const parentIsPrisma = prisma.has(modelName) 224 | if (!parentIsPrisma) extraSharedFileImportReferences.add({ name: `S${modelName}`, import: modelName }) 225 | const suffix = parentIsPrisma ? "P" : "S" 226 | 227 | const parentTypeString = `${suffix}${modelName} ${createParentAdditionallyDefinedFunctions()} ${hasGenerics ? " & Extended" : ""}` 228 | 229 | /** 230 | type CurrentUserAccountAsParent = SCurrentUserAccount & { 231 | users: () => PUser[] | Promise | (() => Promise); 232 | registeredPublishingPartner: () => Promise; 233 | subIsViaGift: () => boolean | Promise | (() => Promise); 234 | } 235 | */ 236 | 237 | dts.rootScope.addTypeAlias(`${modelName}AsParent`, t.tsTypeReference(t.identifier(parentTypeString)), { 238 | generics: hasGenerics ? [{ name: "Extended" }] : [], 239 | }) 240 | const modelFieldFacts = fieldFacts.get(modelName) ?? {} 241 | 242 | // Loop through the resolvers, adding the fields which have resolvers implemented in the source file 243 | modelFacts.resolvers.forEach((resolver) => { 244 | const field = fields[resolver.name] 245 | if (field) { 246 | const fieldName = resolver.name 247 | if (modelFieldFacts[fieldName]) modelFieldFacts[fieldName].hasResolverImplementation = true 248 | else modelFieldFacts[fieldName] = { hasResolverImplementation: true } 249 | 250 | const argsType = inlineArgsForField(field, { mapper: externalMapper.map }) ?? "undefined" 251 | const param = hasGenerics ? "" : "" 252 | 253 | const firstQ = resolver.funcArgCount < 1 ? "?" : "" 254 | const secondQ = resolver.funcArgCount < 2 ? "?" : "" 255 | const qForInfos = resolver.infoParamType === "just_root_destructured" ? "?" : "" 256 | 257 | const innerArgs = `args${firstQ}: ${argsType}, obj${secondQ}: { root: ${modelName}AsParent${param}, context${qForInfos}: RedwoodGraphQLContext, info${qForInfos}: GraphQLResolveInfo }` 258 | const returnType = returnTypeForResolver(returnTypeMapper, field, resolver) 259 | const args = resolver.isFunc || resolver.isUnknown ? `(${innerArgs}) => ${returnType ?? "any"}` : returnType 260 | 261 | const docs = field.astNode ? `SDL: ${graphql.print(field.astNode)}` : "" 262 | const property = t.tsPropertySignature(t.identifier(fieldName), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(args)))) 263 | t.addComment(property, "leading", " " + docs) 264 | 265 | resolverInterface.body.body.push(property) 266 | } else { 267 | resolverInterface.body.body.push( 268 | t.tsPropertySignature(t.identifier(resolver.name), t.tsTypeAnnotation(t.tsTypeReference(t.identifier("void")))) 269 | ) 270 | } 271 | }) 272 | 273 | function createParentAdditionallyDefinedFunctions() { 274 | const fns: string[] = [] 275 | modelFacts.resolvers.forEach((resolver) => { 276 | const existsInGraphQLSchema = fields[resolver.name] 277 | if (!existsInGraphQLSchema) { 278 | console.warn( 279 | `The service file ${filename} has a field ${resolver.name} on ${modelName} that does not exist in the generated schema.graphql` 280 | ) 281 | } 282 | 283 | const prefix = !existsInGraphQLSchema ? "\n// This field does not exist in the generated schema.graphql\n" : "" 284 | const returnType = returnTypeForResolver(externalMapper, existsInGraphQLSchema, resolver) 285 | // fns.push(`${prefix}${resolver.name}: () => Promise<${externalMapper.map(type, {})}>`) 286 | fns.push(`${prefix}${resolver.name}: () => ${returnType}`) 287 | }) 288 | 289 | if (fns.length < 1) return "" 290 | return "& {" + fns.join(", \n") + "}" 291 | } 292 | 293 | fieldFacts.set(modelName, modelFieldFacts) 294 | } 295 | } 296 | 297 | function returnTypeForResolver(mapper: TypeMapper, field: graphql.GraphQLField | undefined, resolver: ResolverFuncFact) { 298 | if (!field) return "void" 299 | 300 | const tType = mapper.map(field.type, { preferNullOverUndefined: true, typenamePrefix: "RT" }) ?? "void" 301 | 302 | let returnType = tType 303 | const all = `${tType} | Promise<${tType}> | (() => Promise<${tType}>)` 304 | 305 | if (resolver.isFunc && resolver.isAsync) returnType = `Promise<${tType}>` 306 | else if (resolver.isFunc && resolver.isObjLiteral) returnType = tType 307 | else if (resolver.isFunc) returnType = all 308 | else if (resolver.isObjLiteral) returnType = tType 309 | else if (resolver.isUnknown) returnType = all 310 | 311 | return returnType 312 | } 313 | /* eslint-enable @typescript-eslint/no-unnecessary-condition */ 314 | -------------------------------------------------------------------------------- /src/sharedSchema.ts: -------------------------------------------------------------------------------- 1 | /// The main schema for objects and inputs 2 | 3 | import * as _t from "@babel/types" 4 | 5 | import * as graphql from "graphql" 6 | 7 | import { AppContext } from "./context.js" 8 | import { builder } from "./tsBuilder.js" 9 | import { typeMapper } from "./typeMap.js" 10 | import { makeStep } from "./utils.js" 11 | 12 | const t = (_t as any).default || _t 13 | 14 | export const createSharedSchemaFiles = async (context: AppContext, verbose: boolean) => { 15 | const step = makeStep(verbose) 16 | await step("Creating shared schema files", () => createSharedExternalSchemaFile(context)) 17 | await step("Creating shared return position schema files", () => createSharedReturnPositionSchemaFile(context)) 18 | 19 | return [ 20 | context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename), 21 | context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedInternalFilename), 22 | ] 23 | } 24 | 25 | function createSharedExternalSchemaFile(context: AppContext) { 26 | const gql = context.gql 27 | const types = gql.getTypeMap() 28 | const knownPrimitives = ["String", "Boolean", "Int"] 29 | 30 | const { prisma, fieldFacts } = context 31 | const mapper = typeMapper(context, {}) 32 | 33 | const priorFile = "" 34 | const dts = builder(priorFile, {}) 35 | 36 | Object.keys(types).forEach((name) => { 37 | if (name.startsWith("__")) { 38 | return 39 | } 40 | 41 | if (knownPrimitives.includes(name)) { 42 | return 43 | } 44 | 45 | const type = types[name] 46 | const pType = prisma.get(name) 47 | 48 | if (graphql.isObjectType(type) || graphql.isInterfaceType(type) || graphql.isInputObjectType(type)) { 49 | // This is slower than it could be, use the add many at once api 50 | const docs = [] 51 | if (pType?.leadingComments) { 52 | docs.push(pType.leadingComments) 53 | } 54 | 55 | if (type.description) { 56 | docs.push(type.description) 57 | } 58 | 59 | dts.rootScope.addInterface( 60 | type.name, 61 | [ 62 | { 63 | name: "__typename", 64 | type: `"${type.name}"`, 65 | optional: true, 66 | }, 67 | ...Object.entries(type.getFields()).map(([fieldName, obj]: [string, graphql.GraphQLField]) => { 68 | const prismaField = pType?.properties.get(fieldName) 69 | const type = obj.type as graphql.GraphQLType 70 | 71 | // if (obj.description) docs.push(obj.description); 72 | const hasResolverImplementation = fieldFacts.get(name)?.[fieldName]?.hasResolverImplementation 73 | const isOptionalInSDL = !graphql.isNonNullType(type) 74 | const doesNotExistInPrisma = false // !prismaField; 75 | 76 | const field = { 77 | name: fieldName, 78 | type: mapper.map(type, { preferNullOverUndefined: true })!, 79 | docs: prismaField?.leadingComments.trim(), 80 | optional: hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma), 81 | } 82 | return field 83 | }), 84 | ], 85 | { exported: true, docs: docs.join(" ") } 86 | ) 87 | } 88 | 89 | if (graphql.isEnumType(type)) { 90 | const union = 91 | '"' + 92 | type 93 | .getValues() 94 | .map((m) => (m as { value: string }).value) 95 | .join('" | "') + 96 | '"' 97 | dts.rootScope.addTypeAlias(type.name, t.tsTypeReference(t.identifier(union)), { exported: true }) 98 | } 99 | 100 | if (graphql.isUnionType(type)) { 101 | const union = type 102 | .getTypes() 103 | .map((m) => m.name) 104 | .join(" | ") 105 | dts.rootScope.addTypeAlias(type.name, t.tsTypeReference(t.identifier(union)), { exported: true }) 106 | } 107 | }) 108 | 109 | const { scalars } = mapper.getReferencedGraphQLThingsInMapping() 110 | for (const s of scalars) { 111 | dts.rootScope.addTypeAlias(s, t.tsAnyKeyword()) 112 | } 113 | 114 | const text = dts.getResult() 115 | const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedFilename) 116 | const prior = context.sys.readFile(fullPath) 117 | if (prior !== text) context.sys.writeFile(fullPath, text) 118 | } 119 | 120 | function createSharedReturnPositionSchemaFile(context: AppContext) { 121 | const { gql, prisma, fieldFacts } = context 122 | const types = gql.getTypeMap() 123 | const mapper = typeMapper(context, { preferPrismaModels: true }) 124 | 125 | const typesToImport = [] as string[] 126 | const knownPrimitives = ["String", "Boolean", "Int"] 127 | 128 | const dts = builder("", {}) 129 | 130 | dts.rootScope.addLeadingComment(`// You may very reasonably ask yourself, 'what is this file?' and why do I need it. 131 | 132 | // Roughly, this file ensures that when a resolver wants to return a type - that 133 | // type will match a prisma model. This is useful because you can trivially extend 134 | // the type in the SDL and not have to worry about type mis-matches because the thing 135 | // you returned does not include those functions. 136 | 137 | // This gets particularly valuable when you want to return a union type, an interface, 138 | // or a model where the prisma model is nested pretty deeply (GraphQL connections, for example.) 139 | `) 140 | 141 | Object.keys(types).forEach((name) => { 142 | if (name.startsWith("__")) { 143 | return 144 | } 145 | 146 | if (knownPrimitives.includes(name)) { 147 | return 148 | } 149 | 150 | const type = types[name] 151 | const pType = prisma.get(name) 152 | 153 | if (graphql.isObjectType(type) || graphql.isInterfaceType(type) || graphql.isInputObjectType(type)) { 154 | // Return straight away if we have a matching type in the prisma schema 155 | // as we dont need it 156 | if (pType) { 157 | typesToImport.push(name) 158 | return 159 | } 160 | 161 | dts.rootScope.addInterface( 162 | type.name, 163 | [ 164 | { 165 | name: "__typename", 166 | type: `"${type.name}"`, 167 | optional: true, 168 | }, 169 | ...Object.entries(type.getFields()).map(([fieldName, obj]: [string, graphql.GraphQLField]) => { 170 | const hasResolverImplementation = fieldFacts.get(name)?.[fieldName]?.hasResolverImplementation 171 | const isOptionalInSDL = !graphql.isNonNullType(obj.type) 172 | const doesNotExistInPrisma = false // !prismaField; 173 | 174 | const field = { 175 | name: fieldName, 176 | type: mapper.map(obj.type, { preferNullOverUndefined: true })!, 177 | optional: hasResolverImplementation ?? (isOptionalInSDL || doesNotExistInPrisma), 178 | } 179 | return field 180 | }), 181 | ], 182 | { exported: true } 183 | ) 184 | } 185 | 186 | if (graphql.isEnumType(type)) { 187 | const union = 188 | '"' + 189 | type 190 | .getValues() 191 | .map((m) => (m as { value: string }).value) 192 | .join('" | "') + 193 | '"' 194 | dts.rootScope.addTypeAlias(type.name, t.tsTypeReference(t.identifier(union)), { exported: true }) 195 | } 196 | 197 | if (graphql.isUnionType(type)) { 198 | const union = type 199 | .getTypes() 200 | .map((m) => m.name) 201 | .join(" | ") 202 | dts.rootScope.addTypeAlias(type.name, t.tsTypeReference(t.identifier(union)), { exported: true }) 203 | } 204 | }) 205 | 206 | const { scalars, prisma: prismaModels } = mapper.getReferencedGraphQLThingsInMapping() 207 | for (const s of scalars) { 208 | dts.rootScope.addTypeAlias(s, t.tsAnyKeyword()) 209 | } 210 | 211 | const allPrismaModels = [...new Set([...prismaModels, ...typesToImport])].sort() 212 | if (allPrismaModels.length) { 213 | dts.setImport("@prisma/client", { subImports: allPrismaModels.map((p) => `${p} as P${p}`) }) 214 | 215 | for (const p of allPrismaModels) { 216 | dts.rootScope.addTypeAlias(p, t.tsTypeReference(t.identifier(`P${p}`))) 217 | } 218 | } 219 | 220 | const text = dts.getResult() 221 | const fullPath = context.join(context.pathSettings.typesFolderRoot, context.pathSettings.sharedInternalFilename) 222 | const prior = context.sys.readFile(fullPath) 223 | if (prior !== text) context.sys.writeFile(fullPath, text) 224 | } 225 | -------------------------------------------------------------------------------- /src/tests/bugs/parentCanBeGraphQLObject.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest" 2 | 3 | import { getDTSFilesForRun, graphql, prisma } from "../testRunner.js" 4 | 5 | it("Uses GraphQL objects when prisma objects are not available for resolver parents", async () => { 6 | const prismaSchema = prisma` 7 | model Game { 8 | id Int @id @default(autoincrement()) 9 | } 10 | ` 11 | 12 | const sdl = graphql` 13 | type Game { 14 | id: Int! 15 | } 16 | 17 | type Puzzle { 18 | id: Int! 19 | } 20 | ` 21 | 22 | const gamesService = ` 23 | import { db } from "src/lib/db"; 24 | 25 | export const Puzzle = { 26 | id: "", 27 | }; 28 | ` 29 | 30 | const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService, prismaSchema }) 31 | const dts = vfsMap.get("/types/games.d.ts")! 32 | expect(dts.trim()).toMatchInlineSnapshot(` 33 | "export interface PuzzleTypeResolvers { 34 | /* SDL: id: Int!*/ 35 | id: number; 36 | } 37 | type PuzzleAsParent = SPuzzle & {id: () => number} ; 38 | import { Puzzle as SPuzzle } from \\"./shared-return-types\\";" 39 | `) 40 | }) 41 | -------------------------------------------------------------------------------- /src/tests/bugs/returnObjectCanBeGraphQLInterfaces.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest" 2 | 3 | import { getDTSFilesForRun, graphql, prisma } from "../testRunner.js" 4 | 5 | it("The retunr type can be a graphql interface", async () => { 6 | const prismaSchema = prisma` 7 | model Game { 8 | id Int @id @default(autoincrement()) 9 | } 10 | ` 11 | 12 | const sdl = graphql` 13 | type Game { 14 | id: Int! 15 | puzzle: Node! 16 | } 17 | 18 | interface Node { 19 | id: ID! 20 | } 21 | ` 22 | 23 | const gamesService = ` 24 | import { db } from "src/lib/db"; 25 | 26 | export const Game = { 27 | puzzle: () => {} 28 | }; 29 | ` 30 | 31 | const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService, prismaSchema }) 32 | const dts = vfsMap.get("/types/games.d.ts")! 33 | expect(dts.trim()).toMatchInlineSnapshot(` 34 | "export interface GameTypeResolvers { 35 | /* SDL: puzzle: Node!*/ 36 | puzzle: (args?: undefined, obj?: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => RTNode | Promise | (() => Promise); 37 | } 38 | type GameAsParent = PGame & {puzzle: () => RTNode | Promise | (() => Promise)} ; 39 | import { Game as PGame } from \\"@prisma/client\\"; 40 | import { GraphQLResolveInfo } from \\"graphql\\"; 41 | import { RedwoodGraphQLContext } from \\"@redwoodjs/graphql-server/dist/types\\"; 42 | import { Node as RTNode } from \\"./shared-return-types\\"; 43 | import { Node } from \\"./shared-schema-types\\";" 44 | `) 45 | }) 46 | -------------------------------------------------------------------------------- /src/tests/features/generatesTypesForUnions.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest" 2 | 3 | import { getDTSFilesForRun, graphql, prisma } from "../testRunner.js" 4 | 5 | it("generates a union type for a gql union", async () => { 6 | const prismaSchema = prisma` 7 | model Game { 8 | id Int @id @default(autoincrement()) 9 | } 10 | ` 11 | 12 | const sdl = graphql` 13 | type Game { 14 | id: Int! 15 | } 16 | type Puzzle { 17 | id: Int! 18 | } 19 | 20 | union Gameish = Game | Puzzle 21 | 22 | type Query { 23 | gameObj: Gameish 24 | gameArr: [Gameish!]! 25 | } 26 | ` 27 | 28 | const gamesService = ` 29 | import { db } from "src/lib/db"; 30 | 31 | export const gameObj = {} 32 | 33 | export const Game = { 34 | id: "", 35 | }; 36 | ` 37 | 38 | const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService, prismaSchema, generateShared: true }) 39 | const dts = vfsMap.get("/types/shared-schema-types.d.ts")! 40 | expect(dts.trim()).toMatchInlineSnapshot(` 41 | "export interface Game { 42 | __typename?: \\"Game\\"; 43 | id?: number; 44 | } 45 | export interface Puzzle { 46 | __typename?: \\"Puzzle\\"; 47 | id: number; 48 | } 49 | export type Gameish = Game | Puzzle; 50 | export interface Query { 51 | __typename?: \\"Query\\"; 52 | gameObj?: Game| null | Puzzle| null| null; 53 | gameArr: (Game | Puzzle)[]; 54 | } 55 | export interface Mutation { 56 | __typename?: \\"Mutation\\"; 57 | __?: string| null; 58 | }" 59 | `) 60 | }) 61 | -------------------------------------------------------------------------------- /src/tests/features/preferPromiseFnWhenKnown.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest" 2 | 3 | import { getDTSFilesForRun, graphql, prisma } from "../testRunner.js" 4 | 5 | it("uses a rn to promise when we see an async tag", async () => { 6 | const prismaSchema = prisma` 7 | model Game { 8 | id Int @id @default(autoincrement()) 9 | homeTeamID Int 10 | awayTeamID Int 11 | } 12 | ` 13 | 14 | const sdl = graphql` 15 | type Game { 16 | id: Int! 17 | homeTeamID: Int! 18 | awayTeamID: Int! 19 | 20 | summarySync: String! 21 | summarySyncBlock: String! 22 | summaryAsync: String! 23 | summary: String! 24 | } 25 | 26 | type Query { 27 | gameObj: Game 28 | gameSync: Game 29 | gameAsync: Game 30 | gameAsync1Arg: Game 31 | gameAsync2Arg: Game 32 | } 33 | ` 34 | 35 | const gamesService = ` 36 | import { db } from "src/lib/db"; 37 | 38 | export const gameSync = () => {} 39 | export const gameAsync = async () => {} 40 | export const gameAsync1Arg = (arg) => {} 41 | export const gameAsync2Arg = (arg, obj) => {} 42 | export const gameObj = {} 43 | 44 | export const Game = { 45 | summary: "", 46 | summarySync: () => "", 47 | summarySyncBlock: () => { 48 | return "" 49 | }, 50 | summaryAsync: async () => "" 51 | }; 52 | ` 53 | 54 | const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService, prismaSchema }) 55 | const dts = vfsMap.get("/types/games.d.ts")! 56 | expect(dts.trim()).toMatchInlineSnapshot(` 57 | "/*SDL: gameSync: Game*/ 58 | export interface GameSyncResolver { 59 | (args?: object, obj?: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame| null | Promise | (() => Promise); 60 | } 61 | /*SDL: gameAsync: Game*/ 62 | export interface GameAsyncResolver { 63 | (args?: object, obj?: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): Promise; 64 | } 65 | /*SDL: gameAsync1Arg: Game*/ 66 | export interface GameAsync1ArgResolver { 67 | (args: object, obj?: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame| null | Promise | (() => Promise); 68 | } 69 | /*SDL: gameAsync2Arg: Game*/ 70 | export interface GameAsync2ArgResolver { 71 | (args: object, obj: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame| null | Promise | (() => Promise); 72 | } 73 | /*SDL: gameObj: Game*/ 74 | export interface GameObjResolver { 75 | (args?: object, obj?: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame| null; 76 | } 77 | export interface GameTypeResolvers { 78 | /* SDL: summary: String!*/ 79 | summary: string; 80 | /* SDL: summarySync: String!*/ 81 | summarySync: (args?: undefined, obj?: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string; 82 | /* SDL: summarySyncBlock: String!*/ 83 | summarySyncBlock: (args?: undefined, obj?: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | Promise | (() => Promise); 84 | /* SDL: summaryAsync: String!*/ 85 | summaryAsync: (args?: undefined, obj?: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => Promise; 86 | } 87 | type GameAsParent = PGame & {summary: () => string, 88 | summarySync: () => string, 89 | summarySyncBlock: () => string | Promise | (() => Promise), 90 | summaryAsync: () => Promise} & Extended; 91 | import { Game as PGame } from \\"@prisma/client\\"; 92 | import { GraphQLResolveInfo } from \\"graphql\\"; 93 | import { RedwoodGraphQLContext } from \\"@redwoodjs/graphql-server/dist/types\\"; 94 | import { Game as RTGame } from \\"./shared-return-types\\"; 95 | import { Query } from \\"./shared-schema-types\\";" 96 | `) 97 | }) 98 | -------------------------------------------------------------------------------- /src/tests/features/returnTypePositionsWhichPreferPrisma.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest" 2 | 3 | import { getDTSFilesForRun, graphql, prisma } from "../testRunner.js" 4 | 5 | it("supports a return position where a prisma object can be given, if the extra fn are defined as resolvers", async () => { 6 | const prismaSchema = prisma` 7 | model Game { 8 | id Int @id @default(autoincrement()) 9 | homeTeamID Int 10 | awayTeamID Int 11 | } 12 | ` 13 | 14 | const sdl = graphql` 15 | type Game { 16 | id: Int! 17 | homeTeamID: Int! 18 | awayTeamID: Int! 19 | 20 | # This is new, and _not_ on the prisma model 21 | summary: String! 22 | } 23 | 24 | type Query { 25 | game: Game 26 | } 27 | ` 28 | 29 | const services = ` 30 | import { db } from "src/lib/db"; 31 | 32 | export const game = () => {} 33 | 34 | export const Game = { 35 | summary: (_obj, { root }) => "" 36 | }; 37 | ` 38 | 39 | const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService: services, prismaSchema }) 40 | const dts = vfsMap.get("/types/games.d.ts")! 41 | 42 | expect(dts.trimStart()).toMatchInlineSnapshot( 43 | ` 44 | "/*SDL: game: Game*/ 45 | export interface GameResolver { 46 | (args?: object, obj?: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame| null | Promise | (() => Promise); 47 | } 48 | export interface GameTypeResolvers { 49 | /* SDL: summary: String!*/ 50 | summary: (args: undefined, obj: { root: GameAsParent, context?: RedwoodGraphQLContext, info?: GraphQLResolveInfo }) => string; 51 | } 52 | type GameAsParent = PGame & {summary: () => string} ; 53 | import { Game as PGame } from \\"@prisma/client\\"; 54 | import { GraphQLResolveInfo } from \\"graphql\\"; 55 | import { RedwoodGraphQLContext } from \\"@redwoodjs/graphql-server/dist/types\\"; 56 | import { Game as RTGame } from \\"./shared-return-types\\"; 57 | import { Query } from \\"./shared-schema-types\\";" 58 | ` 59 | ) 60 | }) 61 | -------------------------------------------------------------------------------- /src/tests/features/supportGenericExtension.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest" 2 | 3 | import { getDTSFilesForRun, graphql, prisma } from "../testRunner.js" 4 | 5 | it("It allows you to add a generic parameter", async () => { 6 | const prismaSchema = prisma` 7 | model Game { 8 | id Int @id @default(autoincrement()) 9 | homeTeamID Int 10 | awayTeamID Int 11 | } 12 | ` 13 | 14 | const sdl = graphql` 15 | type Game { 16 | id: Int! 17 | homeTeamId: Int! 18 | awayTeamId: Int! 19 | } 20 | ` 21 | 22 | const services = ` 23 | import { db } from "src/lib/db"; 24 | 25 | export const Game: GameResolvers<{ type: string }> = {}; 26 | ` 27 | 28 | const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService: services, prismaSchema }) 29 | 30 | expect(vfsMap.get("/types/games.d.ts")!).toContain("interface GameTypeResolvers") 31 | 32 | expect(vfsMap.get("/types/games.d.ts")!).toContain("GameAsParent = PGame & Extended") 33 | 34 | expect(vfsMap.get("/types/games.d.ts"))!.toMatchInlineSnapshot(` 35 | "export interface GameTypeResolvers {} 36 | type GameAsParent = PGame & Extended; 37 | import { Game as PGame } from \\"@prisma/client\\";" 38 | `) 39 | }) 40 | -------------------------------------------------------------------------------- /src/tests/features/supportReferringToEnumsOnlyInSDL.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest" 2 | 3 | import { getDTSFilesForRun, graphql, prisma } from "../testRunner.js" 4 | 5 | it("It adds a reference to the graphql enums you use", async () => { 6 | const prismaSchema = prisma` 7 | model Game { 8 | id Int @id @default(autoincrement()) 9 | } 10 | ` 11 | 12 | const sdl = graphql` 13 | type Game { 14 | id: Int! 15 | games(type: GameType!): [Game!]! 16 | } 17 | 18 | type Query { 19 | allGames(type: GameType!): [Game!]! 20 | } 21 | 22 | enum GameType { 23 | FOOTBALL 24 | BASKETBALL 25 | } 26 | ` 27 | 28 | const services = ` 29 | import { db } from "src/lib/db"; 30 | 31 | export const allGames = () => {} 32 | 33 | export const Game: GameResolvers = {}; 34 | ` 35 | 36 | const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService: services, prismaSchema, generateShared: true }) 37 | 38 | // We are expecting to see import type GameType from "./shared-schema-types" 39 | 40 | expect(vfsMap.get("/types/games.d.ts")).toMatchInlineSnapshot(` 41 | "/*SDL: allGames(type: GameType!): [Game!]!*/ 42 | export interface AllGamesResolver { 43 | (args?: {type: GameType}, obj?: { root: Query, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame[] | Promise | (() => Promise); 44 | } 45 | export interface GameTypeResolvers {} 46 | type GameAsParent = PGame ; 47 | import { Game as PGame } from \\"@prisma/client\\"; 48 | import { GraphQLResolveInfo } from \\"graphql\\"; 49 | import { RedwoodGraphQLContext } from \\"@redwoodjs/graphql-server/dist/types\\"; 50 | import { Game as RTGame } from \\"./shared-return-types\\"; 51 | import { GameType, Query } from \\"./shared-schema-types\\";" 52 | `) 53 | 54 | expect(vfsMap.get("/types/shared-schema-types.d.ts"))!.toMatchInlineSnapshot(` 55 | "export interface Game { 56 | __typename?: \\"Game\\"; 57 | id: number; 58 | games: Game[]; 59 | } 60 | export interface Query { 61 | __typename?: \\"Query\\"; 62 | allGames: Game[]; 63 | } 64 | export type GameType = \\"FOOTBALL\\" | \\"BASKETBALL\\"; 65 | export interface Mutation { 66 | __typename?: \\"Mutation\\"; 67 | __?: string| null; 68 | }" 69 | `) 70 | }) 71 | -------------------------------------------------------------------------------- /src/tests/features/warnOnSuperfluousResolvers.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest" 2 | 3 | import { getDTSFilesForRun, graphql, prisma } from "../testRunner.js" 4 | 5 | it("It prints a warning, and doesn't crash when you have resolvers which exist but are not on the parent", async () => { 6 | const prismaSchema = prisma` 7 | model Game { 8 | id Int @id @default(autoincrement()) 9 | homeTeamID Int 10 | awayTeamID Int 11 | } 12 | ` 13 | 14 | const sdl = graphql` 15 | type Game { 16 | id: Int! 17 | homeTeamId: Int! 18 | awayTeamId: Int! 19 | } 20 | ` 21 | 22 | const services = ` 23 | import { db } from "src/lib/db"; 24 | 25 | export const Game: GameResolvers = { 26 | someRandomThing: () => "hello" 27 | }; 28 | 29 | ` 30 | 31 | const { vfsMap } = await getDTSFilesForRun({ sdl, gamesService: services, prismaSchema }) 32 | 33 | expect(vfsMap.get("/types/games.d.ts")!).toContain("// This field does not exist in the generated schema.graphql\n") 34 | }) 35 | -------------------------------------------------------------------------------- /src/tests/integration.puzzmo.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "node:fs" 2 | import { join, resolve } from "node:path" 3 | 4 | import { createSystem } from "@typescript/vfs" 5 | import { describe, expect, it } from "vitest" 6 | 7 | import { runFullCodegen } from "../index" 8 | 9 | it("Passes", () => expect(true).toBe(true)) 10 | 11 | const hasAccessToPuzzmo = existsSync("../app/package.json") 12 | const desc = hasAccessToPuzzmo ? describe : describe.skip 13 | 14 | desc("Puzzmo", () => { 15 | it("Runs the entire puzzmo codebase fast", async () => { 16 | const puzzmoAPIWD = resolve(process.cwd() + "/..../../../app/apps/api.puzzmo.com") 17 | const vfsMap = new Map() 18 | const vfs = createSystem(vfsMap) 19 | 20 | // Replicates a Redwood project config object 21 | const paths = { 22 | base: puzzmoAPIWD, 23 | api: { 24 | base: puzzmoAPIWD, 25 | config: "-", 26 | dbSchema: join(puzzmoAPIWD, "prisma", "schema.prisma"), 27 | directives: join(puzzmoAPIWD, "src", "directives"), 28 | graphql: join(puzzmoAPIWD, "src", "functions", "graphql.ts"), 29 | lib: join(puzzmoAPIWD, "src", "lib"), 30 | models: "-", 31 | services: join(puzzmoAPIWD, "src", "services"), 32 | src: join(puzzmoAPIWD, "src"), 33 | types: join(puzzmoAPIWD, "types"), 34 | }, 35 | generated: { 36 | schema: join(puzzmoAPIWD, "..", "..", "api-schema.graphql"), 37 | }, 38 | web: {}, 39 | scripts: "-", 40 | } 41 | 42 | await runFullCodegen("redwood", { paths, verbose: true, sys: vfs }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/tests/serviceFile.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs" 2 | import { expect, it } from "vitest" 3 | 4 | import { getCodeFactsForJSTSFileAtPath } from "../serviceFile.codefacts.js" 5 | import { lookAtServiceFile } from "../serviceFile.js" 6 | import { getDTSFilesForRun } from "./testRunner.js" 7 | 8 | it("reads a service file", async () => { 9 | const { appContext, vfsMap } = await getDTSFilesForRun({}) 10 | 11 | vfsMap.set( 12 | "/api/src/services/example.ts", 13 | ` 14 | export const game = () => {} 15 | export function game2() {} 16 | ` 17 | ) 18 | 19 | expect(vfsMap.has("/types/example.d.ts")).toBeFalsy() 20 | await lookAtServiceFile("/api/src/services/example.ts", appContext) 21 | 22 | // this isn't really very useful as a test, but it proves it doesn't crash? 23 | }) 24 | 25 | it("generates useful service facts from a (truncated) real file", async () => { 26 | const { appContext, vfsMap } = await getDTSFilesForRun({}) 27 | 28 | vfsMap.set("/api/src/services/userProfile.ts", readFileSync("./src/tests/vendor/puzzmo/one-offs/userProfiles.ts", "utf8")) 29 | 30 | const facts = getCodeFactsForJSTSFileAtPath("/api/src/services/userProfile.ts", appContext) 31 | expect(facts).toMatchInlineSnapshot(` 32 | { 33 | "UserProfile": { 34 | "hasGenericArg": false, 35 | "resolvers": Map { 36 | "id" => { 37 | "funcArgCount": 2, 38 | "infoParamType": "just_root_destructured", 39 | "isAsync": false, 40 | "isFunc": true, 41 | "isObjLiteral": false, 42 | "isUnknown": false, 43 | "name": "id", 44 | }, 45 | "user" => { 46 | "funcArgCount": 2, 47 | "infoParamType": "just_root_destructured", 48 | "isAsync": false, 49 | "isFunc": true, 50 | "isObjLiteral": false, 51 | "isUnknown": false, 52 | "name": "user", 53 | }, 54 | }, 55 | "typeName": "UserProfile", 56 | }, 57 | "maybe_query_mutation": { 58 | "hasGenericArg": false, 59 | "resolvers": Map { 60 | "updateUserProfile" => { 61 | "funcArgCount": 1, 62 | "infoParamType": "all", 63 | "isAsync": false, 64 | "isFunc": true, 65 | "isObjLiteral": false, 66 | "isUnknown": false, 67 | "name": "updateUserProfile", 68 | }, 69 | "addLeaderboardToUserProfile" => { 70 | "funcArgCount": 1, 71 | "infoParamType": "all", 72 | "isAsync": true, 73 | "isFunc": true, 74 | "isObjLiteral": false, 75 | "isUnknown": false, 76 | "name": "addLeaderboardToUserProfile", 77 | }, 78 | "removeLeaderboardFromUserProfile" => { 79 | "funcArgCount": 1, 80 | "infoParamType": "all", 81 | "isAsync": true, 82 | "isFunc": true, 83 | "isObjLiteral": false, 84 | "isUnknown": false, 85 | "name": "removeLeaderboardFromUserProfile", 86 | }, 87 | "deleteUserProfile" => { 88 | "funcArgCount": 1, 89 | "infoParamType": "all", 90 | "isAsync": false, 91 | "isFunc": true, 92 | "isObjLiteral": false, 93 | "isUnknown": false, 94 | "name": "deleteUserProfile", 95 | }, 96 | }, 97 | "typeName": "maybe_query_mutation", 98 | }, 99 | } 100 | `) 101 | }) 102 | -------------------------------------------------------------------------------- /src/tests/testRunner.ts: -------------------------------------------------------------------------------- 1 | import { getSchema as getPrismaSchema } from "@mrleebo/prisma-ast" 2 | import { createSystem } from "@typescript/vfs" 3 | import { buildSchema } from "graphql" 4 | import { basename, join } from "path" 5 | import { Project } from "ts-morph" 6 | 7 | import { AppContext } from "../context.js" 8 | import { prismaModeller } from "../prismaModeller.js" 9 | import { lookAtServiceFile } from "../serviceFile.js" 10 | import { createSharedSchemaFiles } from "../sharedSchema.js" 11 | import type { CodeFacts, FieldFacts } from "../typeFacts.js" 12 | 13 | interface Run { 14 | gamesService?: string 15 | generateShared?: boolean 16 | prismaSchema?: string 17 | sdl?: string 18 | } 19 | 20 | export async function getDTSFilesForRun(run: Run) { 21 | const prisma = getPrismaSchema(run.prismaSchema ?? "") 22 | let gqlSDL = run.sdl ?? "" 23 | if (!gqlSDL.includes("type Query")) gqlSDL += "type Query { _: String }\n" 24 | if (!gqlSDL.includes("type Mutation")) gqlSDL += "type Mutation { __: String }" 25 | 26 | const schema = buildSchema(gqlSDL) 27 | const project = new Project({ useInMemoryFileSystem: true }) 28 | 29 | const vfsMap = new Map() 30 | 31 | const vfs = createSystem(vfsMap) 32 | 33 | const appContext: AppContext = { 34 | gql: schema, 35 | prisma: prismaModeller(prisma), 36 | tsProject: project, 37 | fieldFacts: new Map(), 38 | codeFacts: new Map(), 39 | pathSettings: { 40 | root: "/", 41 | graphQLSchemaPath: "/.redwood/schema.graphql", 42 | apiServicesPath: "/api/src/services", 43 | prismaDSLPath: "/api/db/schema.prisma", 44 | sharedFilename: "shared-schema-types.d.ts", 45 | sharedInternalFilename: "shared-return-types.d.ts", 46 | typesFolderRoot: "/types", 47 | }, 48 | sys: vfs, 49 | basename, 50 | join, 51 | } 52 | 53 | if (run.gamesService) { 54 | vfsMap.set("/api/src/services/games.ts", run.gamesService) 55 | await lookAtServiceFile("/api/src/services/games.ts", appContext) 56 | } 57 | 58 | if (run.generateShared) { 59 | await createSharedSchemaFiles(appContext, false) 60 | } 61 | 62 | return { 63 | vfsMap, 64 | appContext, 65 | } 66 | } 67 | 68 | export const graphql = (strings: TemplateStringsArray): string => strings[0] 69 | export const prisma = (strings: TemplateStringsArray): string => strings[0] 70 | -------------------------------------------------------------------------------- /src/tests/vendor/puzzmo/one-offs/userProfiles.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | 3 | import { dnull } from "dnull" 4 | 5 | import { db } from "src/lib/db" 6 | 7 | import { 8 | UpdateUserProfileResolver, 9 | DeleteUserProfileResolver, 10 | UserProfileTypeResolvers, 11 | AddLeaderboardToUserProfileResolver, 12 | RemoveLeaderboardFromUserProfileResolver, 13 | } from "src/lib/types/userProfiles" 14 | 15 | export const updateUserProfile: UpdateUserProfileResolver = ({ input, id }) => {} 16 | 17 | export const addLeaderboardToUserProfile: AddLeaderboardToUserProfileResolver = async ({ leaderboardStableID }) => {} 18 | 19 | export const removeLeaderboardFromUserProfile: RemoveLeaderboardFromUserProfileResolver = async ({ leaderboardStableID }) => {} 20 | 21 | export const deleteUserProfile: DeleteUserProfileResolver = (args) => { 22 | const { id } = args 23 | return db.userProfile.delete({ where: { userID: id.replace(":userprofile", "") } }) 24 | } 25 | 26 | export const UserProfile: UserProfileTypeResolvers = { 27 | id: (_, { root }) => root.userID + ":userprofile", 28 | user: (_obj, { root }) => db.user.findFirst({ where: { id: root.userID } }), 29 | } 30 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage-output/games.d.ts: -------------------------------------------------------------------------------- 1 | import type { CreateGameInput, UpdateGameInput } from "./shared-schema-types"; 2 | import type { Game as RTGame, Prediction as RTPrediction, Team as RTTeam, Season as RTSeason } from "./shared-return-types"; 3 | import type { Prediction as PPrediction, Team as PTeam, Season as PSeason, Game as PGame } from "@prisma/client"; 4 | import type { GraphQLResolveInfo } from "graphql"; 5 | import type { RedwoodGraphQLContext } from "@redwoodjs/graphql-server/dist/functions/types"; 6 | 7 | /** SDL: games: [Game!]! */ 8 | export interface GamesResolver { 9 | (args: object, obj: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame[] | Promise | (() => Promise); 10 | } 11 | 12 | /** SDL: upcomingGames: [Game!]! */ 13 | export interface UpcomingGamesResolver { 14 | (args?: object, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame[] | Promise | (() => Promise); 15 | } 16 | 17 | /** SDL: game(id: Int!): Game */ 18 | export interface GameResolver { 19 | (args: { id: number }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame | null | Promise | (() => Promise); 20 | } 21 | 22 | /** SDL: createGame(input: CreateGameInput!): Game! */ 23 | export interface CreateGameResolver { 24 | (args: { input: CreateGameInput }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame | Promise | (() => Promise); 25 | } 26 | 27 | /** SDL: updateGame(id: Int!, input: UpdateGameInput!): Game! */ 28 | export interface UpdateGameResolver { 29 | (args: { id: number, input: UpdateGameInput }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTGame | Promise | (() => Promise); 30 | } 31 | 32 | /** SDL: deleteGame(id: Int!): Game! */ 33 | export interface DeleteGameResolver { 34 | (args: { id: number }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): Promise; 35 | } 36 | 37 | type GameAsParent = PGame & { 38 | predictions: () => Promise>, 39 | homeTeam: () => Promise, 40 | awayTeam: () => Promise, 41 | season: () => Promise 42 | } & Extended; 43 | 44 | export interface GameTypeResolvers { 45 | 46 | /** SDL: predictions: [Prediction]! */ 47 | predictions: (args: undefined, obj: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => Array | Promise> | (() => Promise>); 48 | 49 | /** SDL: homeTeam: Team! */ 50 | homeTeam: (args: undefined, obj: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => RTTeam | Promise | (() => Promise); 51 | 52 | /** SDL: awayTeam: Team! */ 53 | awayTeam: (args: undefined, obj: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => RTTeam | Promise | (() => Promise); 54 | 55 | /** SDL: season: Season! */ 56 | season: (args: undefined, obj: { root: GameAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => RTSeason | Promise | (() => Promise); 57 | } 58 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage-output/predictions.d.ts: -------------------------------------------------------------------------------- 1 | import type { CreatePredictionInput, UpdatePredictionInput } from "./shared-schema-types"; 2 | import type { StandingsResult as RTStandingsResult, Prediction as RTPrediction, User as RTUser, Team as RTTeam, Game as RTGame } from "./shared-return-types"; 3 | import type { User as PUser, Team as PTeam, Game as PGame, Prediction as PPrediction } from "@prisma/client"; 4 | import type { GraphQLResolveInfo } from "graphql"; 5 | import type { RedwoodGraphQLContext } from "@redwoodjs/graphql-server/dist/functions/types"; 6 | 7 | /** SDL: standings(seasonId: Int!): StandingsResult */ 8 | export interface StandingsResolver { 9 | (args: { seasonId: number }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): Promise; 10 | } 11 | 12 | /** SDL: predictions: [Prediction!]! */ 13 | export interface PredictionsResolver { 14 | (args?: object, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTPrediction[] | Promise | (() => Promise); 15 | } 16 | 17 | /** SDL: myPredictions: [Prediction!]! */ 18 | export interface MyPredictionsResolver { 19 | (args: object, obj: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTPrediction[] | Promise | (() => Promise); 20 | } 21 | 22 | /** SDL: prediction(id: Int!): Prediction */ 23 | export interface PredictionResolver { 24 | (args: { id: number }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTPrediction | null | Promise | (() => Promise); 25 | } 26 | 27 | /** SDL: createPrediction(input: CreatePredictionInput!): Prediction! */ 28 | export interface CreatePredictionResolver { 29 | (args: { input: CreatePredictionInput }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTPrediction | Promise | (() => Promise); 30 | } 31 | 32 | /** SDL: updatePrediction(id: Int!, input: UpdatePredictionInput!): Prediction! */ 33 | export interface UpdatePredictionResolver { 34 | (args: { id: number, input: UpdatePredictionInput }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTPrediction | Promise | (() => Promise); 35 | } 36 | 37 | /** SDL: deletePrediction(id: Int!): Prediction! */ 38 | export interface DeletePredictionResolver { 39 | (args: { id: number }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTPrediction | Promise | (() => Promise); 40 | } 41 | 42 | type PredictionAsParent = PPrediction & { 43 | id: () => Promise, 44 | teamId: () => Promise, 45 | gameId: () => Promise, 46 | userId: () => Promise, 47 | prediction: () => Promise, 48 | user: () => Promise, 49 | team: () => Promise, 50 | game: () => Promise 51 | }; 52 | 53 | export interface PredictionTypeResolvers { 54 | 55 | /** SDL: id: Int! */ 56 | id: (args: undefined, obj: { root: PredictionAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => number | Promise | (() => Promise); 57 | 58 | /** SDL: teamId: Int */ 59 | teamId: (args: undefined, obj: { root: PredictionAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => number | null | Promise | (() => Promise); 60 | 61 | /** SDL: gameId: Int! */ 62 | gameId: (args: undefined, obj: { root: PredictionAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => number | Promise | (() => Promise); 63 | 64 | /** SDL: userId: Int! */ 65 | userId: (args: undefined, obj: { root: PredictionAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => number | Promise | (() => Promise); 66 | 67 | /** SDL: prediction: String! */ 68 | prediction: (args: undefined, obj: { root: PredictionAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | Promise | (() => Promise); 69 | 70 | /** SDL: user: User */ 71 | user: (args: undefined, obj: { root: PredictionAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => RTUser | null | Promise | (() => Promise); 72 | 73 | /** SDL: team: Team */ 74 | team: (args: undefined, obj: { root: PredictionAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => RTTeam | null | Promise | (() => Promise); 75 | 76 | /** SDL: game: Game! */ 77 | game: (args: undefined, obj: { root: PredictionAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => RTGame | Promise | (() => Promise); 78 | } 79 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage-output/seasons.d.ts: -------------------------------------------------------------------------------- 1 | import type { CreateSeasonInput, UpdateSeasonInput } from "./shared-schema-types"; 2 | import type { Season as RTSeason, Prediction as RTPrediction } from "./shared-return-types"; 3 | import type { Prediction as PPrediction, Season as PSeason } from "@prisma/client"; 4 | import type { GraphQLResolveInfo } from "graphql"; 5 | import type { RedwoodGraphQLContext } from "@redwoodjs/graphql-server/dist/functions/types"; 6 | 7 | /** SDL: seasons: [Season!]! */ 8 | export interface SeasonsResolver { 9 | (args?: object, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTSeason[] | Promise | (() => Promise); 10 | } 11 | 12 | /** SDL: season(id: Int!): Season */ 13 | export interface SeasonResolver { 14 | (args: { id: number }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTSeason | null | Promise | (() => Promise); 15 | } 16 | 17 | /** SDL: createSeason(input: CreateSeasonInput!): Season! */ 18 | export interface CreateSeasonResolver { 19 | (args: { input: CreateSeasonInput }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTSeason | Promise | (() => Promise); 20 | } 21 | 22 | /** SDL: updateSeason(id: Int!, input: UpdateSeasonInput!): Season! */ 23 | export interface UpdateSeasonResolver { 24 | (args: { id: number, input: UpdateSeasonInput }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTSeason | Promise | (() => Promise); 25 | } 26 | 27 | /** SDL: deleteSeason(id: Int!): Season! */ 28 | export interface DeleteSeasonResolver { 29 | (args: { id: number }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTSeason | Promise | (() => Promise); 30 | } 31 | 32 | type SeasonAsParent = PSeason & { 33 | id: () => Promise, 34 | name: () => Promise, 35 | startDate: () => Promise, 36 | endDate: () => Promise, 37 | Prediction: () => Promise> 38 | }; 39 | 40 | export interface SeasonTypeResolvers { 41 | 42 | /** SDL: id: Int! */ 43 | id: (args: undefined, obj: { root: SeasonAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => number | Promise | (() => Promise); 44 | 45 | /** SDL: name: String! */ 46 | name: (args: undefined, obj: { root: SeasonAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | Promise | (() => Promise); 47 | 48 | /** SDL: startDate: DateTime! */ 49 | startDate: (args: undefined, obj: { root: SeasonAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => DateTime | Promise | (() => Promise); 50 | 51 | /** SDL: endDate: DateTime! */ 52 | endDate: (args: undefined, obj: { root: SeasonAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => DateTime | Promise | (() => Promise); 53 | 54 | /** SDL: Prediction: [Prediction]! */ 55 | Prediction: (args: undefined, obj: { root: SeasonAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => Array | Promise> | (() => Promise>); 56 | } 57 | 58 | type DateTime = any; 59 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage-output/shared-return-types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Game as PGame, Prediction as PPrediction, Season as PSeason, Team as PTeam, User as PUser } from "@prisma/client"; 2 | 3 | // You may very reasonably ask yourself, 'what is this file?' and why do I need it. 4 | 5 | // Roughly, this file ensures that when a resolver wants to return a type - that 6 | // type will match a prisma model. This is useful because you can trivially extend 7 | // the type in the SDL and not have to worry about type mis-matches because the thing 8 | // you returned does not include those functions. 9 | 10 | // This gets particularly useful when you want to return a union type, an interface, 11 | // or a model where the prisma model is nested pretty deeply (connections, for example.) 12 | export interface CreateGameInput { 13 | __typename?: "CreateGameInput"; 14 | awayTeamId: number; 15 | awayTeamScore?: number | null; 16 | homeTeamId: number; 17 | homeTeamScore?: number | null; 18 | isCompleted?: boolean | null; 19 | seasonId: number; 20 | startDateTime: DateTime; 21 | } 22 | 23 | export interface CreatePredictionInput { 24 | __typename?: "CreatePredictionInput"; 25 | gameId: number; 26 | prediction: string; 27 | seasonId: number; 28 | teamId?: number | null; 29 | userId: number; 30 | } 31 | 32 | export interface CreateSeasonInput { 33 | __typename?: "CreateSeasonInput"; 34 | endDate: DateTime; 35 | name: string; 36 | startDate: DateTime; 37 | } 38 | 39 | export interface CreateTeamInput { 40 | __typename?: "CreateTeamInput"; 41 | logoUrl?: string | null; 42 | name: string; 43 | } 44 | 45 | export interface CreateUserInput { 46 | __typename?: "CreateUserInput"; 47 | email: string; 48 | hashedPassword: string; 49 | resetToken?: string | null; 50 | resetTokenExpiresAt?: DateTime | null; 51 | roles: string; 52 | salt: string; 53 | username: string; 54 | } 55 | 56 | export interface Mutation { 57 | __typename?: "Mutation"; 58 | createGame: PGame; 59 | createPrediction: PPrediction; 60 | createSeason: PSeason; 61 | createTeam: PTeam; 62 | createUser: PUser; 63 | deleteGame: PGame; 64 | deletePrediction: PPrediction; 65 | deleteSeason: PSeason; 66 | deleteTeam: PTeam; 67 | deleteUser: PUser; 68 | resetPassword: PUser; 69 | sendResetPasswordEmail?: SuccessInput | null; 70 | updateGame: PGame; 71 | updatePrediction: PPrediction; 72 | updateSeason: PSeason; 73 | updateTeam: PTeam; 74 | updateUser: PUser; 75 | } 76 | 77 | export interface Query { 78 | __typename?: "Query"; 79 | game?: PGame | null; 80 | games: PGame[]; 81 | myPredictions: PPrediction[]; 82 | prediction?: PPrediction | null; 83 | predictions: PPrediction[]; 84 | redwood?: Redwood | null; 85 | season?: PSeason | null; 86 | seasons: PSeason[]; 87 | standings?: StandingsResult | null; 88 | team?: PTeam | null; 89 | teams: PTeam[]; 90 | upcomingGames: PGame[]; 91 | user?: PUser | null; 92 | users: PUser[]; 93 | } 94 | 95 | export interface Redwood { 96 | __typename?: "Redwood"; 97 | currentUser?: JSON | null; 98 | prismaVersion?: string | null; 99 | version?: string | null; 100 | } 101 | 102 | export interface StandingsData { 103 | __typename?: "StandingsData"; 104 | email: string; 105 | score: number; 106 | userId: string; 107 | username: string; 108 | } 109 | 110 | export interface StandingsResult { 111 | __typename?: "StandingsResult"; 112 | userIdRankings: StandingsData[]; 113 | } 114 | 115 | export interface SuccessInput { 116 | __typename?: "SuccessInput"; 117 | message?: string | null; 118 | success?: boolean | null; 119 | } 120 | 121 | export interface UpdateGameInput { 122 | __typename?: "UpdateGameInput"; 123 | awayTeamId?: number | null; 124 | awayTeamScore?: number | null; 125 | homeTeamId?: number | null; 126 | homeTeamScore?: number | null; 127 | isCompleted?: boolean | null; 128 | seasonId?: number | null; 129 | startDateTime?: DateTime | null; 130 | } 131 | 132 | export interface UpdatePredictionInput { 133 | __typename?: "UpdatePredictionInput"; 134 | gameId?: number | null; 135 | prediction?: string | null; 136 | teamId?: number | null; 137 | userId?: number | null; 138 | } 139 | 140 | export interface UpdateSeasonInput { 141 | __typename?: "UpdateSeasonInput"; 142 | endDate?: DateTime | null; 143 | name?: string | null; 144 | startDate?: DateTime | null; 145 | } 146 | 147 | export interface UpdateTeamInput { 148 | __typename?: "UpdateTeamInput"; 149 | logoUrl?: string | null; 150 | name?: string | null; 151 | } 152 | 153 | export interface UpdateUserInput { 154 | __typename?: "UpdateUserInput"; 155 | email?: string | null; 156 | hashedPassword?: string | null; 157 | resetToken?: string | null; 158 | resetTokenExpiresAt?: DateTime | null; 159 | roles?: string | null; 160 | salt?: string | null; 161 | username?: string | null; 162 | } 163 | 164 | type DateTime = any; 165 | type JSON = any; 166 | export type Game = PGame; 167 | export type Prediction = PPrediction; 168 | export type Season = PSeason; 169 | export type Team = PTeam; 170 | export type User = PUser; 171 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage-output/shared-schema-types.d.ts: -------------------------------------------------------------------------------- 1 | export interface CreateGameInput { 2 | __typename?: "CreateGameInput"; 3 | awayTeamId: number; 4 | awayTeamScore?: number | null; 5 | homeTeamId: number; 6 | homeTeamScore?: number | null; 7 | isCompleted?: boolean | null; 8 | seasonId: number; 9 | startDateTime: DateTime; 10 | } 11 | 12 | export interface CreatePredictionInput { 13 | __typename?: "CreatePredictionInput"; 14 | gameId: number; 15 | prediction: string; 16 | seasonId: number; 17 | teamId?: number | null; 18 | userId: number; 19 | } 20 | 21 | export interface CreateSeasonInput { 22 | __typename?: "CreateSeasonInput"; 23 | endDate: DateTime; 24 | name: string; 25 | startDate: DateTime; 26 | } 27 | 28 | export interface CreateTeamInput { 29 | __typename?: "CreateTeamInput"; 30 | logoUrl?: string | null; 31 | name: string; 32 | } 33 | 34 | export interface CreateUserInput { 35 | __typename?: "CreateUserInput"; 36 | email: string; 37 | hashedPassword: string; 38 | resetToken?: string | null; 39 | resetTokenExpiresAt?: DateTime | null; 40 | roles: string; 41 | salt: string; 42 | username: string; 43 | } 44 | 45 | export interface Game { 46 | __typename?: "Game"; 47 | awayTeam?: Team; 48 | awayTeamId: number; 49 | awayTeamScore?: number | null; 50 | homeTeam?: Team; 51 | homeTeamId: number; 52 | homeTeamScore?: number | null; 53 | id: number; 54 | isCompleted: boolean; 55 | predictions?: Array; 56 | season?: Season; 57 | seasonId: number; 58 | startDateTime: DateTime; 59 | } 60 | 61 | export interface Mutation { 62 | __typename?: "Mutation"; 63 | createGame: Game; 64 | createPrediction: Prediction; 65 | createSeason: Season; 66 | createTeam: Team; 67 | createUser: User; 68 | deleteGame: Game; 69 | deletePrediction: Prediction; 70 | deleteSeason: Season; 71 | deleteTeam: Team; 72 | deleteUser: User; 73 | resetPassword: User; 74 | sendResetPasswordEmail?: SuccessInput | null; 75 | updateGame: Game; 76 | updatePrediction: Prediction; 77 | updateSeason: Season; 78 | updateTeam: Team; 79 | updateUser: User; 80 | } 81 | 82 | export interface Prediction { 83 | __typename?: "Prediction"; 84 | game?: Game; 85 | gameId?: number; 86 | id?: number; 87 | prediction?: string; 88 | team?: Team | null; 89 | teamId?: number | null; 90 | user?: User | null; 91 | userId?: number; 92 | } 93 | 94 | export interface Query { 95 | __typename?: "Query"; 96 | game?: Game | null; 97 | games: Game[]; 98 | myPredictions: Prediction[]; 99 | prediction?: Prediction | null; 100 | predictions: Prediction[]; 101 | redwood?: Redwood | null; 102 | season?: Season | null; 103 | seasons: Season[]; 104 | standings?: StandingsResult | null; 105 | team?: Team | null; 106 | teams: Team[]; 107 | upcomingGames: Game[]; 108 | user?: User | null; 109 | users: User[]; 110 | } 111 | 112 | export interface Redwood { 113 | __typename?: "Redwood"; 114 | currentUser?: JSON | null; 115 | prismaVersion?: string | null; 116 | version?: string | null; 117 | } 118 | 119 | export interface Season { 120 | __typename?: "Season"; 121 | Prediction?: Array; 122 | endDate?: DateTime; 123 | id?: number; 124 | name?: string; 125 | startDate?: DateTime; 126 | } 127 | 128 | export interface StandingsData { 129 | __typename?: "StandingsData"; 130 | email: string; 131 | score: number; 132 | userId: string; 133 | username: string; 134 | } 135 | 136 | export interface StandingsResult { 137 | __typename?: "StandingsResult"; 138 | userIdRankings: StandingsData[]; 139 | } 140 | 141 | export interface SuccessInput { 142 | __typename?: "SuccessInput"; 143 | message?: string | null; 144 | success?: boolean | null; 145 | } 146 | 147 | export interface Team { 148 | __typename?: "Team"; 149 | Prediction?: Array; 150 | awayTeamGames?: Array; 151 | homeTeamGames?: Array; 152 | id?: number; 153 | logoUrl?: string | null; 154 | name?: string; 155 | } 156 | 157 | export interface UpdateGameInput { 158 | __typename?: "UpdateGameInput"; 159 | awayTeamId?: number | null; 160 | awayTeamScore?: number | null; 161 | homeTeamId?: number | null; 162 | homeTeamScore?: number | null; 163 | isCompleted?: boolean | null; 164 | seasonId?: number | null; 165 | startDateTime?: DateTime | null; 166 | } 167 | 168 | export interface UpdatePredictionInput { 169 | __typename?: "UpdatePredictionInput"; 170 | gameId?: number | null; 171 | prediction?: string | null; 172 | teamId?: number | null; 173 | userId?: number | null; 174 | } 175 | 176 | export interface UpdateSeasonInput { 177 | __typename?: "UpdateSeasonInput"; 178 | endDate?: DateTime | null; 179 | name?: string | null; 180 | startDate?: DateTime | null; 181 | } 182 | 183 | export interface UpdateTeamInput { 184 | __typename?: "UpdateTeamInput"; 185 | logoUrl?: string | null; 186 | name?: string | null; 187 | } 188 | 189 | export interface UpdateUserInput { 190 | __typename?: "UpdateUserInput"; 191 | email?: string | null; 192 | hashedPassword?: string | null; 193 | resetToken?: string | null; 194 | resetTokenExpiresAt?: DateTime | null; 195 | roles?: string | null; 196 | salt?: string | null; 197 | username?: string | null; 198 | } 199 | 200 | export interface User { 201 | __typename?: "User"; 202 | email?: string; 203 | hashedPassword?: string; 204 | id?: number; 205 | predictions?: Array; 206 | resetToken?: string | null; 207 | resetTokenExpiresAt?: DateTime | null; 208 | roles?: string; 209 | salt?: string; 210 | username?: string; 211 | } 212 | 213 | type DateTime = any; 214 | type JSON = any; 215 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage-output/teams.d.ts: -------------------------------------------------------------------------------- 1 | import type { CreateTeamInput, UpdateTeamInput } from "./shared-schema-types"; 2 | import type { Team as RTTeam, Prediction as RTPrediction, Game as RTGame } from "./shared-return-types"; 3 | import type { Prediction as PPrediction, Game as PGame, Team as PTeam } from "@prisma/client"; 4 | import type { GraphQLResolveInfo } from "graphql"; 5 | import type { RedwoodGraphQLContext } from "@redwoodjs/graphql-server/dist/functions/types"; 6 | 7 | /** SDL: teams: [Team!]! */ 8 | export interface TeamsResolver { 9 | (args?: object, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTTeam[] | Promise | (() => Promise); 10 | } 11 | 12 | /** SDL: team(id: Int!): Team */ 13 | export interface TeamResolver { 14 | (args: { id: number }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTTeam | null | Promise | (() => Promise); 15 | } 16 | 17 | /** SDL: createTeam(input: CreateTeamInput!): Team! */ 18 | export interface CreateTeamResolver { 19 | (args: { input: CreateTeamInput }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTTeam | Promise | (() => Promise); 20 | } 21 | 22 | /** SDL: updateTeam(id: Int!, input: UpdateTeamInput!): Team! */ 23 | export interface UpdateTeamResolver { 24 | (args: { id: number, input: UpdateTeamInput }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTTeam | Promise | (() => Promise); 25 | } 26 | 27 | /** SDL: deleteTeam(id: Int!): Team! */ 28 | export interface DeleteTeamResolver { 29 | (args: { id: number }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTTeam | Promise | (() => Promise); 30 | } 31 | 32 | type TeamAsParent = PTeam & { 33 | id: () => Promise, 34 | name: () => Promise, 35 | logoUrl: () => Promise, 36 | Prediction: () => Promise>, 37 | homeTeamGames: () => Promise>, 38 | awayTeamGames: () => Promise> 39 | }; 40 | 41 | export interface TeamTypeResolvers { 42 | 43 | /** SDL: id: Int! */ 44 | id: (args: undefined, obj: { root: TeamAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => number | Promise | (() => Promise); 45 | 46 | /** SDL: name: String! */ 47 | name: (args: undefined, obj: { root: TeamAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | Promise | (() => Promise); 48 | 49 | /** SDL: logoUrl: String */ 50 | logoUrl: (args: undefined, obj: { root: TeamAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | null | Promise | (() => Promise); 51 | 52 | /** SDL: Prediction: [Prediction]! */ 53 | Prediction: (args: undefined, obj: { root: TeamAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => Array | Promise> | (() => Promise>); 54 | 55 | /** SDL: homeTeamGames: [Game]! */ 56 | homeTeamGames: (args: undefined, obj: { root: TeamAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => Array | Promise> | (() => Promise>); 57 | 58 | /** SDL: awayTeamGames: [Game]! */ 59 | awayTeamGames: (args: undefined, obj: { root: TeamAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => Array | Promise> | (() => Promise>); 60 | } 61 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage-output/users.d.ts: -------------------------------------------------------------------------------- 1 | import type { CreateUserInput, UpdateUserInput } from "./shared-schema-types"; 2 | import type { User as RTUser, SuccessInput as RTSuccessInput, Prediction as RTPrediction } from "./shared-return-types"; 3 | import type { Prediction as PPrediction, User as PUser } from "@prisma/client"; 4 | import type { GraphQLResolveInfo } from "graphql"; 5 | import type { RedwoodGraphQLContext } from "@redwoodjs/graphql-server/dist/functions/types"; 6 | 7 | /** SDL: users: [User!]! */ 8 | export interface UsersResolver { 9 | (args?: object, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTUser[] | Promise | (() => Promise); 10 | } 11 | 12 | /** SDL: user(id: Int!): User */ 13 | export interface UserResolver { 14 | (args: { id: number }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTUser | null | Promise | (() => Promise); 15 | } 16 | 17 | /** SDL: createUser(input: CreateUserInput!): User! */ 18 | export interface CreateUserResolver { 19 | (args: { input: CreateUserInput }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTUser | Promise | (() => Promise); 20 | } 21 | 22 | /** SDL: updateUser(id: Int!, input: UpdateUserInput!): User! */ 23 | export interface UpdateUserResolver { 24 | (args: { id: number, input: UpdateUserInput }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTUser | Promise | (() => Promise); 25 | } 26 | 27 | /** SDL: deleteUser(id: Int!): User! */ 28 | export interface DeleteUserResolver { 29 | (args: { id: number }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): RTUser | Promise | (() => Promise); 30 | } 31 | 32 | /** SDL: sendResetPasswordEmail(email: String!): SuccessInput */ 33 | export interface SendResetPasswordEmailResolver { 34 | (args: { email: string }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): Promise; 35 | } 36 | 37 | /** SDL: resetPassword(password: String!, resetToken: String!): User! */ 38 | export interface ResetPasswordResolver { 39 | (args: { password: string, resetToken: string }, obj?: { root: object, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }): Promise; 40 | } 41 | 42 | type UserAsParent = PUser & { 43 | id: () => Promise, 44 | email: () => Promise, 45 | username: () => Promise, 46 | hashedPassword: () => Promise, 47 | resetToken: () => Promise, 48 | resetTokenExpiresAt: () => Promise, 49 | salt: () => Promise, 50 | roles: () => Promise, 51 | predictions: () => Promise> 52 | }; 53 | 54 | export interface UserTypeResolvers { 55 | 56 | /** SDL: id: Int! */ 57 | id: (args: undefined, obj: { root: UserAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => number | Promise | (() => Promise); 58 | 59 | /** SDL: email: String! */ 60 | email: (args: undefined, obj: { root: UserAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | Promise | (() => Promise); 61 | 62 | /** SDL: username: String! */ 63 | username: (args: undefined, obj: { root: UserAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | Promise | (() => Promise); 64 | 65 | /** SDL: hashedPassword: String! */ 66 | hashedPassword: (args: undefined, obj: { root: UserAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | Promise | (() => Promise); 67 | 68 | /** SDL: resetToken: String */ 69 | resetToken: (args: undefined, obj: { root: UserAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | null | Promise | (() => Promise); 70 | 71 | /** SDL: resetTokenExpiresAt: DateTime */ 72 | resetTokenExpiresAt: (args: undefined, obj: { root: UserAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => DateTime | null | Promise | (() => Promise); 73 | 74 | /** SDL: salt: String! */ 75 | salt: (args: undefined, obj: { root: UserAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | Promise | (() => Promise); 76 | 77 | /** SDL: roles: String! */ 78 | roles: (args: undefined, obj: { root: UserAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => string | Promise | (() => Promise); 79 | 80 | /** SDL: predictions: [Prediction]! */ 81 | predictions: (args: undefined, obj: { root: UserAsParent, context: RedwoodGraphQLContext, info: GraphQLResolveInfo }) => Array | Promise> | (() => Promise>); 82 | } 83 | 84 | type DateTime = any; 85 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/.redwood/schema.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | Use to check whether or not a user is authenticated and is associated 3 | with an optional set of roles. 4 | """ 5 | directive @requireAuth(roles: [String]) on FIELD_DEFINITION 6 | 7 | """Use to skip authentication checks and allow public access.""" 8 | directive @skipAuth on FIELD_DEFINITION 9 | 10 | scalar BigInt 11 | 12 | input CreateGameInput { 13 | awayTeamId: Int! 14 | awayTeamScore: Int 15 | homeTeamId: Int! 16 | homeTeamScore: Int 17 | isCompleted: Boolean 18 | seasonId: Int! 19 | startDateTime: DateTime! 20 | } 21 | 22 | input CreatePredictionInput { 23 | gameId: Int! 24 | prediction: String! 25 | seasonId: Int! 26 | teamId: Int 27 | userId: Int! 28 | } 29 | 30 | input CreateSeasonInput { 31 | endDate: DateTime! 32 | name: String! 33 | startDate: DateTime! 34 | } 35 | 36 | input CreateTeamInput { 37 | logoUrl: String 38 | name: String! 39 | } 40 | 41 | input CreateUserInput { 42 | email: String! 43 | hashedPassword: String! 44 | resetToken: String 45 | resetTokenExpiresAt: DateTime 46 | roles: String! 47 | salt: String! 48 | username: String! 49 | } 50 | 51 | scalar Date 52 | 53 | scalar DateTime 54 | 55 | type Game { 56 | awayTeam: Team! 57 | awayTeamId: Int! 58 | awayTeamScore: Int 59 | homeTeam: Team! 60 | homeTeamId: Int! 61 | homeTeamScore: Int 62 | id: Int! 63 | isCompleted: Boolean! 64 | predictions: [Prediction]! 65 | season: Season! 66 | seasonId: Int! 67 | startDateTime: DateTime! 68 | } 69 | 70 | scalar JSON 71 | 72 | scalar JSONObject 73 | 74 | type Mutation { 75 | createGame(input: CreateGameInput!): Game! 76 | createPrediction(input: CreatePredictionInput!): Prediction! 77 | createSeason(input: CreateSeasonInput!): Season! 78 | createTeam(input: CreateTeamInput!): Team! 79 | createUser(input: CreateUserInput!): User! 80 | deleteGame(id: Int!): Game! 81 | deletePrediction(id: Int!): Prediction! 82 | deleteSeason(id: Int!): Season! 83 | deleteTeam(id: Int!): Team! 84 | deleteUser(id: Int!): User! 85 | resetPassword(password: String!, resetToken: String!): User! 86 | sendResetPasswordEmail(email: String!): SuccessInput 87 | updateGame(id: Int!, input: UpdateGameInput!): Game! 88 | updatePrediction(id: Int!, input: UpdatePredictionInput!): Prediction! 89 | updateSeason(id: Int!, input: UpdateSeasonInput!): Season! 90 | updateTeam(id: Int!, input: UpdateTeamInput!): Team! 91 | updateUser(id: Int!, input: UpdateUserInput!): User! 92 | } 93 | 94 | type Prediction { 95 | game: Game! 96 | gameId: Int! 97 | id: Int! 98 | prediction: String! 99 | team: Team 100 | teamId: Int 101 | user: User 102 | userId: Int! 103 | } 104 | 105 | """About the Redwood queries.""" 106 | type Query { 107 | game(id: Int!): Game 108 | games: [Game!]! 109 | myPredictions: [Prediction!]! 110 | prediction(id: Int!): Prediction 111 | predictions: [Prediction!]! 112 | 113 | """Fetches the Redwood root schema.""" 114 | redwood: Redwood 115 | season(id: Int!): Season 116 | seasons: [Season!]! 117 | standings(seasonId: Int!): StandingsResult 118 | team(id: Int!): Team 119 | teams: [Team!]! 120 | upcomingGames: [Game!]! 121 | user(id: Int!): User 122 | users: [User!]! 123 | } 124 | 125 | """ 126 | The RedwoodJS Root Schema 127 | 128 | Defines details about RedwoodJS such as the current user and version information. 129 | """ 130 | type Redwood { 131 | """The current user.""" 132 | currentUser: JSON 133 | 134 | """The version of Prisma.""" 135 | prismaVersion: String 136 | 137 | """The version of Redwood.""" 138 | version: String 139 | } 140 | 141 | type Season { 142 | Prediction: [Prediction]! 143 | endDate: DateTime! 144 | id: Int! 145 | name: String! 146 | startDate: DateTime! 147 | } 148 | 149 | type StandingsData { 150 | email: String! 151 | score: Int! 152 | userId: String! 153 | username: String! 154 | } 155 | 156 | type StandingsResult { 157 | userIdRankings: [StandingsData!]! 158 | } 159 | 160 | type SuccessInput { 161 | message: String 162 | success: Boolean 163 | } 164 | 165 | type Team { 166 | Prediction: [Prediction]! 167 | awayTeamGames: [Game]! 168 | homeTeamGames: [Game]! 169 | id: Int! 170 | logoUrl: String 171 | name: String! 172 | } 173 | 174 | scalar Time 175 | 176 | input UpdateGameInput { 177 | awayTeamId: Int 178 | awayTeamScore: Int 179 | homeTeamId: Int 180 | homeTeamScore: Int 181 | isCompleted: Boolean 182 | seasonId: Int 183 | startDateTime: DateTime 184 | } 185 | 186 | input UpdatePredictionInput { 187 | gameId: Int 188 | prediction: String 189 | teamId: Int 190 | userId: Int 191 | } 192 | 193 | input UpdateSeasonInput { 194 | endDate: DateTime 195 | name: String 196 | startDate: DateTime 197 | } 198 | 199 | input UpdateTeamInput { 200 | logoUrl: String 201 | name: String 202 | } 203 | 204 | input UpdateUserInput { 205 | email: String 206 | hashedPassword: String 207 | resetToken: String 208 | resetTokenExpiresAt: DateTime 209 | roles: String 210 | salt: String 211 | username: String 212 | } 213 | 214 | type User { 215 | email: String! 216 | hashedPassword: String! 217 | id: Int! 218 | predictions: [Prediction]! 219 | resetToken: String 220 | resetTokenExpiresAt: DateTime 221 | roles: String! 222 | salt: String! 223 | username: String! 224 | } -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/README.md: -------------------------------------------------------------------------------- 1 | Cloned at bb3b2a0c6a5d6dce20cb68cec15ec2436acc4848 2 | 3 | Then reduced to only the useful files, and then possibly modified in the fut ure. 4 | 5 | --- 6 | 7 | # README 8 | 9 | Redwood/TypeScript based website to track soccer predictions, and compare prediction accuracy against others. 10 | 11 | The site itself is not yet live, but will hopefully go live and enter into a beta stage in Fall 2022! 12 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/db/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | binaryTargets = "native" 9 | } 10 | 11 | model Team { 12 | id Int @id @default(autoincrement()) 13 | name String @unique 14 | logoUrl String? 15 | Prediction Prediction[] 16 | homeTeamGames Game[] @relation(name: "homeTeam") 17 | awayTeamGames Game[] @relation(name: "awayTeam") 18 | } 19 | 20 | model Prediction { 21 | id Int @id @default(autoincrement()) 22 | userId Int 23 | gameId Int 24 | teamId Int? 25 | prediction String 26 | seasonId Int 27 | season Season @relation(fields: [seasonId], references: [id]) 28 | game Game @relation(fields: [gameId], references: [id]) 29 | user User @relation(fields: [userId], references: [id]) 30 | team Team? @relation(fields: [teamId], references: [id]) 31 | } 32 | 33 | model Game { 34 | id Int @id @default(autoincrement()) 35 | homeTeamId Int 36 | awayTeamId Int 37 | homeTeamScore Int? 38 | awayTeamScore Int? 39 | seasonId Int 40 | isCompleted Boolean @default(false) 41 | predictions Prediction[] 42 | startDateTime DateTime 43 | season Season @relation(fields: [seasonId], references: [id]) 44 | homeTeam Team? @relation(name: "homeTeam", references: [id], fields: [homeTeamId]) 45 | awayTeam Team? @relation(name: "awayTeam", references: [id], fields: [awayTeamId]) 46 | } 47 | 48 | model Season { 49 | id Int @id @default(autoincrement()) 50 | name String @unique 51 | startDate DateTime 52 | endDate DateTime 53 | Prediction Prediction[] 54 | Game Game[] 55 | } 56 | 57 | model User { 58 | id Int @id @default(autoincrement()) 59 | email String @unique 60 | username String @unique 61 | hashedPassword String 62 | salt String 63 | resetToken String? 64 | resetTokenExpiresAt DateTime? 65 | roles String 66 | predictions Prediction[] 67 | } 68 | 69 | model RW_DataMigration { 70 | version String @id 71 | name String 72 | startedAt DateTime 73 | finishedAt DateTime 74 | } 75 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/jest.config.js: -------------------------------------------------------------------------------- 1 | // More info at https://redwoodjs.com/docs/project-configuration-dev-test-build 2 | 3 | const config = { 4 | rootDir: '../', 5 | preset: '@redwoodjs/testing/config/jest/api', 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@redwoodjs/api": "3.4.0", 7 | "@redwoodjs/graphql-server": "3.4.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/server.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file allows you to configure the Fastify Server settings 3 | * used by the RedwoodJS dev server. 4 | * 5 | * It also applies when running the api server with `yarn rw serve`. 6 | * 7 | * For the Fastify server options that you can set, see: 8 | * https://www.fastify.io/docs/latest/Reference/Server/#factory 9 | * 10 | * Examples include: logger settings, timeouts, maximum payload limits, and more. 11 | * 12 | * Note: This configuration does not apply in a serverless deploy. 13 | */ 14 | 15 | /** @type {import('fastify').FastifyServerOptions} */ 16 | const config = { 17 | requestTimeout: 15_000, 18 | logger: { 19 | level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn', 20 | }, 21 | }; 22 | 23 | module.exports = config; 24 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/graphql/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puzzmo-com/sdl-codegen/b8fbb26bb4d2840c151759c00b8c54ee8e326731/src/tests/vendor/soccersage.io-main/api/src/graphql/.keep -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/graphql/games.sdl.ts: -------------------------------------------------------------------------------- 1 | export const schema = gql` 2 | type Game { 3 | id: Int! 4 | homeTeamId: Int! 5 | awayTeamId: Int! 6 | seasonId: Int! 7 | homeTeamScore: Int 8 | awayTeamScore: Int 9 | isCompleted: Boolean! 10 | predictions: [Prediction]! 11 | homeTeam: Team! 12 | awayTeam: Team! 13 | season: Season! 14 | startDateTime: DateTime! 15 | 16 | # This does not exist in prisma, and would 17 | # screw up a return type of db.game in many places 18 | teamsWhoPredictedHomeWin: [Team!]! 19 | } 20 | 21 | type Query { 22 | games: [Game!]! @skipAuth 23 | upcomingGames: [Game!]! @skipAuth 24 | game(id: Int!): Game @requireAuth 25 | } 26 | 27 | input CreateGameInput { 28 | homeTeamId: Int! 29 | awayTeamId: Int! 30 | seasonId: Int! 31 | startDateTime: DateTime! 32 | homeTeamScore: Int 33 | awayTeamScore: Int 34 | isCompleted: Boolean 35 | } 36 | 37 | input UpdateGameInput { 38 | homeTeamId: Int 39 | awayTeamId: Int 40 | homeTeamScore: Int 41 | awayTeamScore: Int 42 | seasonId: Int 43 | isCompleted: Boolean 44 | startDateTime: DateTime 45 | } 46 | 47 | type Mutation { 48 | createGame(input: CreateGameInput!): Game! @requireAuth 49 | updateGame(id: Int!, input: UpdateGameInput!): Game! @requireAuth 50 | deleteGame(id: Int!): Game! @requireAuth 51 | } 52 | `; 53 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/graphql/predictions.sdl.ts: -------------------------------------------------------------------------------- 1 | export const schema = gql` 2 | type Prediction { 3 | id: Int! 4 | userId: Int! 5 | gameId: Int! 6 | teamId: Int 7 | prediction: String! 8 | team: Team 9 | game: Game! 10 | user: User 11 | } 12 | 13 | type StandingsData { 14 | userId: String! 15 | username: String! 16 | email: String! 17 | score: Int! 18 | } 19 | 20 | type StandingsResult { 21 | userIdRankings: [StandingsData!]! 22 | } 23 | 24 | type Query { 25 | predictions: [Prediction!]! @requireAuth 26 | myPredictions: [Prediction!]! @requireAuth 27 | prediction(id: Int!): Prediction @requireAuth 28 | standings(seasonId: Int!): StandingsResult @skipAuth 29 | } 30 | 31 | input CreatePredictionInput { 32 | userId: Int! 33 | gameId: Int! 34 | seasonId: Int! 35 | teamId: Int 36 | prediction: String! 37 | } 38 | 39 | input UpdatePredictionInput { 40 | userId: Int 41 | gameId: Int 42 | teamId: Int 43 | prediction: String 44 | } 45 | 46 | type Mutation { 47 | createPrediction(input: CreatePredictionInput!): Prediction! 48 | @requireAuth 49 | updatePrediction(id: Int!, input: UpdatePredictionInput!): Prediction! 50 | @requireAuth 51 | deletePrediction(id: Int!): Prediction! @requireAuth 52 | } 53 | `; 54 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/graphql/seasons.sdl.ts: -------------------------------------------------------------------------------- 1 | export const schema = gql` 2 | type Season { 3 | id: Int! 4 | name: String! 5 | startDate: DateTime! 6 | endDate: DateTime! 7 | Prediction: [Prediction]! 8 | } 9 | 10 | type Query { 11 | seasons: [Season!]! @requireAuth 12 | season(id: Int!): Season @requireAuth 13 | } 14 | 15 | input CreateSeasonInput { 16 | name: String! 17 | startDate: DateTime! 18 | endDate: DateTime! 19 | } 20 | 21 | input UpdateSeasonInput { 22 | name: String 23 | startDate: DateTime 24 | endDate: DateTime 25 | } 26 | 27 | type Mutation { 28 | createSeason(input: CreateSeasonInput!): Season! @requireAuth 29 | updateSeason(id: Int!, input: UpdateSeasonInput!): Season! @requireAuth 30 | deleteSeason(id: Int!): Season! @requireAuth 31 | } 32 | `; 33 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/graphql/teams.sdl.ts: -------------------------------------------------------------------------------- 1 | export const schema = gql` 2 | type Team { 3 | id: Int! 4 | name: String! 5 | logoUrl: String 6 | Prediction: [Prediction]! 7 | homeTeamGames: [Game]! 8 | awayTeamGames: [Game]! 9 | } 10 | 11 | type Query { 12 | teams: [Team!]! @requireAuth 13 | team(id: Int!): Team @requireAuth 14 | } 15 | 16 | input CreateTeamInput { 17 | name: String! 18 | logoUrl: String 19 | } 20 | 21 | input UpdateTeamInput { 22 | name: String 23 | logoUrl: String 24 | } 25 | 26 | type Mutation { 27 | createTeam(input: CreateTeamInput!): Team! @requireAuth 28 | updateTeam(id: Int!, input: UpdateTeamInput!): Team! @requireAuth 29 | deleteTeam(id: Int!): Team! @requireAuth 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/graphql/users.sdl.ts: -------------------------------------------------------------------------------- 1 | export const schema = gql` 2 | type User { 3 | id: Int! 4 | username: String! 5 | email: String! 6 | hashedPassword: String! 7 | salt: String! 8 | resetToken: String 9 | resetTokenExpiresAt: DateTime 10 | roles: String! 11 | predictions: [Prediction]! 12 | } 13 | 14 | type Query { 15 | users: [User!]! @requireAuth 16 | user(id: Int!): User @requireAuth 17 | } 18 | 19 | input CreateUserInput { 20 | email: String! 21 | username: String! 22 | hashedPassword: String! 23 | salt: String! 24 | resetToken: String 25 | resetTokenExpiresAt: DateTime 26 | roles: String! 27 | } 28 | 29 | input UpdateUserInput { 30 | username: String 31 | email: String 32 | hashedPassword: String 33 | salt: String 34 | resetToken: String 35 | resetTokenExpiresAt: DateTime 36 | roles: String 37 | } 38 | 39 | type SuccessInput { 40 | success: Boolean 41 | message: String 42 | } 43 | 44 | type Mutation { 45 | createUser(input: CreateUserInput!): User! @requireAuth 46 | updateUser(id: Int!, input: UpdateUserInput!): User! @requireAuth 47 | deleteUser(id: Int!): User! @requireAuth 48 | sendResetPasswordEmail(email: String!): SuccessInput @skipAuth 49 | resetPassword(resetToken: String!, password: String!): User! @skipAuth 50 | } 51 | `; 52 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/services/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puzzmo-com/sdl-codegen/b8fbb26bb4d2840c151759c00b8c54ee8e326731/src/tests/vendor/soccersage.io-main/api/src/services/.keep -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/services/games/games.scenarios.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from '@prisma/client'; 2 | 3 | export const standard = defineScenario({ 4 | game: { 5 | one: { 6 | data: { 7 | homeTeam: { create: { name: 'String8184842' } }, 8 | awayTeam: { create: { name: 'String342092' } }, 9 | startDateTime: new Date(), 10 | season: { 11 | create: { 12 | name: 'String8184842', 13 | startDate: new Date(), 14 | endDate: new Date(), 15 | }, 16 | }, 17 | }, 18 | }, 19 | two: { 20 | data: { 21 | homeTeam: { create: { name: 'String2664163' } }, 22 | awayTeam: { create: { name: 'String9522390' } }, 23 | startDateTime: new Date(), 24 | season: { 25 | create: { 26 | name: 'String9522390', 27 | startDate: new Date(), 28 | endDate: new Date(), 29 | }, 30 | }, 31 | }, 32 | }, 33 | }, 34 | }); 35 | 36 | export type StandardScenario = typeof standard; 37 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/services/games/games.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CreateGameResolver, 3 | DeleteGameResolver, 4 | GameResolver, 5 | GamesResolver, 6 | UpcomingGamesResolver, 7 | UpdateGameResolver, 8 | } from "../../../../../soccersage-output/games.d.ts"; 9 | 10 | import { db } from "src/lib/db"; 11 | 12 | export const games: GamesResolver = (args, obj) => { 13 | return db.game.findMany({ 14 | orderBy: { 15 | startDateTime: "asc", 16 | }, 17 | }); 18 | }; 19 | 20 | export const upcomingGames: UpcomingGamesResolver = () => { 21 | return db.game.findMany({ 22 | where: { isCompleted: false, startDateTime: { gt: new Date() } }, 23 | }); 24 | }; 25 | 26 | export const game: GameResolver = ({ id }) => { 27 | return db.game.findUnique({ 28 | where: { id }, 29 | }); 30 | }; 31 | 32 | export const createGame: CreateGameResolver = ({ input }) => { 33 | return db.game.create({ 34 | data: input, 35 | }); 36 | }; 37 | 38 | export const updateGame: UpdateGameResolver = ({ id, input }) => { 39 | return db.game.update({ 40 | data: input, 41 | where: { id }, 42 | }); 43 | }; 44 | 45 | export const deleteGame: DeleteGameResolver = async ({ id }) => { 46 | await db.prediction.deleteMany({ 47 | where: { gameId: id }, 48 | }); 49 | 50 | return db.game.delete({ 51 | where: { id }, 52 | }); 53 | }; 54 | 55 | export const Game: GameResolvers<{ predictions?: any[] }> = { 56 | predictions: (_obj, { root }) => db.game.findUnique({ where: { id: root.id } }).predictions(), 57 | homeTeam: (_obj, { root }) => db.game.findUnique({ where: { id: root.id } }).homeTeam(), 58 | awayTeam: (_obj, { root }) => db.game.findUnique({ where: { id: root.id } }).awayTeam(), 59 | season: (_obj, { root }) => db.game.findUnique({ where: { id: root.id } }).season(), 60 | }; 61 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/services/predictions/predictions.scenarios.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from '@prisma/client'; 2 | 3 | export const standard = defineScenario({ 4 | prediction: { 5 | one: { 6 | data: { 7 | user: { 8 | create: { 9 | username: 'test', 10 | email: 'test1', 11 | hashedPassword: 'test', 12 | salt: 'test', 13 | roles: 'test', 14 | }, 15 | }, 16 | prediction: 'String', 17 | game: { 18 | create: { 19 | homeTeamScore: 5278373, 20 | awayTeamScore: 9262607, 21 | homeTeam: { create: { name: 'String3223999' } }, 22 | awayTeam: { create: { name: 'String5108965' } }, 23 | startDateTime: new Date('2022-10-27'), 24 | season: { 25 | create: { 26 | name: 'String5108965', 27 | startDate: new Date(), 28 | endDate: new Date(), 29 | }, 30 | }, 31 | }, 32 | }, 33 | season: { 34 | create: { 35 | name: 'Test season 1', 36 | startDate: new Date('2022-05-01'), 37 | endDate: new Date('2022-05-31'), 38 | }, 39 | }, 40 | }, 41 | }, 42 | two: { 43 | data: { 44 | user: { 45 | create: { 46 | username: 'test2', 47 | email: 'test2', 48 | hashedPassword: 'test', 49 | salt: 'test', 50 | roles: 'test', 51 | }, 52 | }, 53 | prediction: 'String', 54 | game: { 55 | create: { 56 | homeTeamScore: 2210328, 57 | awayTeamScore: 4786303, 58 | homeTeam: { create: { name: 'String2218724' } }, 59 | awayTeam: { create: { name: 'String3400904' } }, 60 | startDateTime: new Date('2022-06-01'), 61 | season: { 62 | create: { 63 | name: 'String3400904', 64 | startDate: new Date(), 65 | endDate: new Date(), 66 | }, 67 | }, 68 | }, 69 | }, 70 | season: { 71 | create: { 72 | name: 'Test season 2', 73 | startDate: new Date('2022-06-01'), 74 | endDate: new Date('2022-06-30'), 75 | }, 76 | }, 77 | }, 78 | }, 79 | }, 80 | }); 81 | 82 | export type StandardScenario = typeof standard; 83 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/services/predictions/predictions.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Game, 3 | MutationResolvers, 4 | Prediction as PredictionType, 5 | PredictionResolvers, 6 | QueryResolvers, 7 | User, 8 | } from 'types/graphql'; 9 | 10 | import { getFirstUserFromContext } from 'src/lib/auth'; 11 | import { db } from 'src/lib/db'; 12 | 13 | type PartialGame = Omit< 14 | Game, 15 | 'homeTeam' | 'awayTeam' | 'predictions' | 'season' 16 | >; 17 | type PartialUser = Omit; 18 | 19 | type PartialPrediction = Omit & { 20 | game: PartialGame; 21 | user: PartialUser; 22 | }; 23 | 24 | //TODO: Re-use these functions from the PredictionCard 25 | const getWinningTeamId = (game: PartialGame) => { 26 | if (game.homeTeamScore > game.awayTeamScore) { 27 | return game.homeTeamId; 28 | } else if (game.awayTeamScore > game.homeTeamScore) { 29 | return game.awayTeamId; 30 | } 31 | 32 | return null; 33 | }; 34 | 35 | const getPredictionStatus = ( 36 | prediction: PartialPrediction 37 | ): 'incomplete' | 'correct' | 'incorrect' => { 38 | if (!prediction.game.isCompleted) { 39 | return 'incomplete'; 40 | } 41 | 42 | const winningTeamId = getWinningTeamId(prediction.game); 43 | 44 | return winningTeamId === prediction.teamId ? 'correct' : 'incorrect'; 45 | }; 46 | 47 | export const standings: QueryResolvers['standings'] = async ({ seasonId }) => { 48 | // TODO: This logic finds all predictions, and includes the associated game for each prediction. 49 | // While this works with small amounts of data, this will not scale very well, due to the 50 | // re-retrieval of game data for each prediction. 51 | // Other options to consider if/when more users join: 52 | // 1. - Query for all predictions in a season and all games in a season concurrently. 53 | // a. - This is still not as performant as possible, but would reduce duplicate data and retain live standings. 54 | // 2. - Store standings in a separate schema, and have a CRON job that updates them once an hour. 55 | // a. - This would allow for a much more performant solution, but would remove the ability to have live standings. 56 | const predictions = await db.prediction.findMany({ 57 | where: { seasonId }, 58 | include: { 59 | game: true, 60 | user: true, 61 | }, 62 | }); 63 | 64 | const userPredictionMap = predictions.reduce<{ 65 | [key: string]: PartialPrediction[]; 66 | }>((acc, prediction) => { 67 | if (acc[prediction.userId]) { 68 | acc[prediction.userId].push(prediction); 69 | } else { 70 | acc[prediction.userId] = [prediction]; 71 | } 72 | 73 | return acc; 74 | }, {}); 75 | 76 | // TODO: Define the exact scoring algorithm that we would like to use 77 | const userIdRankings = Object.entries(userPredictionMap).map( 78 | ([userId, predictions]) => { 79 | const { email, username } = predictions[0].user; 80 | const score = predictions.reduce((acc, prediction) => { 81 | const predictionStatus = getPredictionStatus(prediction); 82 | switch (predictionStatus) { 83 | case 'correct': 84 | return acc + 1; 85 | default: 86 | return acc; 87 | } 88 | }, 0); 89 | 90 | return { 91 | email, 92 | username, 93 | userId, 94 | score, 95 | }; 96 | } 97 | ); 98 | 99 | return { 100 | userIdRankings, 101 | }; 102 | }; 103 | 104 | export const predictions: QueryResolvers['predictions'] = () => { 105 | return db.prediction.findMany(); 106 | }; 107 | 108 | export const myPredictions: QueryResolvers['myPredictions'] = ( 109 | _temp, 110 | { context } 111 | ) => { 112 | const user = getFirstUserFromContext(context); 113 | return db.prediction.findMany({ 114 | where: { userId: user.id }, 115 | }); 116 | }; 117 | 118 | export const prediction: QueryResolvers['prediction'] = ({ id }) => { 119 | return db.prediction.findUnique({ 120 | where: { id }, 121 | }); 122 | }; 123 | 124 | export const createPrediction: MutationResolvers['createPrediction'] = ({ 125 | input, 126 | }) => { 127 | return db.prediction.create({ 128 | data: input, 129 | }); 130 | }; 131 | 132 | export const updatePrediction: MutationResolvers['updatePrediction'] = ({ 133 | id, 134 | input, 135 | }) => { 136 | return db.prediction.update({ 137 | data: input, 138 | where: { id }, 139 | }); 140 | }; 141 | 142 | export const deletePrediction: MutationResolvers['deletePrediction'] = ({ 143 | id, 144 | }) => { 145 | return db.prediction.delete({ 146 | where: { id }, 147 | }); 148 | }; 149 | 150 | export const Prediction: PredictionResolvers = { 151 | id: (_obj, { root }) => root.id, 152 | teamId: (_obj, { root }) => root.teamId, 153 | gameId: (_obj, { root }) => root.gameId, 154 | userId: (_obj, { root }) => root.userId, 155 | prediction: (_obj, { root }) => root.prediction, 156 | user: (_obj, { root }) => 157 | db.user.findUnique({ where: { id: root.userId } }), 158 | team: (_obj, { root }) => 159 | db.prediction.findUnique({ where: { id: root.id } }).team(), 160 | game: (_obj, { root }) => 161 | db.prediction.findUnique({ where: { id: root.id } }).game(), 162 | }; 163 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/services/seasons/seasons.scenarios.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from '@prisma/client'; 2 | 3 | export const standard = defineScenario({ 4 | season: { 5 | one: { 6 | data: { 7 | name: 'String2358270', 8 | startDate: '2022-06-29T23:14:20Z', 9 | endDate: '2022-06-29T23:14:20Z', 10 | }, 11 | }, 12 | two: { 13 | data: { 14 | name: 'String9676386', 15 | startDate: '2022-06-29T23:14:20Z', 16 | endDate: '2022-06-29T23:14:20Z', 17 | }, 18 | }, 19 | }, 20 | }); 21 | 22 | export type StandardScenario = typeof standard; 23 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/services/seasons/seasons.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | QueryResolvers, 3 | MutationResolvers, 4 | SeasonResolvers, 5 | } from 'types/graphql'; 6 | 7 | import { db } from 'src/lib/db'; 8 | 9 | export const seasons: QueryResolvers['seasons'] = () => { 10 | return db.season.findMany(); 11 | }; 12 | 13 | export const season: QueryResolvers['season'] = ({ id }) => { 14 | return db.season.findUnique({ 15 | where: { id }, 16 | }); 17 | }; 18 | 19 | export const createSeason: MutationResolvers['createSeason'] = ({ input }) => { 20 | return db.season.create({ 21 | data: input, 22 | }); 23 | }; 24 | 25 | export const updateSeason: MutationResolvers['updateSeason'] = ({ 26 | id, 27 | input, 28 | }) => { 29 | return db.season.update({ 30 | data: input, 31 | where: { id }, 32 | }); 33 | }; 34 | 35 | export const deleteSeason: MutationResolvers['deleteSeason'] = ({ id }) => { 36 | return db.season.delete({ 37 | where: { id }, 38 | }); 39 | }; 40 | 41 | export const Season: SeasonResolvers = { 42 | id: (_obj, { root }) => root.id, 43 | name: (_obj, { root }) => root.name, 44 | startDate: (_obj, { root }) => root.startDate, 45 | endDate: (_obj, { root }) => root.endDate, 46 | Prediction: (_obj, { root }) => 47 | db.season.findUnique({ where: { id: root.id } }).Prediction(), 48 | }; 49 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/services/teams/teams.scenarios.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from '@prisma/client'; 2 | 3 | export const standard = defineScenario({ 4 | team: { 5 | one: { data: { name: 'String7701370' } }, 6 | two: { data: { name: 'String3485322' } }, 7 | }, 8 | }); 9 | 10 | export type StandardScenario = typeof standard; 11 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/services/teams/teams.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | QueryResolvers, 3 | MutationResolvers, 4 | TeamResolvers, 5 | } from 'types/graphql'; 6 | 7 | import { db } from 'src/lib/db'; 8 | 9 | export const teams: QueryResolvers['teams'] = () => { 10 | return db.team.findMany(); 11 | }; 12 | 13 | export const team: QueryResolvers['team'] = ({ id }) => { 14 | return db.team.findUnique({ 15 | where: { id }, 16 | }); 17 | }; 18 | 19 | export const createTeam: MutationResolvers['createTeam'] = ({ input }) => { 20 | return db.team.create({ 21 | data: input, 22 | }); 23 | }; 24 | 25 | export const updateTeam: MutationResolvers['updateTeam'] = ({ id, input }) => { 26 | return db.team.update({ 27 | data: input, 28 | where: { id }, 29 | }); 30 | }; 31 | 32 | export const deleteTeam: MutationResolvers['deleteTeam'] = ({ id }) => { 33 | return db.team.delete({ 34 | where: { id }, 35 | }); 36 | }; 37 | 38 | export const Team: TeamResolvers = { 39 | id: (_obj, { root }) => root.id, 40 | name: (_obj, { root }) => root.name, 41 | logoUrl: (_obj, { root }) => root.logoUrl, 42 | Prediction: (_obj, { root }) => 43 | db.team.findUnique({ where: { id: root.id } }).Prediction(), 44 | homeTeamGames: (_obj, { root }) => 45 | db.team.findUnique({ where: { id: root.id } }).homeTeamGames(), 46 | awayTeamGames: (_obj, { root }) => 47 | db.team.findUnique({ where: { id: root.id } }).awayTeamGames(), 48 | }; 49 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/services/users/users.scenarios.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from '@prisma/client'; 2 | 3 | export const standard = defineScenario({ 4 | user: { 5 | one: { 6 | data: { 7 | username: 'test', 8 | email: 'String8843735', 9 | hashedPassword: 'String', 10 | salt: 'String', 11 | roles: 'String', 12 | }, 13 | }, 14 | two: { 15 | data: { 16 | username: 'test2', 17 | email: 'String9261536', 18 | hashedPassword: 'String', 19 | salt: 'String', 20 | roles: 'String', 21 | }, 22 | }, 23 | }, 24 | }); 25 | 26 | export type StandardScenario = typeof standard; 27 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/src/services/users/users.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto" 2 | 3 | import { addDays } from "date-fns" 4 | import nodemailer from "nodemailer" 5 | import type { MutationResolvers, QueryResolvers, UserResolvers } from "types/graphql" 6 | 7 | import { hashPassword } from "@redwoodjs/api" 8 | import { RedwoodGraphQLError } from "@redwoodjs/graphql-server" 9 | 10 | import { db } from "src/lib/db" 11 | import { generateResetPasswordToken } from "src/lib/generate-reset-password-token" 12 | 13 | const transporter = nodemailer.createTransport({ 14 | service: "gmail", 15 | auth: { 16 | user: process.env.EMAIL_USER, 17 | pass: process.env.EMAIL_PASS, 18 | }, 19 | }) 20 | 21 | export const users: QueryResolvers["users"] = () => { 22 | return db.user.findMany() 23 | } 24 | 25 | export const user: QueryResolvers["user"] = ({ id }) => { 26 | return db.user.findUnique({ 27 | where: { id }, 28 | }) 29 | } 30 | 31 | export const createUser: MutationResolvers["createUser"] = ({ input }) => { 32 | return db.user.create({ 33 | data: input, 34 | }) 35 | } 36 | 37 | export const updateUser: MutationResolvers["updateUser"] = ({ id, input }) => { 38 | return db.user.update({ 39 | data: input, 40 | where: { id }, 41 | }) 42 | } 43 | 44 | export const deleteUser: MutationResolvers["deleteUser"] = ({ id }) => { 45 | return db.user.delete({ 46 | where: { id }, 47 | }) 48 | } 49 | 50 | export const sendResetPasswordEmail: MutationResolvers["sendResetPasswordEmail"] = async ({ email }) => { 51 | const lowerCaseEmail = email.toLowerCase() 52 | console.log("Received ID to email to reset: " + lowerCaseEmail) 53 | 54 | const associatedUser = await db.user.findUnique({ 55 | where: { email: lowerCaseEmail }, 56 | }) 57 | 58 | if (!associatedUser) { 59 | console.log("Could not find associated user") 60 | return { 61 | success: false, 62 | message: "Could not find user with email: " + lowerCaseEmail, 63 | } 64 | } 65 | 66 | const resetToken = generateResetPasswordToken(16) 67 | 68 | await db.user.update({ 69 | where: { email: lowerCaseEmail }, 70 | data: { resetToken, resetTokenExpiresAt: addDays(new Date(), 1) }, 71 | }) 72 | 73 | console.log("Prepping mail options") 74 | const resetPasswordLink = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}` 75 | const mailOptions = { 76 | from: process.env.EMAIL_USER, 77 | to: lowerCaseEmail, 78 | subject: "Please Reset Your Passowrd", 79 | html: `You are receiveing this because you have requested to reseet your password at predictor.io. 80 | Please click on the following link, or paste this into your browser to complete the process:
81 | ${resetPasswordLink}

82 | 83 | If you did not request this, please respond to ${process.env.EMAIL_USER} and delete the email.`, 84 | } 85 | 86 | console.log("Sending reset password email") 87 | try { 88 | await transporter.sendMail(mailOptions) 89 | } catch (error) { 90 | const errorMessage = "An unknown error occurred when sending reset password email: " + error 91 | console.error(errorMessage) 92 | return { 93 | success: false, 94 | message: errorMessage, 95 | } 96 | } 97 | console.log("Reset password email sent") 98 | 99 | return { 100 | success: true, 101 | message: "An email has been sent to: " + lowerCaseEmail + ". Please check your inbox.", 102 | } 103 | } 104 | 105 | export const resetPassword: MutationResolvers["resetPassword"] = async ({ resetToken, password }) => { 106 | const associatedUser = await db.user.findFirst({ 107 | where: { resetToken }, 108 | }) 109 | 110 | if (associatedUser.resetToken !== resetToken) { 111 | throw new RedwoodGraphQLError("Could not find user with reset token.") 112 | } 113 | 114 | const salt = crypto.randomBytes(16).toString("base64") 115 | const [hashedPassword] = hashPassword(password, salt) 116 | 117 | return await db.user.update({ 118 | where: { 119 | id: associatedUser.id, 120 | }, 121 | data: { 122 | hashedPassword, 123 | salt, 124 | resetToken: null, 125 | }, 126 | }) 127 | } 128 | 129 | export const User: UserResolvers = { 130 | id: (_obj, { root }) => root.id, 131 | email: (_obj, { root }) => root.email, 132 | username: (_obj, { root }) => root.username, 133 | hashedPassword: (_obj, { root }) => root.hashedPassword, 134 | resetToken: (_obj, { root }) => root.resetToken, 135 | resetTokenExpiresAt: (_obj, { root }) => root.resetTokenExpiresAt, 136 | salt: (_obj, { root }) => root.salt, 137 | roles: (_obj, { root }) => root.roles, 138 | predictions: (_obj, { root }) => db.user.findUnique({ where: { id: root.id } }).predictions(), 139 | } 140 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "allowJs": true, 5 | "esModuleInterop": true, 6 | "target": "esnext", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "baseUrl": "./", 10 | "rootDirs": [ 11 | "./src", 12 | "../.redwood/types/mirror/api/src" 13 | ], 14 | "paths": { 15 | "src/*": [ 16 | "./src/*", 17 | "../.redwood/types/mirror/api/src/*" 18 | ], 19 | "types/*": ["./types/*", "../types/*"], 20 | "@redwoodjs/testing": ["../node_modules/@redwoodjs/testing/api"] 21 | }, 22 | "typeRoots": [ 23 | "../node_modules/@types", 24 | "./node_modules/@types" 25 | ], 26 | "types": ["jest"], 27 | }, 28 | "include": [ 29 | "src", 30 | "../.redwood/types/includes/all-*", 31 | "../.redwood/types/includes/api-*", 32 | "../types" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/tests/vendor/soccersage.io-main/license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mitchell Stark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/tsBuilder.ts: -------------------------------------------------------------------------------- 1 | import _generator from "@babel/generator" 2 | import * as _parser from "@babel/parser" 3 | import _traverse from "@babel/traverse" 4 | import { 5 | addComment, 6 | BlockStatement, 7 | Declaration, 8 | ExpressionStatement, 9 | Statement, 10 | TSType, 11 | TSTypeParameterDeclaration, 12 | } from "@babel/types" 13 | import * as _t from "@babel/types" 14 | 15 | interface InterfaceProperty { 16 | docs?: string 17 | name: string 18 | optional: boolean 19 | type: string 20 | } 21 | 22 | interface InterfaceCallSignature { 23 | docs?: string 24 | params: { name: string; optional?: boolean; type: string }[] 25 | returnType: string 26 | type: "call-signature" 27 | } 28 | 29 | interface NodeConfig { 30 | docs?: string 31 | exported?: boolean 32 | generics?: { name: string }[] 33 | } 34 | 35 | // Babel is a CJS package and uses `default` as named binding (`exports.default =`). 36 | // https://github.com/babel/babel/issues/15269. 37 | const generator = (_generator as any).default as typeof _generator || _generator 38 | const t = (_t as any).default as typeof _t || _t 39 | const parser = (_parser as any).default as typeof _parser || _parser 40 | const traverse = (_traverse as any).default as typeof _traverse || _traverse 41 | 42 | export const builder = (priorSource: string, _opts: {}) => { 43 | const sourceFile = parser.parse(priorSource, { sourceType: "module", plugins: ["jsx", "typescript"] }) 44 | 45 | /** Declares an import which should exist in the source document */ 46 | const setImport = (source: string, opts: { mainImport?: string; subImports?: string[] }) => { 47 | const imports = sourceFile.program.body.filter((s) => s.type === "ImportDeclaration") 48 | 49 | const existing = imports.find((i) => i.source.value === source) 50 | if (!existing) { 51 | const imports = [] as (_t.ImportSpecifier | _t.ImportDefaultSpecifier)[] 52 | if (opts.mainImport) { 53 | imports.push(t.importDefaultSpecifier(t.identifier(opts.mainImport))) 54 | } 55 | 56 | if (opts.subImports) { 57 | imports.push(...opts.subImports.map((si) => t.importSpecifier(t.identifier(si), t.identifier(si)))) 58 | } 59 | 60 | const importDeclaration = t.importDeclaration(imports, t.stringLiteral(source)) 61 | sourceFile.program.body.push(importDeclaration) 62 | return 63 | } 64 | 65 | if (!existing.specifiers.find((f) => f.type === "ImportDefaultSpecifier") && opts.mainImport) { 66 | existing.specifiers.push(t.importDefaultSpecifier(t.identifier(opts.mainImport))) 67 | } 68 | 69 | if (opts.subImports) { 70 | const existingImports = existing.specifiers.map((e) => e.local.name) 71 | const newImports = opts.subImports.filter((si) => !existingImports.includes(si)) 72 | 73 | if (newImports.length) { 74 | existing.specifiers.push(...newImports.map((si) => t.importSpecifier(t.identifier(si), t.identifier(si)))) 75 | } 76 | } 77 | } 78 | 79 | /** Allows creating a type alias via an AST parsed string */ 80 | const setTypeViaTemplate = (template: string) => { 81 | const type = parser.parse(template, { sourceType: "module", plugins: ["jsx", "typescript"] }) 82 | 83 | const typeDeclaration = type.program.body.find((s) => s.type === "TSTypeAliasDeclaration") 84 | if (!typeDeclaration) throw new Error("No type declaration found in template: " + template) 85 | 86 | const oldTypeDeclaration = sourceFile.program.body.find( 87 | (s) => s.type === "TSTypeAliasDeclaration" && s.id.name === typeDeclaration.id.name 88 | ) 89 | if (!oldTypeDeclaration) { 90 | sourceFile.program.body.push(typeDeclaration) 91 | return 92 | } 93 | 94 | if (!t.isTSTypeAliasDeclaration(oldTypeDeclaration)) throw new Error("Expected TSTypeAliasDeclaration") 95 | 96 | const newAnnotion = typeDeclaration.typeAnnotation 97 | 98 | // is literal primitive 99 | if (newAnnotion.type.endsWith("LiteralTypeAnnotation")) { 100 | oldTypeDeclaration.typeAnnotation = newAnnotion 101 | return 102 | } 103 | 104 | if (t.isTSTypeLiteral(newAnnotion) && t.isTSTypeLiteral(oldTypeDeclaration.typeAnnotation)) { 105 | for (const field of newAnnotion.members) { 106 | const matchingOnOld = oldTypeDeclaration.typeAnnotation.members.find((mm) => { 107 | if (!t.isTSPropertySignature(mm) || !t.isTSPropertySignature(field)) return false 108 | if (!t.isIdentifier(mm.key) || !t.isIdentifier(field.key)) return false 109 | return mm.key.name === field.key.name 110 | }) 111 | 112 | if (matchingOnOld) { 113 | matchingOnOld.typeAnnotation = field.typeAnnotation 114 | } else { 115 | oldTypeDeclaration.typeAnnotation.members.push(field) 116 | } 117 | } 118 | 119 | return 120 | } 121 | 122 | // @ts-expect-error - ts/js babel interop issue 123 | const code = generator(newAnnotion).code 124 | throw new Error(`Unsupported type annotation: ${newAnnotion.type} - ${code}`) 125 | } 126 | 127 | /** An internal API for describing a new area for inputting template info */ 128 | const createScope = (scopeName: string, scopeNode: _t.Node, statements: Statement[]) => { 129 | const addFunction = (name: string) => { 130 | let functionNode = statements.find( 131 | (s) => t.isVariableDeclaration(s) && t.isIdentifier(s.declarations[0].id) && s.declarations[0].id.name === name 132 | ) as _t.VariableDeclaration | undefined 133 | 134 | if (!functionNode) { 135 | functionNode = t.variableDeclaration("const", [ 136 | t.variableDeclarator(t.identifier(name), t.arrowFunctionExpression([], t.blockStatement([]))), 137 | ]) 138 | statements.push(functionNode) 139 | } 140 | 141 | const arrowFn = functionNode.declarations[0].init as _t.ArrowFunctionExpression 142 | if (!t.isArrowFunctionExpression(arrowFn)) throw new Error("Expected ArrowFunctionExpression") 143 | 144 | return { 145 | node: arrowFn, 146 | addParam: (name: string, type: string) => { 147 | const param = t.identifier(name) 148 | 149 | const fromParse = getTypeLevelAST(type) 150 | param.typeAnnotation = t.tsTypeAnnotation(fromParse) 151 | 152 | const exists = arrowFn.params.find((p) => p.type === "Identifier" && p.name === name) 153 | if (!exists) arrowFn.params.push(param) 154 | else exists.typeAnnotation = param.typeAnnotation 155 | }, 156 | 157 | scope: createScope(name, arrowFn, (arrowFn.body as BlockStatement).body), 158 | } 159 | } 160 | 161 | const addVariableDeclaration = (name: string, add: (prior: _t.Expression | undefined) => _t.Expression) => { 162 | const prior = statements.find( 163 | (b) => t.isVariableDeclaration(b) && t.isIdentifier(b.declarations[0].id) && b.declarations[0].id.name === name 164 | ) 165 | 166 | if (prior && t.isVariableDeclaration(prior) && t.isVariableDeclarator(prior.declarations[0]) && prior.declarations[0].init) { 167 | prior.declarations[0].init = add(prior.declarations[0].init) 168 | return 169 | } 170 | 171 | const declaration = t.variableDeclaration("const", [t.variableDeclarator(t.identifier(name), add(undefined))]) 172 | statements.push(declaration) 173 | } 174 | 175 | const addTypeAlias = (name: string, type: "any" | "string" | TSType, nodeConfig?: NodeConfig) => { 176 | const prior = statements.find( 177 | (s) => 178 | (t.isTSTypeAliasDeclaration(s) && s.id.name === name) || 179 | (t.isExportNamedDeclaration(s) && t.isTSTypeAliasDeclaration(s.declaration) && s.declaration.id.name === name) 180 | ) 181 | if (prior) return 182 | 183 | // Allow having some easy literals 184 | let typeNode = null 185 | if (typeof type === "string") { 186 | if (type === "any") typeNode = t.tsAnyKeyword() 187 | if (type === "string") typeNode = t.tsStringKeyword() 188 | } else { 189 | typeNode = type 190 | } 191 | 192 | if (!typeNode) throw new Error("Unknown type") 193 | const alias = t.tsTypeAliasDeclaration(t.identifier(name), null, typeNode) 194 | const statement = nodeFromNodeConfig(alias, nodeConfig) 195 | statements.push(statement) 196 | 197 | return alias 198 | } 199 | 200 | const addInterface = (name: string, fields: (InterfaceCallSignature | InterfaceProperty)[], nodeConfig?: NodeConfig) => { 201 | const prior = statements.find( 202 | (s) => 203 | (t.isTSInterfaceDeclaration(s) && s.id.name === name) || 204 | (t.isExportNamedDeclaration(s) && t.isTSInterfaceDeclaration(s.declaration) && s.declaration.id.name === name) 205 | ) 206 | 207 | if (prior) { 208 | if (t.isTSInterfaceDeclaration(prior)) return prior 209 | if (t.isExportNamedDeclaration(prior) && t.isTSInterfaceDeclaration(prior.declaration)) return prior.declaration 210 | throw new Error("Unknown state") 211 | } 212 | 213 | const body = t.tsInterfaceBody( 214 | fields.map((f) => { 215 | // Allow call signatures 216 | if (!("name" in f) && f.type === "call-signature") { 217 | const sig = t.tsCallSignatureDeclaration( 218 | null, // generics 219 | f.params.map((p) => { 220 | const i = t.identifier(p.name) 221 | i.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier(p.type))) 222 | if (p.optional) i.optional = true 223 | return i 224 | }), 225 | t.tsTypeAnnotation(t.tsTypeReference(t.identifier(f.returnType))) 226 | ) 227 | return sig 228 | } else { 229 | const prop = t.tsPropertySignature(t.identifier(f.name), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(f.type)))) 230 | prop.optional = f.optional 231 | if (f.docs?.length) t.addComment(prop, "leading", " " + f.docs) 232 | return prop 233 | } 234 | }) 235 | ) 236 | 237 | const interfaceDec = t.tsInterfaceDeclaration(t.identifier(name), null, null, body) 238 | const statement = nodeFromNodeConfig(interfaceDec, nodeConfig) 239 | statements.push(statement) 240 | return interfaceDec 241 | } 242 | 243 | const addLeadingComment = (comment: string) => { 244 | const firstStatement = statements[0] || scopeNode 245 | if (firstStatement) { 246 | if (firstStatement.leadingComments?.find((c) => c.value === comment)) return 247 | t.addComment(firstStatement, "leading", comment) 248 | } else { 249 | t.addComment(scopeNode, "leading", comment) 250 | } 251 | } 252 | 253 | return { 254 | addFunction, 255 | addVariableDeclaration, 256 | addTypeAlias, 257 | addInterface, 258 | addLeadingComment, 259 | } 260 | } 261 | 262 | /** Experimental function for parsing out a graphql template tag, and ensuring certain fields have been called */ 263 | const updateGraphQLTemplateTag = (expression: _t.Expression, path: string, modelFields: string[]) => { 264 | if (path !== ".") throw new Error("Only support updating the root of the graphql tag ATM") 265 | // @ts-expect-error - ts/js babel interop issue 266 | traverse( 267 | expression, 268 | { 269 | TaggedTemplateExpression(path: _traverse.NodePath<_t.TaggedTemplateExpression>) { 270 | const { tag, quasi } = path.node 271 | if (t.isIdentifier(tag) && tag.name === "graphql") { 272 | // This is the graphql query 273 | const query = quasi.quasis[0].value.raw 274 | const inner = query.match(/\{(.*)\}/)?.[1] 275 | if (inner === undefined) throw new Error("Could not find inner query") 276 | 277 | path.replaceWithSourceString(`graphql\`${query.replace(inner, `${inner}, ${modelFields.join(", ")}`)}\``) 278 | path.stop() 279 | } 280 | }, 281 | }, 282 | // Uh oh, not really sure what a Scope object does here 283 | {} as any 284 | ) 285 | return expression 286 | } 287 | 288 | const parseStatement = (code: string) => 289 | parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] }).program.body[0] as ExpressionStatement 290 | 291 | // @ts-expect-error - ts/js babel interop issue 292 | const getResult = () => generator(sourceFile.program, {}).code 293 | 294 | const rootScope = createScope("root", sourceFile, sourceFile.program.body) 295 | return { setImport, getResult, setTypeViaTemplate, parseStatement, updateGraphQLTemplateTag, rootScope } 296 | } 297 | 298 | /** Parses something as though it is in type-space and extracts the subset of the AST that the string represents */ 299 | const getTypeLevelAST = (type: string) => { 300 | const typeAST = parser.parse(`type A = ${type}`, { sourceType: "module", plugins: ["jsx", "typescript"] }) 301 | const typeDeclaration = typeAST.program.body.find((s) => t.isTSTypeAliasDeclaration(s)) 302 | if (!typeDeclaration) throw new Error("No type declaration found in template: " + type) 303 | return typeDeclaration.typeAnnotation 304 | } 305 | 306 | export type TSBuilder = ReturnType 307 | 308 | /** A little helper to handle all the extras for */ 309 | const nodeFromNodeConfig = ( 310 | node: T, 311 | nodeConfig?: NodeConfig 312 | ) => { 313 | const statement = nodeConfig?.exported ? t.exportNamedDeclaration(node) : node 314 | if (nodeConfig?.docs) addComment(statement, "leading", nodeConfig.docs) 315 | if (nodeConfig?.generics && nodeConfig.generics.length > 0) { 316 | node.typeParameters = t.tsTypeParameterDeclaration(nodeConfig.generics.map((g) => t.tsTypeParameter(null, null, g.name))) 317 | } 318 | 319 | return statement 320 | } 321 | -------------------------------------------------------------------------------- /src/typeFacts.ts: -------------------------------------------------------------------------------- 1 | export type FieldFacts = Record 2 | 3 | export interface FieldFact { 4 | hasResolverImplementation?: true 5 | // isPrismaBacked?: true; 6 | } 7 | 8 | // The data-model for the service file which contains the SDL matched functions 9 | 10 | /** A representation of the code inside the source file's */ 11 | export type CodeFacts = Record 12 | 13 | export interface ModelResolverFacts { 14 | /** Should we type the type as a generic with an override */ 15 | hasGenericArg: boolean 16 | /** Individual resolvers found for this model */ 17 | resolvers: Map 18 | /** The name (or lack of) for the GraphQL type which we are mapping */ 19 | typeName: string | "maybe_query_mutation" 20 | } 21 | 22 | export interface ResolverFuncFact { 23 | /** How many args are defined? */ 24 | funcArgCount: number 25 | /** The type of the fn */ 26 | infoParamType?: "all" | "just_root_destructured" 27 | /** Is it declared as an async fn */ 28 | isAsync: boolean 29 | /** is 'function abc() {}' */ 30 | isFunc: boolean 31 | /** is 'const ABC = {}' */ 32 | isObjLiteral: boolean 33 | /** We don't know what declaration is */ 34 | isUnknown: boolean 35 | /** The name of the fn */ 36 | name: string 37 | } 38 | -------------------------------------------------------------------------------- /src/typeMap.ts: -------------------------------------------------------------------------------- 1 | import * as graphql from "graphql" 2 | 3 | import { AppContext } from "./context.js" 4 | 5 | export type TypeMapper = ReturnType 6 | 7 | export const typeMapper = (context: AppContext, config: { preferPrismaModels?: true }) => { 8 | const referencedGraphQLTypes = new Set() 9 | const referencedPrismaModels = new Set() 10 | const customScalars = new Set() 11 | 12 | const clear = () => { 13 | referencedGraphQLTypes.clear() 14 | customScalars.clear() 15 | referencedPrismaModels.clear() 16 | } 17 | 18 | const getReferencedGraphQLThingsInMapping = () => { 19 | return { 20 | types: [...referencedGraphQLTypes.keys()], 21 | scalars: [...customScalars.keys()], 22 | prisma: [...referencedPrismaModels.keys()], 23 | } 24 | } 25 | 26 | const map = ( 27 | type: graphql.GraphQLType, 28 | mapConfig: { 29 | parentWasNotNull?: true 30 | preferNullOverUndefined?: true 31 | typenamePrefix?: string 32 | } 33 | ): string | undefined => { 34 | const prefix = mapConfig.typenamePrefix ?? "" 35 | 36 | // The AST for GQL uses a parent node to indicate the !, we need the opposite 37 | // for TS which uses '| undefined' after. 38 | if (graphql.isNonNullType(type)) { 39 | return map(type.ofType, { parentWasNotNull: true, ...mapConfig }) 40 | } 41 | 42 | // So we can add the | undefined 43 | const getInner = () => { 44 | if (graphql.isListType(type)) { 45 | const typeStr = map(type.ofType, mapConfig) 46 | if (!typeStr) return "any" 47 | 48 | if (graphql.isNonNullType(type.ofType)) { 49 | // If its a union type, we need to wrap it in brackets 50 | // so that the [] is not applied to the last union itemu 51 | if (typeStr.includes("|")) { 52 | return `(${typeStr})[]` 53 | } 54 | 55 | return `${typeStr}[]` 56 | } else { 57 | return `Array<${typeStr}>` 58 | } 59 | } 60 | 61 | if (graphql.isScalarType(type)) { 62 | switch (type.toString()) { 63 | case "Int": 64 | return "number" 65 | case "Float": 66 | return "number" 67 | case "String": 68 | return "string" 69 | case "Boolean": 70 | return "boolean" 71 | } 72 | 73 | customScalars.add(type.name) 74 | return type.name 75 | } 76 | 77 | if (graphql.isObjectType(type)) { 78 | if (config.preferPrismaModels && context.prisma.has(type.name)) { 79 | referencedPrismaModels.add(type.name) 80 | return "P" + type.name 81 | } else { 82 | // GraphQL only type 83 | referencedGraphQLTypes.add(type.name) 84 | return prefix + type.name 85 | } 86 | } 87 | 88 | if (graphql.isInterfaceType(type)) { 89 | referencedGraphQLTypes.add(type.name) 90 | return prefix + type.name 91 | } 92 | 93 | if (graphql.isUnionType(type)) { 94 | const types = type.getTypes() 95 | referencedGraphQLTypes.add(type.name) 96 | return types.map((t) => map(t, mapConfig)).join(" | ") 97 | } 98 | 99 | if (graphql.isEnumType(type)) { 100 | referencedGraphQLTypes.add(type.name) 101 | return prefix + type.name 102 | } 103 | 104 | if (graphql.isInputObjectType(type)) { 105 | referencedGraphQLTypes.add(type.name) 106 | 107 | return prefix + type.name 108 | } 109 | 110 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions 111 | throw new Error(`Unknown type ${type} - ${JSON.stringify(type, null, 2)}`) 112 | } 113 | 114 | const suffix = mapConfig.parentWasNotNull ? "" : mapConfig.preferNullOverUndefined ? "| null" : " | undefined" 115 | return getInner() + suffix 116 | } 117 | 118 | return { map, clear, getReferencedGraphQLThingsInMapping } 119 | } 120 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { System } from "typescript" 2 | 3 | export interface SDLCodeGenOptions { 4 | /** We'll use the one which comes with TypeScript if one isn't given */ 5 | system?: System 6 | } 7 | 8 | // These are directly ported from Redwood at 9 | // packages/project-config/src/paths.ts 10 | 11 | // Slightly amended to reduce the constraints on the Redwood team to make changes to this obj! 12 | 13 | interface NodeTargetPaths { 14 | base: string 15 | config: string 16 | // dataMigrations: string 17 | // db: string 18 | dbSchema: string 19 | directives: string 20 | // dist: string 21 | // functions: string 22 | // generators: string 23 | graphql: string 24 | lib: string 25 | models: string 26 | services: string 27 | src: string 28 | types: string 29 | } 30 | 31 | export interface RedwoodPaths { 32 | api: NodeTargetPaths 33 | base: string 34 | generated: { 35 | // base: string 36 | // prebuild: string 37 | schema: string 38 | // types: { 39 | // includes: string 40 | // mirror: string 41 | // } 42 | } 43 | scripts: string 44 | web: object 45 | } 46 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as graphql from "graphql" 2 | import * as tsMorph from "ts-morph" 3 | 4 | import { TSBuilder } from "./tsBuilder.js" 5 | import { TypeMapper } from "./typeMap.js" 6 | 7 | export const varStartsWithUppercase = (v: tsMorph.VariableDeclaration) => v.getName()[0].startsWith(v.getName()[0].toUpperCase()) 8 | export const nameDoesNotStartsWithUnderscore = (v: tsMorph.VariableDeclaration) => !v.getName()[0].startsWith("_") 9 | 10 | export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1) 11 | 12 | export const variableDeclarationIsAsync = (vd: tsMorph.VariableDeclaration) => { 13 | const res = !!vd.getFirstChildByKind(tsMorph.SyntaxKind.AsyncKeyword) 14 | return res 15 | } 16 | 17 | export const inlineArgsForField = (field: graphql.GraphQLField, config: { mapper: TypeMapper["map"] }) => { 18 | return field.args.length 19 | ? // Always use an args obj 20 | `{${field.args 21 | .map((f) => { 22 | const type = config.mapper(f.type, {}) 23 | if (!type) throw new Error(`No type for ${f.name} on ${field.name}!`) 24 | 25 | const q = type.includes("undefined") ? "?" : "" 26 | const displayType = type.replace("| undefined", "") 27 | return `${f.name}${q}: ${displayType}` 28 | }) 29 | .join(", ")}}` 30 | : undefined 31 | } 32 | 33 | export const createAndReferOrInlineArgsForField = ( 34 | field: graphql.GraphQLField, 35 | config: { 36 | dts: TSBuilder 37 | mapper: TypeMapper["map"] 38 | name: string 39 | noSeparateType?: true 40 | } 41 | ) => { 42 | const inlineArgs = inlineArgsForField(field, config) 43 | if (!inlineArgs) return undefined 44 | if (inlineArgs.length < 120) return inlineArgs 45 | 46 | const dts = config.dts 47 | dts.rootScope.addInterface( 48 | `${config.name}Args`, 49 | field.args.map((a) => ({ 50 | name: a.name, 51 | type: config.mapper(a.type, {})!, 52 | optional: false, 53 | })) 54 | ) 55 | 56 | return `${config.name}Args` 57 | } 58 | 59 | export const makeStep = (verbose: boolean) => async (msg: string, fn: () => Promise | Promise | void) => { 60 | if (!verbose) return fn() 61 | console.log("[sdl-codegen] " + msg) 62 | console.time("[sdl-codegen] " + msg) 63 | await fn() 64 | console.timeEnd("[sdl-codegen] " + msg) 65 | } 66 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": true, 5 | "esModuleInterop": false, 6 | "module": "Node16", 7 | "moduleResolution": "node16", 8 | "outDir": "lib", 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "ES2021" 13 | }, 14 | "exclude": ["node_modules", "src/tests"], 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({}) 4 | --------------------------------------------------------------------------------