├── .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