├── .changeset ├── README.md └── config.json ├── .github └── workflows │ ├── release.yml │ └── tests.yaml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── examples ├── codegen-gql-files │ ├── mercurius-schema.json │ ├── package.json │ ├── src │ │ ├── graphql │ │ │ ├── generated.ts │ │ │ ├── operations │ │ │ │ └── example.gql │ │ │ └── schema │ │ │ │ ├── dog.gql │ │ │ │ ├── human.gql │ │ │ │ ├── mutation.gql │ │ │ │ ├── query.gql │ │ │ │ └── subscription.gql │ │ ├── index.ts │ │ └── listen.ts │ ├── test │ │ ├── example.test.ts │ │ └── tsconfig.json │ └── tsconfig.json ├── codegen │ ├── package.json │ ├── src │ │ ├── graphql │ │ │ ├── generated.ts │ │ │ └── operations │ │ │ │ └── example.gql │ │ ├── index.ts │ │ └── listen.ts │ ├── test │ │ ├── example.test.ts │ │ └── tsconfig.json │ └── tsconfig.json └── manual │ ├── package.json │ ├── src │ ├── index.ts │ └── listen.ts │ ├── test │ ├── app.test.ts │ └── tsconfig.json │ └── tsconfig.json ├── package.json ├── packages └── mercurius-codegen │ ├── .nycrc.json │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── ava.config.cjs │ ├── package.json │ ├── src │ ├── code.ts │ ├── index.ts │ ├── mercuriusLoaders.ts │ ├── outputSchema.ts │ ├── prettier.ts │ ├── schema.ts │ ├── utils.ts │ └── write.ts │ ├── test │ ├── generated.ts │ ├── index.test.ts │ ├── operations │ │ └── hello.gql │ ├── snapshots │ │ ├── index.test.ts.md │ │ └── index.test.ts.snap │ └── tsconfig.json │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── renovate.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "master", 8 | "updateInternalDependencies": "patch", 9 | "ignore": ["example-*"], 10 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 11 | "onlyUpdatePeerDependentsWhenOutOfRange": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout Main 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: Use Node 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: '20.x' 18 | - name: Cache pnpm modules 19 | uses: actions/cache@v4 20 | env: 21 | cache-name: cache-pnpm-modules 22 | with: 23 | path: ~/.pnpm-store 24 | key: ${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }} 25 | restore-keys: | 26 | ${{ runner.os }}- 27 | - name: install pnpm 28 | run: npm i pnpm@latest -g 29 | - name: Install Dependencies 30 | run: pnpm i 31 | - name: Create Release Pull Request or Publish to npm 32 | id: changesets 33 | uses: changesets/action@v1 34 | with: 35 | publish: pnpm ci:release 36 | version: pnpm ci:version 37 | commit: 'chore(release): update monorepo packages versions' 38 | title: 'Upcoming Release Changes' 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build-and-test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node-version: [18, 20] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | - name: Setup node 16 | uses: actions/setup-node@v2-beta 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm install -g pnpm@latest 20 | - run: pnpm i 21 | - run: pnpm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | #custom 107 | 108 | tmp -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | .nyc_output 3 | coverage 4 | dist 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pablo Sáez 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mercurius-typescript 2 | 3 | ## Examples 4 | 5 | All the examples are available in the [examples folder](./examples/) 6 | 7 | ## Libraries 8 | 9 | - [Mercurius Codegen](./packages/mercurius-codegen/) - Integration with [GraphQL Code Generator](https://graphql-code-generator.com/) to get automatic type-safety and autocompletion. 10 | 11 | ## Contributing 12 | 13 | This project uses [pnpm workspaces](https://pnpm.io/workspaces) 14 | 15 | But the examples are just basic Node.js packages, but for development and to test them yourself, you will need to [install it](https://pnpm.io/installation) and then execute 16 | 17 | ```bash 18 | pnpm i 19 | ``` 20 | 21 | To install all the required dependencies 22 | 23 | And then execute: 24 | 25 | ```bash 26 | pnpm test 27 | ``` 28 | 29 | To tests all the projects 30 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/mercurius-schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | "type Dog {\n name: String!\n owner: Human\n}", 3 | "type Human {\n name: String!\n}", 4 | "type Mutation {\n add(x: Int!, y: Int!): Int!\n createNotification(message: String!): Boolean!\n}", 5 | "type Query {\n Hello: String!\n dogs: [Dog!]!\n}", 6 | "type Subscription {\n newNotification: String!\n}" 7 | ] 8 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-codegen-gql-files", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Mercurius + GraphQL Code Generator example", 6 | "keywords": [ 7 | "graphql-codegen" 8 | ], 9 | "license": "MIT", 10 | "author": "PabloSz", 11 | "main": "dist/index.js", 12 | "scripts": { 13 | "dev": "bob-tsm --node-env=dev --cjs --watch=src src/listen.ts", 14 | "start": "bob-tsm --node-env=prod --cjs src/listen.ts", 15 | "test": "cross-env NODE_ENV=test tap --node-arg=--require=bob-tsm" 16 | }, 17 | "dependencies": { 18 | "@graphql-tools/load-files": "^7.0.1", 19 | "fastify": "^5.2.1", 20 | "graphql": "^16.10.0", 21 | "mercurius": "^16.0.1", 22 | "mercurius-codegen": "workspace:*" 23 | }, 24 | "devDependencies": { 25 | "@graphql-typed-document-node/core": "^3.2.0", 26 | "@types/node": "^22.13.0", 27 | "@types/tap": "^18.0.0", 28 | "bob-tsm": "^1.1.2", 29 | "cross-env": "^7.0.3", 30 | "esbuild": "^0.24.2", 31 | "mercurius-integration-testing": "^9.0.1", 32 | "prettier": "^3.4.2", 33 | "tap": "^16.3.10", 34 | "typescript": "^5.7.3" 35 | }, 36 | "tap": { 37 | "check-coverage": false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/src/graphql/generated.ts: -------------------------------------------------------------------------------- 1 | import type { GraphQLResolveInfo } from 'graphql' 2 | import type { MercuriusContext } from 'mercurius' 3 | import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' 4 | export type Maybe = T | null 5 | export type InputMaybe = Maybe 6 | export type Exact = { 7 | [K in keyof T]: T[K] 8 | } 9 | export type MakeOptional = Omit & { 10 | [SubKey in K]?: Maybe 11 | } 12 | export type MakeMaybe = Omit & { 13 | [SubKey in K]: Maybe 14 | } 15 | export type ResolverFn = ( 16 | parent: TParent, 17 | args: TArgs, 18 | context: TContext, 19 | info: GraphQLResolveInfo, 20 | ) => 21 | | Promise> 22 | | import('mercurius-codegen').DeepPartial 23 | export type RequireFields = Omit & { 24 | [P in K]-?: NonNullable 25 | } 26 | /** All built-in and custom scalars, mapped to their actual values */ 27 | export type Scalars = { 28 | ID: string 29 | String: string 30 | Boolean: boolean 31 | Int: number 32 | Float: number 33 | _FieldSet: any 34 | } 35 | 36 | export type Dog = { 37 | __typename?: 'Dog' 38 | name: Scalars['String'] 39 | owner?: Maybe 40 | } 41 | 42 | export type Human = { 43 | __typename?: 'Human' 44 | name: Scalars['String'] 45 | } 46 | 47 | export type Mutation = { 48 | __typename?: 'Mutation' 49 | add: Scalars['Int'] 50 | createNotification: Scalars['Boolean'] 51 | } 52 | 53 | export type MutationaddArgs = { 54 | x: Scalars['Int'] 55 | y: Scalars['Int'] 56 | } 57 | 58 | export type MutationcreateNotificationArgs = { 59 | message: Scalars['String'] 60 | } 61 | 62 | export type Query = { 63 | __typename?: 'Query' 64 | Hello: Scalars['String'] 65 | dogs: Array 66 | } 67 | 68 | export type Subscription = { 69 | __typename?: 'Subscription' 70 | newNotification: Scalars['String'] 71 | } 72 | 73 | export type ResolverTypeWrapper = Promise | T 74 | 75 | export type ResolverWithResolve = { 76 | resolve: ResolverFn 77 | } 78 | export type Resolver = 79 | | ResolverFn 80 | | ResolverWithResolve 81 | 82 | export type SubscriptionSubscribeFn = ( 83 | parent: TParent, 84 | args: TArgs, 85 | context: TContext, 86 | info: GraphQLResolveInfo, 87 | ) => AsyncIterable | Promise> 88 | 89 | export type SubscriptionResolveFn = ( 90 | parent: TParent, 91 | args: TArgs, 92 | context: TContext, 93 | info: GraphQLResolveInfo, 94 | ) => TResult | Promise 95 | 96 | export interface SubscriptionSubscriberObject< 97 | TResult, 98 | TKey extends string, 99 | TParent, 100 | TContext, 101 | TArgs, 102 | > { 103 | subscribe: SubscriptionSubscribeFn< 104 | { [key in TKey]: TResult }, 105 | TParent, 106 | TContext, 107 | TArgs 108 | > 109 | resolve?: SubscriptionResolveFn< 110 | TResult, 111 | { [key in TKey]: TResult }, 112 | TContext, 113 | TArgs 114 | > 115 | } 116 | 117 | export interface SubscriptionResolverObject { 118 | subscribe: SubscriptionSubscribeFn 119 | resolve: SubscriptionResolveFn 120 | } 121 | 122 | export type SubscriptionObject< 123 | TResult, 124 | TKey extends string, 125 | TParent, 126 | TContext, 127 | TArgs, 128 | > = 129 | | SubscriptionSubscriberObject 130 | | SubscriptionResolverObject 131 | 132 | export type SubscriptionResolver< 133 | TResult, 134 | TKey extends string, 135 | TParent = {}, 136 | TContext = {}, 137 | TArgs = {}, 138 | > = 139 | | (( 140 | ...args: any[] 141 | ) => SubscriptionObject) 142 | | SubscriptionObject 143 | 144 | export type TypeResolveFn = ( 145 | parent: TParent, 146 | context: TContext, 147 | info: GraphQLResolveInfo, 148 | ) => Maybe | Promise> 149 | 150 | export type IsTypeOfResolverFn = ( 151 | obj: T, 152 | context: TContext, 153 | info: GraphQLResolveInfo, 154 | ) => boolean | Promise 155 | 156 | export type NextResolverFn = () => Promise 157 | 158 | export type DirectiveResolverFn< 159 | TResult = {}, 160 | TParent = {}, 161 | TContext = {}, 162 | TArgs = {}, 163 | > = ( 164 | next: NextResolverFn, 165 | parent: TParent, 166 | args: TArgs, 167 | context: TContext, 168 | info: GraphQLResolveInfo, 169 | ) => TResult | Promise 170 | 171 | /** Mapping between all available schema types and the resolvers types */ 172 | export type ResolversTypes = { 173 | Dog: ResolverTypeWrapper 174 | String: ResolverTypeWrapper 175 | Human: ResolverTypeWrapper 176 | Mutation: ResolverTypeWrapper<{}> 177 | Int: ResolverTypeWrapper 178 | Boolean: ResolverTypeWrapper 179 | Query: ResolverTypeWrapper<{}> 180 | Subscription: ResolverTypeWrapper<{}> 181 | } 182 | 183 | /** Mapping between all available schema types and the resolvers parents */ 184 | export type ResolversParentTypes = { 185 | Dog: Dog 186 | String: Scalars['String'] 187 | Human: Human 188 | Mutation: {} 189 | Int: Scalars['Int'] 190 | Boolean: Scalars['Boolean'] 191 | Query: {} 192 | Subscription: {} 193 | } 194 | 195 | export type DogResolvers< 196 | ContextType = MercuriusContext, 197 | ParentType extends ResolversParentTypes['Dog'] = ResolversParentTypes['Dog'], 198 | > = { 199 | name?: Resolver 200 | owner?: Resolver, ParentType, ContextType> 201 | isTypeOf?: IsTypeOfResolverFn 202 | } 203 | 204 | export type HumanResolvers< 205 | ContextType = MercuriusContext, 206 | ParentType extends 207 | ResolversParentTypes['Human'] = ResolversParentTypes['Human'], 208 | > = { 209 | name?: Resolver 210 | isTypeOf?: IsTypeOfResolverFn 211 | } 212 | 213 | export type MutationResolvers< 214 | ContextType = MercuriusContext, 215 | ParentType extends 216 | ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation'], 217 | > = { 218 | add?: Resolver< 219 | ResolversTypes['Int'], 220 | ParentType, 221 | ContextType, 222 | RequireFields 223 | > 224 | createNotification?: Resolver< 225 | ResolversTypes['Boolean'], 226 | ParentType, 227 | ContextType, 228 | RequireFields 229 | > 230 | } 231 | 232 | export type QueryResolvers< 233 | ContextType = MercuriusContext, 234 | ParentType extends 235 | ResolversParentTypes['Query'] = ResolversParentTypes['Query'], 236 | > = { 237 | Hello?: Resolver 238 | dogs?: Resolver, ParentType, ContextType> 239 | } 240 | 241 | export type SubscriptionResolvers< 242 | ContextType = MercuriusContext, 243 | ParentType extends 244 | ResolversParentTypes['Subscription'] = ResolversParentTypes['Subscription'], 245 | > = { 246 | newNotification?: SubscriptionResolver< 247 | ResolversTypes['String'], 248 | 'newNotification', 249 | ParentType, 250 | ContextType 251 | > 252 | } 253 | 254 | export type Resolvers = { 255 | Dog?: DogResolvers 256 | Human?: HumanResolvers 257 | Mutation?: MutationResolvers 258 | Query?: QueryResolvers 259 | Subscription?: SubscriptionResolvers 260 | } 261 | 262 | export type Loader = ( 263 | queries: Array<{ 264 | obj: TObj 265 | params: TParams 266 | }>, 267 | context: TContext & { 268 | reply: import('fastify').FastifyReply 269 | }, 270 | ) => Promise>> 271 | export type LoaderResolver = 272 | | Loader 273 | | { 274 | loader: Loader 275 | opts?: { 276 | cache?: boolean 277 | } 278 | } 279 | export interface Loaders< 280 | TContext = import('mercurius').MercuriusContext & { 281 | reply: import('fastify').FastifyReply 282 | }, 283 | > { 284 | Dog?: { 285 | name?: LoaderResolver 286 | owner?: LoaderResolver, Dog, {}, TContext> 287 | } 288 | 289 | Human?: { 290 | name?: LoaderResolver 291 | } 292 | } 293 | export type helloQueryVariables = Exact<{ [key: string]: never }> 294 | 295 | export type helloQuery = { __typename?: 'Query'; Hello: string } 296 | 297 | export type addMutationVariables = Exact<{ 298 | x: Scalars['Int'] 299 | y: Scalars['Int'] 300 | }> 301 | 302 | export type addMutation = { __typename?: 'Mutation'; add: number } 303 | 304 | export type dogsQueryVariables = Exact<{ [key: string]: never }> 305 | 306 | export type dogsQuery = { 307 | __typename?: 'Query' 308 | dogs: Array<{ 309 | __typename?: 'Dog' 310 | name: string 311 | owner?: { __typename?: 'Human'; name: string } | null 312 | }> 313 | } 314 | 315 | export type createNotificationMutationVariables = Exact<{ 316 | message: Scalars['String'] 317 | }> 318 | 319 | export type createNotificationMutation = { 320 | __typename?: 'Mutation' 321 | createNotification: boolean 322 | } 323 | 324 | export type newNotificationSubscriptionVariables = Exact<{ 325 | [key: string]: never 326 | }> 327 | 328 | export type newNotificationSubscription = { 329 | __typename?: 'Subscription' 330 | newNotification: string 331 | } 332 | 333 | export const helloDocument = { 334 | kind: 'Document', 335 | definitions: [ 336 | { 337 | kind: 'OperationDefinition', 338 | operation: 'query', 339 | name: { kind: 'Name', value: 'hello' }, 340 | selectionSet: { 341 | kind: 'SelectionSet', 342 | selections: [{ kind: 'Field', name: { kind: 'Name', value: 'Hello' } }], 343 | }, 344 | }, 345 | ], 346 | } as unknown as DocumentNode 347 | export const addDocument = { 348 | kind: 'Document', 349 | definitions: [ 350 | { 351 | kind: 'OperationDefinition', 352 | operation: 'mutation', 353 | name: { kind: 'Name', value: 'add' }, 354 | variableDefinitions: [ 355 | { 356 | kind: 'VariableDefinition', 357 | variable: { kind: 'Variable', name: { kind: 'Name', value: 'x' } }, 358 | type: { 359 | kind: 'NonNullType', 360 | type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, 361 | }, 362 | }, 363 | { 364 | kind: 'VariableDefinition', 365 | variable: { kind: 'Variable', name: { kind: 'Name', value: 'y' } }, 366 | type: { 367 | kind: 'NonNullType', 368 | type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, 369 | }, 370 | }, 371 | ], 372 | selectionSet: { 373 | kind: 'SelectionSet', 374 | selections: [ 375 | { 376 | kind: 'Field', 377 | name: { kind: 'Name', value: 'add' }, 378 | arguments: [ 379 | { 380 | kind: 'Argument', 381 | name: { kind: 'Name', value: 'x' }, 382 | value: { kind: 'Variable', name: { kind: 'Name', value: 'x' } }, 383 | }, 384 | { 385 | kind: 'Argument', 386 | name: { kind: 'Name', value: 'y' }, 387 | value: { kind: 'Variable', name: { kind: 'Name', value: 'y' } }, 388 | }, 389 | ], 390 | }, 391 | ], 392 | }, 393 | }, 394 | ], 395 | } as unknown as DocumentNode 396 | export const dogsDocument = { 397 | kind: 'Document', 398 | definitions: [ 399 | { 400 | kind: 'OperationDefinition', 401 | operation: 'query', 402 | name: { kind: 'Name', value: 'dogs' }, 403 | selectionSet: { 404 | kind: 'SelectionSet', 405 | selections: [ 406 | { 407 | kind: 'Field', 408 | name: { kind: 'Name', value: 'dogs' }, 409 | selectionSet: { 410 | kind: 'SelectionSet', 411 | selections: [ 412 | { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 413 | { 414 | kind: 'Field', 415 | name: { kind: 'Name', value: 'owner' }, 416 | selectionSet: { 417 | kind: 'SelectionSet', 418 | selections: [ 419 | { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 420 | ], 421 | }, 422 | }, 423 | ], 424 | }, 425 | }, 426 | ], 427 | }, 428 | }, 429 | ], 430 | } as unknown as DocumentNode 431 | export const createNotificationDocument = { 432 | kind: 'Document', 433 | definitions: [ 434 | { 435 | kind: 'OperationDefinition', 436 | operation: 'mutation', 437 | name: { kind: 'Name', value: 'createNotification' }, 438 | variableDefinitions: [ 439 | { 440 | kind: 'VariableDefinition', 441 | variable: { 442 | kind: 'Variable', 443 | name: { kind: 'Name', value: 'message' }, 444 | }, 445 | type: { 446 | kind: 'NonNullType', 447 | type: { 448 | kind: 'NamedType', 449 | name: { kind: 'Name', value: 'String' }, 450 | }, 451 | }, 452 | }, 453 | ], 454 | selectionSet: { 455 | kind: 'SelectionSet', 456 | selections: [ 457 | { 458 | kind: 'Field', 459 | name: { kind: 'Name', value: 'createNotification' }, 460 | arguments: [ 461 | { 462 | kind: 'Argument', 463 | name: { kind: 'Name', value: 'message' }, 464 | value: { 465 | kind: 'Variable', 466 | name: { kind: 'Name', value: 'message' }, 467 | }, 468 | }, 469 | ], 470 | }, 471 | ], 472 | }, 473 | }, 474 | ], 475 | } as unknown as DocumentNode< 476 | createNotificationMutation, 477 | createNotificationMutationVariables 478 | > 479 | export const newNotificationDocument = { 480 | kind: 'Document', 481 | definitions: [ 482 | { 483 | kind: 'OperationDefinition', 484 | operation: 'subscription', 485 | name: { kind: 'Name', value: 'newNotification' }, 486 | selectionSet: { 487 | kind: 'SelectionSet', 488 | selections: [ 489 | { kind: 'Field', name: { kind: 'Name', value: 'newNotification' } }, 490 | ], 491 | }, 492 | }, 493 | ], 494 | } as unknown as DocumentNode< 495 | newNotificationSubscription, 496 | newNotificationSubscriptionVariables 497 | > 498 | declare module 'mercurius' { 499 | interface IResolvers 500 | extends Resolvers {} 501 | interface MercuriusLoaders extends Loaders {} 502 | } 503 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/src/graphql/operations/example.gql: -------------------------------------------------------------------------------- 1 | query hello { 2 | Hello 3 | } 4 | 5 | mutation add($x: Int!, $y: Int!) { 6 | add(x: $x, y: $y) 7 | } 8 | 9 | query dogs { 10 | dogs { 11 | name 12 | owner { 13 | name 14 | } 15 | } 16 | } 17 | 18 | mutation createNotification($message: String!) { 19 | createNotification(message: $message) 20 | } 21 | 22 | subscription newNotification { 23 | newNotification 24 | } 25 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/src/graphql/schema/dog.gql: -------------------------------------------------------------------------------- 1 | type Dog { 2 | name: String! 3 | owner: Human 4 | } 5 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/src/graphql/schema/human.gql: -------------------------------------------------------------------------------- 1 | type Human { 2 | name: String! 3 | } 4 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/src/graphql/schema/mutation.gql: -------------------------------------------------------------------------------- 1 | type Mutation { 2 | add(x: Int!, y: Int!): Int! 3 | createNotification(message: String!): Boolean! 4 | } 5 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/src/graphql/schema/query.gql: -------------------------------------------------------------------------------- 1 | type Query { 2 | Hello: String! 3 | dogs: [Dog!]! 4 | } 5 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/src/graphql/schema/subscription.gql: -------------------------------------------------------------------------------- 1 | type Subscription { 2 | newNotification: String! 3 | } 4 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/src/index.ts: -------------------------------------------------------------------------------- 1 | import Fastify, { FastifyReply, FastifyRequest } from 'fastify' 2 | import { buildSchema } from 'graphql' 3 | import mercurius, { IResolvers, MercuriusLoaders } from 'mercurius' 4 | import { 5 | codegenMercurius, 6 | CodegenMercuriusOptions, 7 | loadSchemaFiles, 8 | } from 'mercurius-codegen' 9 | 10 | export const app = Fastify({ 11 | logger: true, 12 | }) 13 | 14 | const codegenMercuriusOptions: CodegenMercuriusOptions = { 15 | targetPath: './src/graphql/generated.ts', 16 | operationsGlob: './src/graphql/operations/*.gql', 17 | watchOptions: { 18 | enabled: process.env.NODE_ENV === 'development', 19 | }, 20 | } 21 | 22 | const { schema } = loadSchemaFiles('src/graphql/schema/**/*.gql', { 23 | watchOptions: { 24 | enabled: process.env.NODE_ENV === 'development', 25 | onChange(schema) { 26 | app.graphql.replaceSchema(buildSchema(schema.join('\n'))) 27 | app.graphql.defineResolvers(resolvers) 28 | 29 | codegenMercurius(app, codegenMercuriusOptions).catch(console.error) 30 | }, 31 | }, 32 | }) 33 | 34 | const buildContext = async (req: FastifyRequest, _reply: FastifyReply) => { 35 | return { 36 | authorization: req.headers.authorization, 37 | } 38 | } 39 | 40 | type PromiseType = T extends PromiseLike ? U : T 41 | 42 | declare module 'mercurius' { 43 | interface MercuriusContext 44 | extends PromiseType> {} 45 | } 46 | 47 | const dogs = [ 48 | { name: 'Max' }, 49 | { name: 'Charlie' }, 50 | { name: 'Buddy' }, 51 | { name: 'Max' }, 52 | ] 53 | 54 | const owners: Record = { 55 | Max: { 56 | name: 'Jennifer', 57 | }, 58 | Charlie: { 59 | name: 'Sarah', 60 | }, 61 | Buddy: { 62 | name: 'Tracy', 63 | }, 64 | } 65 | 66 | const NOTIFICATION = 'notification' 67 | 68 | const resolvers: IResolvers = { 69 | Query: { 70 | Hello(root, args, ctx, info) { 71 | // root ~ {} 72 | root 73 | // args ~ {} 74 | args 75 | // ctx.authorization ~ string | undefined 76 | ctx.authorization 77 | // info ~ GraphQLResolveInfo 78 | info 79 | 80 | return 'world' 81 | }, 82 | dogs() { 83 | return dogs 84 | }, 85 | }, 86 | Mutation: { 87 | add(root, { x, y }, ctx, info) { 88 | // root ~ {} 89 | root 90 | // x ~ string 91 | x 92 | // x ~ string 93 | y 94 | // ctx.authorization ~ string | undefined 95 | ctx.authorization 96 | // info ~ GraphQLResolveInfo 97 | info 98 | 99 | return x + y 100 | }, 101 | createNotification(_root, { message }, { pubsub }) { 102 | pubsub.publish({ 103 | topic: NOTIFICATION, 104 | payload: { 105 | newNotification: message, 106 | }, 107 | }) 108 | return true 109 | }, 110 | }, 111 | Subscription: { 112 | newNotification: { 113 | subscribe: (_root, _args, { pubsub }) => { 114 | return pubsub.subscribe(NOTIFICATION) 115 | }, 116 | }, 117 | }, 118 | } 119 | 120 | const loaders: MercuriusLoaders = { 121 | Dog: { 122 | async owner(queries, _ctx) { 123 | return queries.map(({ obj }) => owners[obj.name]) 124 | }, 125 | }, 126 | } 127 | 128 | app.register(mercurius, { 129 | schema, 130 | resolvers, 131 | loaders, 132 | context: buildContext, 133 | subscription: true, 134 | }) 135 | 136 | codegenMercurius(app, codegenMercuriusOptions).catch(console.error) 137 | 138 | // app.listen(8000) 139 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/src/listen.ts: -------------------------------------------------------------------------------- 1 | import { app } from './index' 2 | 3 | app.listen(8000) 4 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/test/example.test.ts: -------------------------------------------------------------------------------- 1 | import { createMercuriusTestClient } from 'mercurius-integration-testing' 2 | import tap from 'tap' 3 | 4 | import { app } from '../src' 5 | import { 6 | addDocument, 7 | createNotificationDocument, 8 | dogsDocument, 9 | helloDocument, 10 | newNotificationDocument, 11 | } from '../src/graphql/generated' 12 | 13 | const client = createMercuriusTestClient(app) 14 | 15 | tap.teardown(async () => { 16 | await app.close() 17 | }) 18 | 19 | tap.test('works', async (t) => { 20 | t.plan(4) 21 | 22 | await client.query(helloDocument).then((response) => { 23 | t.equal(response.data.Hello, 'world') 24 | t.equal(response.errors, undefined) 25 | }) 26 | 27 | await client 28 | .mutate(addDocument, { 29 | variables: { 30 | x: 1, 31 | y: 2, 32 | }, 33 | }) 34 | .then((response) => { 35 | t.equal(response.data.add, 3) 36 | t.equal(response.errors, undefined) 37 | }) 38 | }) 39 | 40 | tap.test('query with loaders', async (t) => { 41 | const response = await client.query(dogsDocument) 42 | 43 | t.same(response, { 44 | data: { 45 | dogs: [ 46 | { name: 'Max', owner: { name: 'Jennifer' } }, 47 | { name: 'Charlie', owner: { name: 'Sarah' } }, 48 | { name: 'Buddy', owner: { name: 'Tracy' } }, 49 | { name: 'Max', owner: { name: 'Jennifer' } }, 50 | ], 51 | }, 52 | }) 53 | 54 | t.end() 55 | }) 56 | 57 | tap.test('subscription', async (t) => { 58 | t.plan(2) 59 | const notificationMessage = 'Hello World' 60 | 61 | const client = createMercuriusTestClient(app) 62 | 63 | await new Promise(async (resolve) => { 64 | await client.subscribe({ 65 | query: newNotificationDocument, 66 | onData(response) { 67 | t.same(response, { 68 | data: { 69 | newNotification: notificationMessage, 70 | }, 71 | }) 72 | 73 | resolve() 74 | }, 75 | }) 76 | await client 77 | .mutate(createNotificationDocument, { 78 | variables: { 79 | message: notificationMessage, 80 | }, 81 | }) 82 | .then((response) => { 83 | t.same(response, { 84 | data: { 85 | createNotification: true, 86 | }, 87 | }) 88 | }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "exclude": [] 5 | } 6 | -------------------------------------------------------------------------------- /examples/codegen-gql-files/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist" /* Redirect output structure to the directory. */, 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true /* Skip type checking of declaration files. */, 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | }, 69 | "include": ["src"], 70 | "exclude": ["test"] 71 | } 72 | -------------------------------------------------------------------------------- /examples/codegen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-codegen", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Mercurius + GraphQL Code Generator example", 6 | "keywords": [ 7 | "graphql-codegen" 8 | ], 9 | "license": "MIT", 10 | "author": "PabloSz", 11 | "main": "dist/index.js", 12 | "scripts": { 13 | "dev": "bob-tsm --node-env=dev --cjs --watch=src src/listen.ts", 14 | "start": "bob-tsm --node-env=prod --cjs src/listen.ts", 15 | "test": "cross-env NODE_ENV=test tap --node-arg=--require=bob-tsm" 16 | }, 17 | "dependencies": { 18 | "fastify": "^5.2.1", 19 | "graphql": "^16.10.0", 20 | "mercurius": "^16.0.1", 21 | "mercurius-codegen": "workspace:*" 22 | }, 23 | "devDependencies": { 24 | "@graphql-typed-document-node/core": "^3.2.0", 25 | "@types/node": "^22.13.0", 26 | "@types/tap": "^18.0.0", 27 | "bob-tsm": "^1.1.2", 28 | "cross-env": "^7.0.3", 29 | "esbuild": "^0.24.2", 30 | "mercurius-integration-testing": "^9.0.1", 31 | "prettier": "^3.4.2", 32 | "tap": "^16.3.10", 33 | "typescript": "^5.7.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/codegen/src/graphql/generated.ts: -------------------------------------------------------------------------------- 1 | import type { GraphQLResolveInfo } from 'graphql' 2 | import type { MercuriusContext } from 'mercurius' 3 | import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' 4 | export type Maybe = T | null 5 | export type InputMaybe = Maybe 6 | export type Exact = { 7 | [K in keyof T]: T[K] 8 | } 9 | export type MakeOptional = Omit & { 10 | [SubKey in K]?: Maybe 11 | } 12 | export type MakeMaybe = Omit & { 13 | [SubKey in K]: Maybe 14 | } 15 | export type ResolverFn = ( 16 | parent: TParent, 17 | args: TArgs, 18 | context: TContext, 19 | info: GraphQLResolveInfo, 20 | ) => 21 | | Promise> 22 | | import('mercurius-codegen').DeepPartial 23 | export type RequireFields = Omit & { 24 | [P in K]-?: NonNullable 25 | } 26 | /** All built-in and custom scalars, mapped to their actual values */ 27 | export type Scalars = { 28 | ID: string 29 | String: string 30 | Boolean: boolean 31 | Int: number 32 | Float: number 33 | _FieldSet: any 34 | } 35 | 36 | export type Human = { 37 | __typename?: 'Human' 38 | name: Scalars['String'] 39 | } 40 | 41 | export type Foo = { 42 | __typename?: 'Foo' 43 | bars?: Maybe>>>>>> 44 | } 45 | 46 | export type Dog = { 47 | __typename?: 'Dog' 48 | name: Scalars['String'] 49 | owner?: Maybe 50 | } 51 | 52 | export type Query = { 53 | __typename?: 'Query' 54 | Hello: Scalars['String'] 55 | dogs: Array 56 | getFoo?: Maybe 57 | } 58 | 59 | export type Mutation = { 60 | __typename?: 'Mutation' 61 | add: Scalars['Int'] 62 | createNotification: Scalars['Boolean'] 63 | } 64 | 65 | export type MutationaddArgs = { 66 | x: Scalars['Int'] 67 | y: Scalars['Int'] 68 | } 69 | 70 | export type MutationcreateNotificationArgs = { 71 | message: Scalars['String'] 72 | } 73 | 74 | export type Subscription = { 75 | __typename?: 'Subscription' 76 | newNotification: Scalars['String'] 77 | } 78 | 79 | export type ResolverTypeWrapper = Promise | T 80 | 81 | export type ResolverWithResolve = { 82 | resolve: ResolverFn 83 | } 84 | export type Resolver = 85 | | ResolverFn 86 | | ResolverWithResolve 87 | 88 | export type SubscriptionSubscribeFn = ( 89 | parent: TParent, 90 | args: TArgs, 91 | context: TContext, 92 | info: GraphQLResolveInfo, 93 | ) => AsyncIterable | Promise> 94 | 95 | export type SubscriptionResolveFn = ( 96 | parent: TParent, 97 | args: TArgs, 98 | context: TContext, 99 | info: GraphQLResolveInfo, 100 | ) => TResult | Promise 101 | 102 | export interface SubscriptionSubscriberObject< 103 | TResult, 104 | TKey extends string, 105 | TParent, 106 | TContext, 107 | TArgs, 108 | > { 109 | subscribe: SubscriptionSubscribeFn< 110 | { [key in TKey]: TResult }, 111 | TParent, 112 | TContext, 113 | TArgs 114 | > 115 | resolve?: SubscriptionResolveFn< 116 | TResult, 117 | { [key in TKey]: TResult }, 118 | TContext, 119 | TArgs 120 | > 121 | } 122 | 123 | export interface SubscriptionResolverObject { 124 | subscribe: SubscriptionSubscribeFn 125 | resolve: SubscriptionResolveFn 126 | } 127 | 128 | export type SubscriptionObject< 129 | TResult, 130 | TKey extends string, 131 | TParent, 132 | TContext, 133 | TArgs, 134 | > = 135 | | SubscriptionSubscriberObject 136 | | SubscriptionResolverObject 137 | 138 | export type SubscriptionResolver< 139 | TResult, 140 | TKey extends string, 141 | TParent = {}, 142 | TContext = {}, 143 | TArgs = {}, 144 | > = 145 | | (( 146 | ...args: any[] 147 | ) => SubscriptionObject) 148 | | SubscriptionObject 149 | 150 | export type TypeResolveFn = ( 151 | parent: TParent, 152 | context: TContext, 153 | info: GraphQLResolveInfo, 154 | ) => Maybe | Promise> 155 | 156 | export type IsTypeOfResolverFn = ( 157 | obj: T, 158 | context: TContext, 159 | info: GraphQLResolveInfo, 160 | ) => boolean | Promise 161 | 162 | export type NextResolverFn = () => Promise 163 | 164 | export type DirectiveResolverFn< 165 | TResult = {}, 166 | TParent = {}, 167 | TContext = {}, 168 | TArgs = {}, 169 | > = ( 170 | next: NextResolverFn, 171 | parent: TParent, 172 | args: TArgs, 173 | context: TContext, 174 | info: GraphQLResolveInfo, 175 | ) => TResult | Promise 176 | 177 | /** Mapping between all available schema types and the resolvers types */ 178 | export type ResolversTypes = { 179 | Human: ResolverTypeWrapper 180 | String: ResolverTypeWrapper 181 | Foo: ResolverTypeWrapper 182 | Int: ResolverTypeWrapper 183 | Dog: ResolverTypeWrapper 184 | Query: ResolverTypeWrapper<{}> 185 | Mutation: ResolverTypeWrapper<{}> 186 | Boolean: ResolverTypeWrapper 187 | Subscription: ResolverTypeWrapper<{}> 188 | } 189 | 190 | /** Mapping between all available schema types and the resolvers parents */ 191 | export type ResolversParentTypes = { 192 | Human: Human 193 | String: Scalars['String'] 194 | Foo: Foo 195 | Int: Scalars['Int'] 196 | Dog: Dog 197 | Query: {} 198 | Mutation: {} 199 | Boolean: Scalars['Boolean'] 200 | Subscription: {} 201 | } 202 | 203 | export type HumanResolvers< 204 | ContextType = MercuriusContext, 205 | ParentType extends 206 | ResolversParentTypes['Human'] = ResolversParentTypes['Human'], 207 | > = { 208 | name?: Resolver 209 | isTypeOf?: IsTypeOfResolverFn 210 | } 211 | 212 | export type FooResolvers< 213 | ContextType = MercuriusContext, 214 | ParentType extends ResolversParentTypes['Foo'] = ResolversParentTypes['Foo'], 215 | > = { 216 | bars?: Resolver< 217 | Maybe>>>>>>, 218 | ParentType, 219 | ContextType 220 | > 221 | isTypeOf?: IsTypeOfResolverFn 222 | } 223 | 224 | export type DogResolvers< 225 | ContextType = MercuriusContext, 226 | ParentType extends ResolversParentTypes['Dog'] = ResolversParentTypes['Dog'], 227 | > = { 228 | name?: Resolver 229 | owner?: Resolver, ParentType, ContextType> 230 | isTypeOf?: IsTypeOfResolverFn 231 | } 232 | 233 | export type QueryResolvers< 234 | ContextType = MercuriusContext, 235 | ParentType extends 236 | ResolversParentTypes['Query'] = ResolversParentTypes['Query'], 237 | > = { 238 | Hello?: Resolver 239 | dogs?: Resolver, ParentType, ContextType> 240 | getFoo?: Resolver, ParentType, ContextType> 241 | } 242 | 243 | export type MutationResolvers< 244 | ContextType = MercuriusContext, 245 | ParentType extends 246 | ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation'], 247 | > = { 248 | add?: Resolver< 249 | ResolversTypes['Int'], 250 | ParentType, 251 | ContextType, 252 | RequireFields 253 | > 254 | createNotification?: Resolver< 255 | ResolversTypes['Boolean'], 256 | ParentType, 257 | ContextType, 258 | RequireFields 259 | > 260 | } 261 | 262 | export type SubscriptionResolvers< 263 | ContextType = MercuriusContext, 264 | ParentType extends 265 | ResolversParentTypes['Subscription'] = ResolversParentTypes['Subscription'], 266 | > = { 267 | newNotification?: SubscriptionResolver< 268 | ResolversTypes['String'], 269 | 'newNotification', 270 | ParentType, 271 | ContextType 272 | > 273 | } 274 | 275 | export type Resolvers = { 276 | Human?: HumanResolvers 277 | Foo?: FooResolvers 278 | Dog?: DogResolvers 279 | Query?: QueryResolvers 280 | Mutation?: MutationResolvers 281 | Subscription?: SubscriptionResolvers 282 | } 283 | 284 | export type Loader = ( 285 | queries: Array<{ 286 | obj: TObj 287 | params: TParams 288 | }>, 289 | context: TContext & { 290 | reply: import('fastify').FastifyReply 291 | }, 292 | ) => Promise>> 293 | export type LoaderResolver = 294 | | Loader 295 | | { 296 | loader: Loader 297 | opts?: { 298 | cache?: boolean 299 | } 300 | } 301 | export interface Loaders< 302 | TContext = import('mercurius').MercuriusContext & { 303 | reply: import('fastify').FastifyReply 304 | }, 305 | > { 306 | Human?: { 307 | name?: LoaderResolver 308 | } 309 | 310 | Foo?: { 311 | bars?: LoaderResolver< 312 | Maybe>>>>, 313 | Foo, 314 | {}, 315 | TContext 316 | > 317 | } 318 | 319 | Dog?: { 320 | name?: LoaderResolver 321 | owner?: LoaderResolver, Dog, {}, TContext> 322 | } 323 | } 324 | export type addMutationVariables = Exact<{ 325 | x: Scalars['Int'] 326 | y: Scalars['Int'] 327 | }> 328 | 329 | export type addMutation = { __typename?: 'Mutation'; add: number } 330 | 331 | export type helloQueryVariables = Exact<{ [key: string]: never }> 332 | 333 | export type helloQuery = { __typename?: 'Query'; Hello: string } 334 | 335 | export type dogsQueryVariables = Exact<{ [key: string]: never }> 336 | 337 | export type dogsQuery = { 338 | __typename?: 'Query' 339 | dogs: Array<{ 340 | __typename?: 'Dog' 341 | name: string 342 | owner?: { __typename?: 'Human'; name: string } | null 343 | }> 344 | } 345 | 346 | export type createNotificationMutationVariables = Exact<{ 347 | message: Scalars['String'] 348 | }> 349 | 350 | export type createNotificationMutation = { 351 | __typename?: 'Mutation' 352 | createNotification: boolean 353 | } 354 | 355 | export type newNotificationSubscriptionVariables = Exact<{ 356 | [key: string]: never 357 | }> 358 | 359 | export type newNotificationSubscription = { 360 | __typename?: 'Subscription' 361 | newNotification: string 362 | } 363 | 364 | export const addDocument = { 365 | kind: 'Document', 366 | definitions: [ 367 | { 368 | kind: 'OperationDefinition', 369 | operation: 'mutation', 370 | name: { kind: 'Name', value: 'add' }, 371 | variableDefinitions: [ 372 | { 373 | kind: 'VariableDefinition', 374 | variable: { kind: 'Variable', name: { kind: 'Name', value: 'x' } }, 375 | type: { 376 | kind: 'NonNullType', 377 | type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, 378 | }, 379 | }, 380 | { 381 | kind: 'VariableDefinition', 382 | variable: { kind: 'Variable', name: { kind: 'Name', value: 'y' } }, 383 | type: { 384 | kind: 'NonNullType', 385 | type: { kind: 'NamedType', name: { kind: 'Name', value: 'Int' } }, 386 | }, 387 | }, 388 | ], 389 | selectionSet: { 390 | kind: 'SelectionSet', 391 | selections: [ 392 | { 393 | kind: 'Field', 394 | name: { kind: 'Name', value: 'add' }, 395 | arguments: [ 396 | { 397 | kind: 'Argument', 398 | name: { kind: 'Name', value: 'x' }, 399 | value: { kind: 'Variable', name: { kind: 'Name', value: 'x' } }, 400 | }, 401 | { 402 | kind: 'Argument', 403 | name: { kind: 'Name', value: 'y' }, 404 | value: { kind: 'Variable', name: { kind: 'Name', value: 'y' } }, 405 | }, 406 | ], 407 | }, 408 | ], 409 | }, 410 | }, 411 | ], 412 | } as unknown as DocumentNode 413 | export const helloDocument = { 414 | kind: 'Document', 415 | definitions: [ 416 | { 417 | kind: 'OperationDefinition', 418 | operation: 'query', 419 | name: { kind: 'Name', value: 'hello' }, 420 | selectionSet: { 421 | kind: 'SelectionSet', 422 | selections: [{ kind: 'Field', name: { kind: 'Name', value: 'Hello' } }], 423 | }, 424 | }, 425 | ], 426 | } as unknown as DocumentNode 427 | export const dogsDocument = { 428 | kind: 'Document', 429 | definitions: [ 430 | { 431 | kind: 'OperationDefinition', 432 | operation: 'query', 433 | name: { kind: 'Name', value: 'dogs' }, 434 | selectionSet: { 435 | kind: 'SelectionSet', 436 | selections: [ 437 | { 438 | kind: 'Field', 439 | name: { kind: 'Name', value: 'dogs' }, 440 | selectionSet: { 441 | kind: 'SelectionSet', 442 | selections: [ 443 | { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 444 | { 445 | kind: 'Field', 446 | name: { kind: 'Name', value: 'owner' }, 447 | selectionSet: { 448 | kind: 'SelectionSet', 449 | selections: [ 450 | { kind: 'Field', name: { kind: 'Name', value: 'name' } }, 451 | ], 452 | }, 453 | }, 454 | ], 455 | }, 456 | }, 457 | ], 458 | }, 459 | }, 460 | ], 461 | } as unknown as DocumentNode 462 | export const createNotificationDocument = { 463 | kind: 'Document', 464 | definitions: [ 465 | { 466 | kind: 'OperationDefinition', 467 | operation: 'mutation', 468 | name: { kind: 'Name', value: 'createNotification' }, 469 | variableDefinitions: [ 470 | { 471 | kind: 'VariableDefinition', 472 | variable: { 473 | kind: 'Variable', 474 | name: { kind: 'Name', value: 'message' }, 475 | }, 476 | type: { 477 | kind: 'NonNullType', 478 | type: { 479 | kind: 'NamedType', 480 | name: { kind: 'Name', value: 'String' }, 481 | }, 482 | }, 483 | }, 484 | ], 485 | selectionSet: { 486 | kind: 'SelectionSet', 487 | selections: [ 488 | { 489 | kind: 'Field', 490 | name: { kind: 'Name', value: 'createNotification' }, 491 | arguments: [ 492 | { 493 | kind: 'Argument', 494 | name: { kind: 'Name', value: 'message' }, 495 | value: { 496 | kind: 'Variable', 497 | name: { kind: 'Name', value: 'message' }, 498 | }, 499 | }, 500 | ], 501 | }, 502 | ], 503 | }, 504 | }, 505 | ], 506 | } as unknown as DocumentNode< 507 | createNotificationMutation, 508 | createNotificationMutationVariables 509 | > 510 | export const newNotificationDocument = { 511 | kind: 'Document', 512 | definitions: [ 513 | { 514 | kind: 'OperationDefinition', 515 | operation: 'subscription', 516 | name: { kind: 'Name', value: 'newNotification' }, 517 | selectionSet: { 518 | kind: 'SelectionSet', 519 | selections: [ 520 | { kind: 'Field', name: { kind: 'Name', value: 'newNotification' } }, 521 | ], 522 | }, 523 | }, 524 | ], 525 | } as unknown as DocumentNode< 526 | newNotificationSubscription, 527 | newNotificationSubscriptionVariables 528 | > 529 | declare module 'mercurius' { 530 | interface IResolvers 531 | extends Resolvers {} 532 | interface MercuriusLoaders extends Loaders {} 533 | } 534 | -------------------------------------------------------------------------------- /examples/codegen/src/graphql/operations/example.gql: -------------------------------------------------------------------------------- 1 | mutation add($x: Int!, $y: Int!) { 2 | add(x: $x, y: $y) 3 | } 4 | 5 | query hello { 6 | Hello 7 | } 8 | 9 | query dogs { 10 | dogs { 11 | name 12 | owner { 13 | name 14 | } 15 | } 16 | } 17 | 18 | mutation createNotification($message: String!) { 19 | createNotification(message: $message) 20 | } 21 | 22 | subscription newNotification { 23 | newNotification 24 | } 25 | -------------------------------------------------------------------------------- /examples/codegen/src/index.ts: -------------------------------------------------------------------------------- 1 | import Fastify, { FastifyReply, FastifyRequest } from 'fastify' 2 | import mercurius, { IResolvers, MercuriusLoaders } from 'mercurius' 3 | import { codegenMercurius, gql } from 'mercurius-codegen' 4 | 5 | export const app = Fastify({ 6 | logger: process.env.NODE_ENV !== 'test', 7 | }) 8 | 9 | const buildContext = async (req: FastifyRequest, _reply: FastifyReply) => { 10 | return { 11 | authorization: req.headers.authorization, 12 | } 13 | } 14 | 15 | type PromiseType = T extends PromiseLike ? U : T 16 | 17 | declare module 'mercurius' { 18 | interface MercuriusContext 19 | extends PromiseType> {} 20 | } 21 | 22 | const schema = gql` 23 | type Human { 24 | name: String! 25 | } 26 | 27 | type Foo { 28 | bars: [[[Int]]] 29 | } 30 | 31 | type Dog { 32 | name: String! 33 | owner: Human 34 | } 35 | 36 | type Query { 37 | Hello: String! 38 | dogs: [Dog!]! 39 | getFoo: Foo 40 | } 41 | 42 | type Mutation { 43 | add(x: Int!, y: Int!): Int! 44 | createNotification(message: String!): Boolean! 45 | } 46 | 47 | type Subscription { 48 | newNotification: String! 49 | } 50 | ` 51 | 52 | const dogs = [ 53 | { name: 'Max' }, 54 | { name: 'Charlie' }, 55 | { name: 'Buddy' }, 56 | { name: 'Max' }, 57 | ] 58 | 59 | const owners: Record = { 60 | Max: { 61 | name: 'Jennifer', 62 | }, 63 | Charlie: { 64 | name: 'Sarah', 65 | }, 66 | Buddy: { 67 | name: 'Tracy', 68 | }, 69 | } 70 | 71 | const NOTIFICATION = 'notification' 72 | 73 | const resolvers: IResolvers = { 74 | Query: { 75 | Hello(root, args, ctx, info) { 76 | // root ~ {} 77 | root 78 | // args ~ {} 79 | args 80 | // ctx.authorization ~ string | undefined 81 | ctx.authorization 82 | // info ~ GraphQLResolveInfo 83 | info 84 | 85 | return 'world' 86 | }, 87 | dogs() { 88 | return dogs 89 | }, 90 | }, 91 | Mutation: { 92 | add(root, { x, y }, ctx, info) { 93 | // root ~ {} 94 | root 95 | // x ~ number 96 | x 97 | // x ~ number 98 | y 99 | // ctx.authorization ~ string | undefined 100 | ctx.authorization 101 | // info ~ GraphQLResolveInfo 102 | info 103 | 104 | return x + y 105 | }, 106 | createNotification(_root, { message }, { pubsub }) { 107 | pubsub.publish({ 108 | topic: NOTIFICATION, 109 | payload: { 110 | newNotification: message, 111 | }, 112 | }) 113 | return true 114 | }, 115 | }, 116 | Subscription: { 117 | newNotification: { 118 | subscribe: (_root, _args, { pubsub }) => { 119 | return pubsub.subscribe(NOTIFICATION) 120 | }, 121 | }, 122 | }, 123 | } 124 | 125 | const loaders: MercuriusLoaders = { 126 | Dog: { 127 | async owner(queries, _ctx) { 128 | return queries.map(({ obj }) => owners[obj.name]) 129 | }, 130 | }, 131 | } 132 | 133 | app.register(mercurius, { 134 | schema, 135 | resolvers, 136 | loaders, 137 | context: buildContext, 138 | subscription: true, 139 | }) 140 | 141 | codegenMercurius(app, { 142 | targetPath: './src/graphql/generated.ts', 143 | operationsGlob: './src/graphql/operations/*.gql', 144 | codegenConfig: { 145 | loadersCustomParentTypes: { 146 | Human: 'never', 147 | }, 148 | }, 149 | }).catch(console.error) 150 | -------------------------------------------------------------------------------- /examples/codegen/src/listen.ts: -------------------------------------------------------------------------------- 1 | import { app } from './index' 2 | 3 | app.listen(8000) 4 | -------------------------------------------------------------------------------- /examples/codegen/test/example.test.ts: -------------------------------------------------------------------------------- 1 | import { createMercuriusTestClient } from 'mercurius-integration-testing' 2 | import tap from 'tap' 3 | 4 | import { app } from '../src' 5 | import { 6 | addDocument, 7 | createNotificationDocument, 8 | dogsDocument, 9 | helloDocument, 10 | newNotificationDocument, 11 | } from '../src/graphql/generated' 12 | 13 | const client = createMercuriusTestClient(app) 14 | 15 | tap.teardown(async () => { 16 | await app.close() 17 | }) 18 | 19 | tap.test('hello works', async (t) => { 20 | t.plan(4) 21 | 22 | await client.query(helloDocument).then((response) => { 23 | t.equal(response.data.Hello, 'world') 24 | t.equal(response.errors, undefined) 25 | }) 26 | 27 | await client 28 | .mutate(addDocument, { 29 | variables: { 30 | x: 1, 31 | y: 2, 32 | }, 33 | }) 34 | .then((response) => { 35 | t.equal(response.data.add, 3) 36 | t.equal(response.errors, undefined) 37 | }) 38 | }) 39 | 40 | tap.test('query with loaders', async (t) => { 41 | const response = await client.query(dogsDocument) 42 | 43 | t.same(response, { 44 | data: { 45 | dogs: [ 46 | { name: 'Max', owner: { name: 'Jennifer' } }, 47 | { name: 'Charlie', owner: { name: 'Sarah' } }, 48 | { name: 'Buddy', owner: { name: 'Tracy' } }, 49 | { name: 'Max', owner: { name: 'Jennifer' } }, 50 | ], 51 | }, 52 | }) 53 | 54 | t.end() 55 | }) 56 | 57 | tap.test('subscription', async (t) => { 58 | t.plan(2) 59 | const notificationMessage = 'Hello World' 60 | 61 | const client = createMercuriusTestClient(app) 62 | 63 | await new Promise(async (resolve) => { 64 | await client.subscribe({ 65 | query: newNotificationDocument, 66 | onData(response) { 67 | t.same(response, { 68 | data: { 69 | newNotification: notificationMessage, 70 | }, 71 | }) 72 | 73 | resolve() 74 | }, 75 | }) 76 | await client 77 | .mutate(createNotificationDocument, { 78 | variables: { 79 | message: notificationMessage, 80 | }, 81 | }) 82 | .then((response) => { 83 | t.same(response, { 84 | data: { 85 | createNotification: true, 86 | }, 87 | }) 88 | }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /examples/codegen/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "exclude": [] 5 | } 6 | -------------------------------------------------------------------------------- /examples/codegen/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist" /* Redirect output structure to the directory. */, 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true /* Skip type checking of declaration files. */, 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | }, 69 | "include": ["src"], 70 | "exclude": ["test"] 71 | } 72 | -------------------------------------------------------------------------------- /examples/manual/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-manual", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Mercurius basic example using only included types", 6 | "license": "MIT", 7 | "author": "PabloSz", 8 | "main": "dist/index.js", 9 | "scripts": { 10 | "dev": "bob-tsm --node-env=dev --cjs --watch=src src/listen.ts", 11 | "start": "bob-tsm --node-env=prod --cjs src/listen.ts", 12 | "test": "cross-env NODE_ENV=test tap --node-arg=--require=bob-tsm" 13 | }, 14 | "dependencies": { 15 | "fastify": "^5.2.1", 16 | "graphql": "^16.10.0", 17 | "mercurius": "^16.0.1" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^22.13.0", 21 | "@types/tap": "^18.0.0", 22 | "bob-tsm": "^1.1.2", 23 | "cross-env": "^7.0.3", 24 | "esbuild": "^0.24.2", 25 | "graphql-tag": "^2.12.6", 26 | "mercurius-integration-testing": "^9.0.1", 27 | "prettier": "^3.4.2", 28 | "tap": "^16.3.10", 29 | "typescript": "^5.7.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/manual/src/index.ts: -------------------------------------------------------------------------------- 1 | import Fastify, { FastifyReply, FastifyRequest } from 'fastify' 2 | import mercurius, { 3 | IFieldResolver, 4 | IResolvers, 5 | MercuriusContext, 6 | MercuriusLoaders, 7 | } from 'mercurius' 8 | 9 | export const app = Fastify({ 10 | logger: true, 11 | }) 12 | 13 | const buildContext = async (req: FastifyRequest, _reply: FastifyReply) => { 14 | return { 15 | authorization: req.headers.authorization, 16 | } 17 | } 18 | 19 | type PromiseType = T extends PromiseLike ? U : T 20 | 21 | declare module 'mercurius' { 22 | interface MercuriusContext 23 | extends PromiseType> {} 24 | } 25 | 26 | const schema = ` 27 | type Human { 28 | name: String! 29 | } 30 | 31 | type Dog { 32 | name: String! 33 | owner: Human 34 | } 35 | 36 | type Query { 37 | helloTyped: String! 38 | helloInline: String! 39 | dogs: [Dog!]! 40 | } 41 | 42 | type Subscription { 43 | newNotification: String! 44 | } 45 | 46 | type Mutation { 47 | createNotification(message: String!): Boolean! 48 | } 49 | ` 50 | 51 | const helloTyped: IFieldResolver< 52 | {} /** Root */, 53 | MercuriusContext /** Context */, 54 | {} /** Args */ 55 | > = (root, args, ctx, info) => { 56 | // root ~ {} 57 | root 58 | // args ~ {} 59 | args 60 | // ctx.authorization ~ string | undefined 61 | ctx.authorization 62 | // info ~ GraphQLResolveInfo 63 | info 64 | 65 | return 'world' 66 | } 67 | 68 | const NOTIFICATION = 'notification' 69 | 70 | const dogs = [ 71 | { name: 'Max' }, 72 | { name: 'Charlie' }, 73 | { name: 'Buddy' }, 74 | { name: 'Max' }, 75 | ] 76 | 77 | const owners: Record = { 78 | Max: { 79 | name: 'Jennifer', 80 | }, 81 | Charlie: { 82 | name: 'Sarah', 83 | }, 84 | Buddy: { 85 | name: 'Tracy', 86 | }, 87 | } 88 | 89 | const resolvers: IResolvers = { 90 | Query: { 91 | helloTyped, 92 | helloInline: (root: {}, args: {}, ctx, info) => { 93 | // root ~ {} 94 | root 95 | // args ~ {} 96 | args 97 | // ctx.authorization ~ string | undefined 98 | ctx.authorization 99 | // info ~ GraphQLResolveInfo 100 | info 101 | 102 | return 'world' 103 | }, 104 | dogs: (_root, _args, _ctx_, _info) => dogs, 105 | }, 106 | Mutation: { 107 | createNotification(_root, { message }: { message: string }, { pubsub }) { 108 | pubsub.publish({ 109 | topic: NOTIFICATION, 110 | payload: { 111 | newNotification: message, 112 | }, 113 | }) 114 | return true 115 | }, 116 | }, 117 | Subscription: { 118 | newNotification: { 119 | subscribe: (_root, _args, { pubsub }) => { 120 | return pubsub.subscribe(NOTIFICATION) 121 | }, 122 | }, 123 | }, 124 | } 125 | 126 | const loaders: MercuriusLoaders = { 127 | Dog: { 128 | async owner(queries, _ctx) { 129 | return queries.map( 130 | ({ obj }: { obj: { name: string } }) => owners[obj.name], 131 | ) 132 | }, 133 | }, 134 | } 135 | 136 | app.register(mercurius, { 137 | schema, 138 | resolvers, 139 | context: buildContext, 140 | subscription: true, 141 | loaders, 142 | }) 143 | -------------------------------------------------------------------------------- /examples/manual/src/listen.ts: -------------------------------------------------------------------------------- 1 | import { app } from './index' 2 | 3 | app.listen(8000) 4 | -------------------------------------------------------------------------------- /examples/manual/test/app.test.ts: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag' 2 | import { createMercuriusTestClient } from 'mercurius-integration-testing' 3 | import tap from 'tap' 4 | 5 | import { app } from '../src' 6 | 7 | const client = createMercuriusTestClient(app) 8 | 9 | tap.teardown(async () => { 10 | await app.close() 11 | }) 12 | 13 | tap.test('query', async (t) => { 14 | t.plan(1) 15 | 16 | await client 17 | .query<{ 18 | hello: string 19 | helloTyped: string 20 | helloInline: string 21 | }>(gql` 22 | query { 23 | helloTyped 24 | helloInline 25 | } 26 | `) 27 | .then((response) => { 28 | t.same(response, { 29 | data: { 30 | helloTyped: 'world', 31 | helloInline: 'world', 32 | }, 33 | }) 34 | }) 35 | }) 36 | 37 | tap.test('subscription', async (t) => { 38 | t.plan(2) 39 | const notificationMessage = 'Hello World' 40 | 41 | const client = createMercuriusTestClient(app) 42 | 43 | await new Promise(async (resolve) => { 44 | await client.subscribe<{ 45 | newNotification: string 46 | }>({ 47 | query: gql` 48 | subscription { 49 | newNotification 50 | } 51 | `, 52 | onData(response) { 53 | t.same(response, { 54 | data: { 55 | newNotification: notificationMessage, 56 | }, 57 | }) 58 | 59 | resolve() 60 | }, 61 | }) 62 | await client 63 | .mutate< 64 | { 65 | createNotification: boolean 66 | }, 67 | { 68 | message: string 69 | } 70 | >( 71 | gql` 72 | mutation ($message: String!) { 73 | createNotification(message: $message) 74 | } 75 | `, 76 | { 77 | variables: { 78 | message: notificationMessage, 79 | }, 80 | }, 81 | ) 82 | .then((response) => { 83 | t.same(response, { 84 | data: { 85 | createNotification: true, 86 | }, 87 | }) 88 | }) 89 | }) 90 | }) 91 | 92 | tap.test('loaders', async (t) => { 93 | const response = await client.query(gql` 94 | query { 95 | dogs { 96 | name 97 | owner { 98 | name 99 | } 100 | } 101 | } 102 | `) 103 | 104 | t.same(response, { 105 | data: { 106 | dogs: [ 107 | { name: 'Max', owner: { name: 'Jennifer' } }, 108 | { name: 'Charlie', owner: { name: 'Sarah' } }, 109 | { name: 'Buddy', owner: { name: 'Tracy' } }, 110 | { name: 'Max', owner: { name: 'Jennifer' } }, 111 | ], 112 | }, 113 | }) 114 | 115 | t.end() 116 | }) 117 | -------------------------------------------------------------------------------- /examples/manual/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "exclude": [] 5 | } 6 | -------------------------------------------------------------------------------- /examples/manual/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist" /* Redirect output structure to the directory. */, 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 30 | "strictNullChecks": true /* Enable strict null checks. */, 31 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 32 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 33 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 34 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 35 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 36 | 37 | /* Additional Checks */ 38 | "noUnusedLocals": true /* Report errors on unused locals. */, 39 | "noUnusedParameters": true /* Report errors on unused parameters. */, 40 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 41 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true /* Skip type checking of declaration files. */, 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | }, 69 | "include": ["src"], 70 | "exclude": ["test"] 71 | } 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mercurius-typescript", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "All the examples are available in the [examples folder](./examples/)", 6 | "homepage": "https://github.com/PabloSzx/mercurius-typescript#readme", 7 | "bugs": { 8 | "url": "https://github.com/PabloSzx/mercurius-typescript/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/PabloSzx/mercurius-typescript.git" 13 | }, 14 | "license": "MIT", 15 | "author": "PabloSz", 16 | "scripts": { 17 | "ci:release": "pnpm pretty && pnpm -r publish --access public --no-git-checks", 18 | "ci:version": "pnpm pretty && changeset version && pnpm i --no-frozen-lockfile --lockfile-only --ignore-scripts && pnpm update -r mercurius-codegen", 19 | "clean": "pnpm dlx rimraf \"**/{node_modules,dist,coverage,.nyc_output}\" pnpm-lock.yaml && pnpm i", 20 | "prepare": "husky install", 21 | "pretty": "prettier --write \"**/*.{ts,json}\"", 22 | "test": "pnpm test --recursive --no-bail --stream" 23 | }, 24 | "devDependencies": { 25 | "@changesets/cli": "^2.27.12", 26 | "@types/node": "^22.13.0", 27 | "changesets-github-release": "^0.1.0", 28 | "esbuild": "^0.24.2", 29 | "husky": "^9.1.7", 30 | "prettier": "^3.4.2", 31 | "pretty-quick": "^4.0.0", 32 | "rimraf": "^3.0.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "check-coverage": true, 4 | "extension": [".ts"], 5 | "reporter": ["lcov", "text-summary"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # mercurius-codegen 2 | 3 | ## 6.0.1 4 | 5 | ### Patch Changes 6 | 7 | - c017be4: Support mercurious v16 peer dependency 8 | 9 | ## 6.0.0 10 | 11 | ### Major Changes 12 | 13 | - b7997a5: Update for Mercurius v5, Fastify v15 and Prettier v3 14 | 15 | ## 5.0.3 16 | 17 | ### Patch Changes 18 | 19 | - a2d5a6b: Add support for addUnderscoreToArgsType codegen config 20 | 21 | ## 5.0.2 22 | 23 | ### Patch Changes 24 | 25 | - 14a45fd: Allow peer dependency to mercurius v12 26 | 27 | ## 5.0.1 28 | 29 | ### Patch Changes 30 | 31 | - 532a891: Export `CodegenMercuriusOptions` interface 32 | 33 | ## 5.0.0 34 | 35 | ### Major Changes 36 | 37 | - 2655e45: Update for mercurius v11 38 | 39 | ## 4.0.1 40 | 41 | ### Patch Changes 42 | 43 | - c3f8aa9: Fix @graphql-codegen/typescript-resolvers types import 44 | 45 | ## 4.0.0 46 | 47 | ### Major Changes 48 | 49 | - 8b08174: Update to Fastify v4 and Mercurius v10 50 | 51 | Thanks to @ZiiMakc, PR https://github.com/mercurius-js/mercurius-typescript/pull/285 52 | 53 | ## 3.3.1 54 | 55 | ### Patch Changes 56 | 57 | - 82fa1ad: Export custom Loader types from generated TS #180 (thanks to @luke88jones) 58 | 59 | ## 3.3.0 60 | 61 | ### Minor Changes 62 | 63 | - 43701ff: Support mercurius@^9.0.0 & graphql-js v16 64 | 65 | ## 3.2.0 66 | 67 | ### Minor Changes 68 | 69 | - ad1303c: Disable pre-built schema generation for `loadSchemaFiles` if "targetPath" is set to `null` 70 | 71 | ## 3.1.1 72 | 73 | ### Patch Changes 74 | 75 | - 42301d7: Update @graphql-tools/load-files 76 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pablo Sáez 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 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/README.md: -------------------------------------------------------------------------------- 1 | # mercurius-codegen 2 | 3 | [![npm version](https://badge.fury.io/js/mercurius-codegen.svg)](https://badge.fury.io/js/mercurius-codegen) 4 | 5 | Get full type-safety and autocompletion for [Mercurius](http://mercurius.dev/) using [TypeScript](https://www.typescriptlang.org/) and [GraphQL Code Generator](https://graphql-code-generator.com/) seamlessly while you code. 6 | 7 | ```sh 8 | pnpm add mercurius-codegen 9 | pnpm add -D prettier 10 | # or 11 | yarn add mercurius-codegen 12 | yarn add -D prettier 13 | # or 14 | npm install mercurius-codegen 15 | npm install -D prettier 16 | ``` 17 | 18 | ## Usage 19 | 20 | > **For convenience**, this package also exports a _fake_ `gql` tag that gives tooling support for _"prettier formatting"_ and _"IDE syntax highlighting"_. **It's completely optional**. 21 | 22 | ```ts 23 | import Fastify from 'fastify' 24 | import mercurius from 'mercurius' 25 | import { codegenMercurius, gql } from 'mercurius-codegen' 26 | 27 | const app = Fastify() 28 | 29 | app.register(mercurius, { 30 | schema: gql` 31 | type Query { 32 | hello(greetings: String!): String! 33 | } 34 | `, 35 | resolvers: { 36 | Query: { 37 | hello(_root, { greetings }) { 38 | // greetings ~ string 39 | return 'Hello World' 40 | }, 41 | }, 42 | }, 43 | }) 44 | 45 | codegenMercurius(app, { 46 | targetPath: './src/graphql/generated.ts', 47 | }).catch(console.error) 48 | 49 | // Then it will automatically generate the file, 50 | // and without doing anything special, 51 | // the resolvers are going to be typed, 52 | // or if your resolvers are in different files... 53 | 54 | app.listen(8000) 55 | ``` 56 | 57 | ```ts 58 | import { IResolvers } from 'mercurius' 59 | 60 | // Fully typed! 61 | export const resolvers: IResolvers = { 62 | Query: { 63 | hello(_root, { greetings }) { 64 | // greetings ~ string 65 | return 'Hello World' 66 | }, 67 | }, 68 | } 69 | ``` 70 | 71 | It also gives type-safety for [Mercurius Loaders](https://mercurius.dev/#/docs/loaders): 72 | 73 | ```ts 74 | import { MercuriusLoaders } from 'mercurius' 75 | 76 | // Fully typed! 77 | export const loaders: MercuriusLoaders = { 78 | Dog: { 79 | async owner(queries, ctx) { 80 | // queries & ctx are typed accordingly 81 | return queries.map(({ obj, params }) => { 82 | // obj & params are typed accordingly 83 | return owners[obj.name] 84 | }) 85 | }, 86 | }, 87 | } 88 | ``` 89 | 90 | > By default it disables itself if `NODE_ENV` is **'production'** 91 | 92 | > It automatically uses [prettier](https://prettier.io/) resolving the most nearby config for you. 93 | 94 | ### Operations 95 | 96 | **mercurius-codegen** also supports giving it GraphQL Operation files, basically client `queries`, `mutations` or `subscriptions`, and it creates [Typed Document Nodes](https://github.com/dotansimha/graphql-typed-document-node), that later can be used by other libraries, like for example [mercurius-integration-testing](https://github.com/mercurius-js/mercurius-integration-testing) (_that has native support for typed document nodes_), and then be able to have end-to-end type-safety and auto-completion. 97 | 98 | > You might need to install `@graphql-typed-document-node/core` manually in your project. 99 | 100 | ```ts 101 | import { codegenMercurius } from 'mercurius-codegen' 102 | 103 | codegenMercurius(app, { 104 | targetPath: './src/graphql/generated.ts', 105 | // You can also specify an array of globs 106 | operationsGlob: './src/graphql/operations/*.gql', 107 | }).catch(console.error) 108 | ``` 109 | 110 | > /your-project/src/graphql/operations/example.gql 111 | 112 | ```graphql 113 | query hello { 114 | HelloWorld 115 | } 116 | ``` 117 | 118 | Then, for example, in your tests: 119 | 120 | ```ts 121 | import { createMercuriusTestClient } from 'mercurius-integration-testing' 122 | 123 | import { helloDocument } from '../src/graphql/generated' 124 | import { app } from '../src/server' 125 | 126 | // ... 127 | 128 | const client = createMercuriusTestClient(app) 129 | 130 | const response = await client.query(helloDocument) 131 | 132 | // response is completely typed! 133 | ``` 134 | 135 | > Keep in mind that you can always call `codegenMercurius` multiple times for different environments and different paths if you prefer to keep the production code as light as possible (which is generally a good practice). 136 | 137 | ## LazyPromise 138 | 139 | This library also exports a very lightweight helper that is very useful for lazy resolution of promises, for example, to prevent un-requested data to be fetched from a database. 140 | 141 | Basically, it creates a lazy promise that defers execution until it's awaited or when .then() / .catch() is called, perfect for GraphQL Resolvers. 142 | 143 | > Internally, it uses [p-lazy](https://github.com/sindresorhus/p-lazy), which is also re-exported from this library 144 | 145 | ```ts 146 | import { LazyPromise } from 'mercurius-codegen' 147 | 148 | // ... 149 | 150 | // users == Promise 151 | const users = LazyPromise(() => { 152 | return db.users.findMany({ 153 | // ... 154 | }) 155 | }) 156 | ``` 157 | 158 | ### Options 159 | 160 | There are some extra options that can be specified: 161 | 162 | ```ts 163 | interface CodegenMercuriusOptions { 164 | /** 165 | * Specify the target path of the code generation. 166 | * 167 | * Relative to the directory of the executed script if targetPath isn't absolute 168 | * @example './src/graphql/generated.ts' 169 | */ 170 | targetPath: string 171 | /** 172 | * Disable the code generation manually 173 | * 174 | * @default process.env.NODE_ENV === 'production' 175 | */ 176 | disable?: boolean 177 | /** 178 | * Don't notify to the console 179 | */ 180 | silent?: boolean 181 | /** 182 | * Specify GraphQL Code Generator configuration 183 | * @example 184 | * codegenConfig: { 185 | * scalars: { 186 | * DateTime: "Date", 187 | * }, 188 | * } 189 | */ 190 | codegenConfig?: CodegenPluginsConfig 191 | /** 192 | * Add code at the beginning of the generated code 193 | */ 194 | preImportCode?: string 195 | /** 196 | * Operations glob patterns 197 | */ 198 | operationsGlob?: string[] | string 199 | /** 200 | * Watch Options for operations GraphQL files 201 | */ 202 | watchOptions?: { 203 | /** 204 | * Enable file watching 205 | * 206 | * @default false 207 | */ 208 | enabled?: boolean 209 | /** 210 | * Extra Chokidar options to be passed 211 | */ 212 | chokidarOptions?: ChokidarOptions 213 | /** 214 | * Unique watch instance 215 | * 216 | * `Especially useful for hot module replacement environments, preventing memory leaks` 217 | * 218 | * @default true 219 | */ 220 | uniqueWatch?: boolean 221 | } 222 | /** 223 | * Write the resulting schema as a `.gql` or `.graphql` schema file. 224 | * 225 | * If `true`, it outputs to `./schema.gql` 226 | * If a string it specified, it writes to that location 227 | * 228 | * @default false 229 | */ 230 | outputSchema?: boolean | string 231 | } 232 | 233 | codegenMercurius(app, { 234 | targetPath: './src/graphql/generated.ts', 235 | operationsGlob: ['./src/graphql/operations/*.gql'], 236 | disable: false, 237 | silent: true, 238 | codegenConfig: { 239 | scalars: { 240 | DateTime: 'Date', 241 | }, 242 | }, 243 | preImportCode: ` 244 | // Here you can put any code and it will be added at very beginning of the file 245 | `, 246 | watchOptions: { 247 | enabled: true, 248 | }, 249 | outputSchema: true, 250 | }).catch(console.error) 251 | ``` 252 | 253 | ### GraphQL Schema from files 254 | 255 | As shown in the [**examples/codegen-gql-files**](/examples/codegen-gql-files/src/index.ts), you can load all your schema type definitions directly from GraphQL _.gql_ files, and this library gives you a function that eases that process, allowing you to get the schema from files, watch for changes and preloading the schema for production environments. 256 | 257 | - Usage options 258 | 259 | ```ts 260 | export interface LoadSchemaOptions { 261 | /** 262 | * Watch options 263 | */ 264 | watchOptions?: { 265 | /** 266 | * Enable file watching 267 | * @default false 268 | */ 269 | enabled?: boolean 270 | /** 271 | * Custom function to be executed after schema change 272 | */ 273 | onChange?: (schema: string[]) => void 274 | /** 275 | * Extra Chokidar options to be passed 276 | */ 277 | chokidarOptions?: ChokidarOptions 278 | /** 279 | * Unique watch instance 280 | * 281 | * `Especially useful for hot module replacement environments, preventing memory leaks` 282 | * 283 | * @default true 284 | */ 285 | uniqueWatch?: boolean 286 | } 287 | /** 288 | * Pre-build options 289 | */ 290 | prebuild?: { 291 | /** 292 | * Enable use pre-built schema if found. 293 | * 294 | * @default process.env.NODE_ENV === "production" 295 | */ 296 | enabled?: boolean 297 | } 298 | /** 299 | * Don't notify the console 300 | */ 301 | silent?: boolean 302 | } 303 | ``` 304 | 305 | ```ts 306 | import Fastify from 'fastify' 307 | import mercurius from 'mercurius' 308 | import { buildSchema } from 'graphql' 309 | import { codegenMercurius, loadSchemaFiles } from 'mercurius-codegen' 310 | 311 | const app = Fastify() 312 | 313 | const { schema } = loadSchemaFiles('src/graphql/schema/**/*.gql', { 314 | watchOptions: { 315 | enabled: process.env.NODE_ENV === 'development', 316 | onChange(schema) { 317 | app.graphql.replaceSchema(buildSchema(schema.join('\n'))) 318 | app.graphql.defineResolvers(resolvers) 319 | 320 | codegenMercurius(app, { 321 | targetPath: './src/graphql/generated.ts', 322 | operationsGlob: './src/graphql/operations/*.gql', 323 | }).catch(console.error) 324 | }, 325 | }, 326 | }) 327 | 328 | app.register(mercurius, { 329 | schema, 330 | // .... 331 | }) 332 | ``` 333 | 334 | ## License 335 | 336 | MIT 337 | 338 | --- 339 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/ava.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extensions: ['ts'], 3 | require: ['ts-node/register'], 4 | timeout: '30s', 5 | } 6 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mercurius-codegen", 3 | "version": "6.0.1", 4 | "keywords": [ 5 | "fastify", 6 | "graphql", 7 | "gql", 8 | "mercurius", 9 | "typescript", 10 | "codegen" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/mercurius-js/mercurius-typescript" 15 | }, 16 | "license": "MIT", 17 | "author": "PabloSz ", 18 | "main": "dist/index.js", 19 | "types": "dist/index.d.ts", 20 | "files": [ 21 | "dist" 22 | ], 23 | "scripts": { 24 | "build": "tsc --removeComments && tsc --emitDeclarationOnly", 25 | "dev": "tsc --watch --preserveWatchOutput", 26 | "prepare": "tsc --removeComments && tsc --emitDeclarationOnly", 27 | "postpublish": "gh-release", 28 | "test": "cross-env TS_NODE_PROJECT=test/tsconfig.json c8 ava test/index.test.ts", 29 | "test:watch": "bob-watch -w src test package.json -c \"pnpm test\"", 30 | "watch": "concurrently -r pnpm:test:watch \"wait-on coverage && serve coverage/lcov-report\" \"wait-on -s 1 tcp:5000 && open-cli http://localhost:5000\"" 31 | }, 32 | "dependencies": { 33 | "@graphql-codegen/core": "^3.1.0", 34 | "@graphql-codegen/plugin-helpers": "^4.2.0", 35 | "@graphql-codegen/typed-document-node": "^4.0.1", 36 | "@graphql-codegen/typescript": "^3.0.4", 37 | "@graphql-codegen/typescript-operations": "^3.0.4", 38 | "@graphql-codegen/typescript-resolvers": "^3.2.1", 39 | "@graphql-codegen/visitor-plugin-common": "^3.1.1", 40 | "@graphql-tools/load-files": "^7.0.1", 41 | "@graphql-tools/utils": "^10.7.2", 42 | "@graphql-typed-document-node/core": "^3.2.0", 43 | "chokidar": "^3.6.0", 44 | "mkdirp": "^2.1.6" 45 | }, 46 | "devDependencies": { 47 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 48 | "@types/mkdirp": "^2.0.0", 49 | "@types/node": "^22.13.0", 50 | "@types/prettier": "^3.0.0", 51 | "@types/proxyquire": "^1.3.31", 52 | "@types/rimraf": "^3.0.2", 53 | "ava": "^6.2.0", 54 | "bob-watch": "^0.1.2", 55 | "c8": "^10.1.3", 56 | "changesets-github-release": "^0.1.0", 57 | "concurrently": "^9.1.2", 58 | "cross-env": "^7.0.3", 59 | "fastify": "^5.2.1", 60 | "graphql": "^16.10.0", 61 | "mercurius": "^16.0.1", 62 | "mercurius-codegen": "workspace:*", 63 | "nyc": "17.1.0", 64 | "open-cli": "^8.0.0", 65 | "prettier": "^3.4.2", 66 | "proxyquire": "^2.1.3", 67 | "rimraf": "^3.0.2", 68 | "serve": "^14.2.4", 69 | "tmp-promise": "^3.0.3", 70 | "ts-node": "^10.9.2", 71 | "typescript": "^5.7.3", 72 | "wait-for-expect": "^3.0.2", 73 | "wait-on": "^8.0.2" 74 | }, 75 | "peerDependencies": { 76 | "fastify": "^5.2.1", 77 | "graphql": "*", 78 | "mercurius": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", 79 | "prettier": "^3.4.2" 80 | }, 81 | "engines": { 82 | "pnpm": ">=9.15.5" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/src/code.ts: -------------------------------------------------------------------------------- 1 | import type {} from 'mercurius' 2 | import type { GraphQLSchema } from 'graphql' 3 | import type { TypeScriptPluginConfig } from '@graphql-codegen/typescript' 4 | import type { TypeScriptResolversPluginConfig } from '@graphql-codegen/typescript-resolvers' 5 | import type { CodegenPlugin } from '@graphql-codegen/plugin-helpers' 6 | import type { Source } from '@graphql-tools/utils' 7 | import type { MercuriusLoadersPluginConfig } from './mercuriusLoaders' 8 | 9 | import { dirname, resolve } from 'path' 10 | 11 | import { formatPrettier } from './prettier' 12 | import { writeFileIfChanged } from './write' 13 | import { toGraphQLString } from './utils' 14 | 15 | type MidCodegenPluginsConfig = TypeScriptPluginConfig & 16 | TypeScriptResolversPluginConfig & 17 | MercuriusLoadersPluginConfig 18 | 19 | export interface CodegenPluginsConfig extends MidCodegenPluginsConfig { 20 | [k: string]: unknown 21 | } 22 | 23 | export async function generateCode( 24 | schema: GraphQLSchema, 25 | codegenConfig: CodegenPluginsConfig = {}, 26 | preImportCode?: string, 27 | silent?: boolean, 28 | operationsGlob?: string[] | string, 29 | ) { 30 | const { codegen } = await import('@graphql-codegen/core') 31 | const typescriptPlugin = await import('@graphql-codegen/typescript') 32 | const typescriptResolversPlugin = await import( 33 | '@graphql-codegen/typescript-resolvers' 34 | ) 35 | const operationsPlugins = operationsGlob 36 | ? { 37 | typescriptOperations: await import( 38 | '@graphql-codegen/typescript-operations' 39 | ), 40 | typedDocumentNode: await import('@graphql-codegen/typed-document-node'), 41 | } 42 | : null 43 | const { parse } = await import('graphql') 44 | const { MercuriusLoadersPlugin } = await import('./mercuriusLoaders') 45 | const { loadFiles } = await import('@graphql-tools/load-files') 46 | const { printSchemaWithDirectives } = await import('@graphql-tools/utils') 47 | 48 | const documents = operationsGlob 49 | ? await loadFiles(operationsGlob).then((operations) => 50 | operations 51 | .map((op) => String(toGraphQLString(op)).trim()) 52 | .filter(Boolean) 53 | .map((operationString) => { 54 | const operationSource: Source = { 55 | document: parse(operationString), 56 | schema, 57 | } 58 | return operationSource 59 | }), 60 | ) 61 | : ([] as []) 62 | 63 | let code = preImportCode || '' 64 | 65 | if ( 66 | codegenConfig.namingConvention != null && 67 | codegenConfig.namingConvention !== 'keep' 68 | ) { 69 | if (!silent) { 70 | console.warn( 71 | `namingConvention "${codegenConfig.namingConvention}" is not supported! it has been set to "keep" automatically.`, 72 | ) 73 | } 74 | } 75 | 76 | code += await codegen({ 77 | config: Object.assign( 78 | { 79 | federation: true, 80 | contextType: 'mercurius#MercuriusContext', 81 | useTypeImports: true, 82 | customResolverFn: 83 | '(parent: TParent, args: TArgs, context: TContext, info: GraphQLResolveInfo) => Promise> | import("mercurius-codegen").DeepPartial', 84 | internalResolversPrefix: '', 85 | } as CodegenPluginsConfig, 86 | codegenConfig, 87 | { 88 | namingConvention: 'keep', 89 | } as CodegenPluginsConfig, 90 | ), 91 | documents, 92 | filename: 'mercurius.generated.ts', 93 | pluginMap: Object.assign( 94 | { 95 | typescript: typescriptPlugin, 96 | typescriptResolvers: typescriptResolversPlugin, 97 | mercuriusLoaders: MercuriusLoadersPlugin, 98 | }, 99 | operationsPlugins, 100 | ) as { 101 | [name: string]: CodegenPlugin 102 | }, 103 | plugins: [ 104 | { 105 | typescript: {}, 106 | }, 107 | { 108 | typescriptResolvers: {}, 109 | }, 110 | { 111 | mercuriusLoaders: {}, 112 | }, 113 | ...(operationsPlugins 114 | ? [ 115 | { 116 | typescriptOperations: {}, 117 | }, 118 | { 119 | typedDocumentNode: {}, 120 | }, 121 | ] 122 | : []), 123 | ], 124 | schema: parse(printSchemaWithDirectives(schema)), 125 | }) 126 | 127 | code += ` 128 | declare module "mercurius" { 129 | interface IResolvers extends Resolvers { } 130 | interface MercuriusLoaders extends Loaders { } 131 | } 132 | ` 133 | 134 | return await formatPrettier(code, 'typescript') 135 | } 136 | 137 | export async function writeGeneratedCode({ 138 | code, 139 | targetPath, 140 | }: { 141 | code: string 142 | targetPath: string 143 | }) { 144 | const { default: mkdirp } = await import('mkdirp') 145 | 146 | targetPath = resolve(targetPath) 147 | 148 | await mkdirp(dirname(targetPath)) 149 | 150 | return writeFileIfChanged(targetPath, code) 151 | } 152 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/src/index.ts: -------------------------------------------------------------------------------- 1 | import type {} from 'mercurius' 2 | import type { FastifyInstance } from 'fastify' 3 | import type { FSWatcher, WatchOptions as ChokidarOptions } from 'chokidar' 4 | import type { CodegenPluginsConfig } from './code' 5 | 6 | import { MercuriusLoadersPlugin } from './mercuriusLoaders' 7 | import { deferredPromise } from './utils' 8 | 9 | const { plugin } = MercuriusLoadersPlugin 10 | export { plugin } 11 | 12 | export type {} from '@graphql-codegen/plugin-helpers' 13 | 14 | export interface CodegenMercuriusOptions { 15 | /** 16 | * Specify the target path of the code generation. 17 | * 18 | * Relative to the directory of the executed script if targetPath isn't absolute 19 | * @example './src/graphql/generated.ts' 20 | */ 21 | targetPath: string 22 | /** 23 | * Disable the code generation manually 24 | * 25 | * @default process.env.NODE_ENV === 'production' 26 | */ 27 | disable?: boolean 28 | /** 29 | * Don't notify to the console 30 | */ 31 | silent?: boolean 32 | /** 33 | * Specify GraphQL Code Generator configuration 34 | * @example 35 | * codegenConfig: { 36 | * scalars: { 37 | * DateTime: "Date", 38 | * }, 39 | * } 40 | */ 41 | codegenConfig?: CodegenPluginsConfig 42 | /** 43 | * Add code in the beginning of the generated code 44 | */ 45 | preImportCode?: string 46 | /** 47 | * Operations glob patterns 48 | */ 49 | operationsGlob?: string[] | string 50 | /** 51 | * Watch Options for operations GraphQL files 52 | */ 53 | watchOptions?: { 54 | /** 55 | * Enable file watching 56 | * 57 | * @default false 58 | */ 59 | enabled?: boolean 60 | /** 61 | * Extra Chokidar options to be passed 62 | */ 63 | chokidarOptions?: ChokidarOptions 64 | /** 65 | * Unique watch instance 66 | * 67 | * `Specially useful for hot module replacement environments, preventing memory leaks` 68 | * 69 | * @default true 70 | */ 71 | uniqueWatch?: boolean 72 | } 73 | /** 74 | * Write the resulting schema as a `.gql` or `.graphql` schema file. 75 | * 76 | * If `true`, it outputs to `./schema.gql` 77 | * If a string it specified, it writes to that location 78 | * 79 | * @default false 80 | */ 81 | outputSchema?: boolean | string 82 | } 83 | 84 | declare const global: typeof globalThis & { 85 | mercuriusOperationsWatchCleanup?: () => void 86 | } 87 | 88 | export async function codegenMercurius( 89 | app: FastifyInstance, 90 | { 91 | disable = process.env.NODE_ENV === 'production', 92 | targetPath, 93 | silent, 94 | codegenConfig, 95 | preImportCode, 96 | operationsGlob, 97 | watchOptions, 98 | outputSchema = false, 99 | }: CodegenMercuriusOptions, 100 | ): Promise<{ 101 | closeWatcher: () => Promise 102 | watcher: Promise 103 | }> { 104 | const noopCloseWatcher = async () => false 105 | if (disable) { 106 | return { 107 | closeWatcher: noopCloseWatcher, 108 | watcher: Promise.resolve(undefined), 109 | } 110 | } 111 | 112 | await app.ready() 113 | 114 | if (typeof app.graphql !== 'function') { 115 | throw Error('Mercurius is not registered in Fastify Instance!') 116 | } 117 | 118 | const { generateCode, writeGeneratedCode } = await import('./code') 119 | const { writeOutputSchema } = await import('./outputSchema') 120 | 121 | return new Promise((resolve, reject) => { 122 | const log = (...message: Parameters<(typeof console)['log']>) => 123 | silent ? undefined : console.log(...message) 124 | 125 | setImmediate(() => { 126 | const schema = app.graphql.schema 127 | 128 | async function watchExecute() { 129 | const { 130 | enabled: watchEnabled = false, 131 | chokidarOptions, 132 | uniqueWatch = true, 133 | } = watchOptions || {} 134 | 135 | if (watchEnabled && operationsGlob) { 136 | const { watch } = await import('chokidar') 137 | 138 | let watcherPromise = deferredPromise() 139 | 140 | const watcher = watch( 141 | operationsGlob, 142 | Object.assign( 143 | { 144 | useFsEvents: false, 145 | } as ChokidarOptions, 146 | chokidarOptions, 147 | ), 148 | ) 149 | 150 | let isReady = false 151 | 152 | watcher.on('ready', () => { 153 | isReady = true 154 | log(`[mercurius-codegen] Watching for changes in ${operationsGlob}`) 155 | watcherPromise.resolve(watcher) 156 | }) 157 | 158 | watcher.on('error', watcherPromise.reject) 159 | 160 | let closed = false 161 | 162 | const closeWatcher = async () => { 163 | if (closed) return false 164 | 165 | closed = true 166 | await watcher.close() 167 | return true 168 | } 169 | 170 | if (uniqueWatch) { 171 | if (typeof global.mercuriusOperationsWatchCleanup === 'function') { 172 | global.mercuriusOperationsWatchCleanup() 173 | } 174 | 175 | global.mercuriusOperationsWatchCleanup = closeWatcher 176 | } 177 | 178 | const listener = ( 179 | eventName: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', 180 | changedPath: string, 181 | ) => { 182 | if (!isReady) return 183 | 184 | log( 185 | `[mercurius-codegen] ${changedPath} ${eventName}, re-generating...`, 186 | ) 187 | 188 | generateCode( 189 | schema, 190 | codegenConfig, 191 | preImportCode, 192 | silent, 193 | operationsGlob, 194 | ).then((code) => { 195 | writeGeneratedCode({ 196 | code, 197 | targetPath, 198 | }).then((absoluteTargetPath) => { 199 | log( 200 | `[mercurius-codegen] Code re-generated at ${absoluteTargetPath}`, 201 | ) 202 | }, console.error) 203 | }, console.error) 204 | } 205 | watcher.on('all', listener) 206 | 207 | return { 208 | closeWatcher, 209 | watcher: watcherPromise.promise, 210 | } 211 | } 212 | 213 | return { 214 | closeWatcher: noopCloseWatcher, 215 | watcher: Promise.resolve(undefined), 216 | } 217 | } 218 | 219 | writeOutputSchema(app, outputSchema).catch(reject) 220 | 221 | generateCode( 222 | schema, 223 | codegenConfig, 224 | preImportCode, 225 | silent, 226 | operationsGlob, 227 | ).then((code) => { 228 | writeGeneratedCode({ 229 | code, 230 | targetPath, 231 | }).then((absoluteTargetPath) => { 232 | log(`[mercurius-codegen] Code generated at ${absoluteTargetPath}`) 233 | 234 | watchExecute().then((watchResult) => { 235 | resolve(watchResult) 236 | }, reject) 237 | }, reject) 238 | }, reject) 239 | }) 240 | }) 241 | } 242 | 243 | export default codegenMercurius 244 | 245 | export { gql, DeepPartial, LazyPromise, PLazy } from './utils' 246 | export { CodegenPluginsConfig, generateCode, writeGeneratedCode } from './code' 247 | 248 | export const loadSchemaFiles: typeof import('./schema').loadSchemaFiles = ( 249 | ...args 250 | ) => require('./schema').loadSchemaFiles(...args) 251 | 252 | export type { LoadSchemaOptions, PrebuildOptions } from './schema' 253 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/src/mercuriusLoaders.ts: -------------------------------------------------------------------------------- 1 | import { CodegenPlugin } from '@graphql-codegen/plugin-helpers' 2 | import { GraphQLField, GraphQLType } from 'graphql' 3 | 4 | import type { RawResolversConfig } from '@graphql-codegen/visitor-plugin-common' 5 | 6 | /** 7 | * Subset of Config Options that are supported by this plugin 8 | */ 9 | export type SupportedConfigOptions = Pick< 10 | RawResolversConfig, 11 | 'addUnderscoreToArgsType' 12 | > 13 | 14 | export interface MercuriusLoadersPluginConfig extends SupportedConfigOptions { 15 | namespacedImportName?: string 16 | loadersCustomParentTypes?: Record 17 | } 18 | 19 | export const MercuriusLoadersPlugin: CodegenPlugin = 20 | { 21 | async plugin(schema, _documents, config) { 22 | const { 23 | GraphQLList, 24 | GraphQLObjectType, 25 | GraphQLScalarType, 26 | isNonNullType, 27 | isListType, 28 | } = await import('graphql') 29 | 30 | const namespacedImportPrefix = config.namespacedImportName 31 | ? `${config.namespacedImportName}.` 32 | : '' 33 | 34 | const schemaConfig = schema.toConfig() 35 | 36 | const queryType = schema.getQueryType() 37 | const mutationType = schema.getMutationType() 38 | const subscriptionType = schema.getSubscriptionType() 39 | 40 | let code = ` 41 | export type Loader = ( 42 | queries: Array<{ 43 | obj: TObj; 44 | params: TParams; 45 | }>, 46 | context: TContext & { 47 | reply: import("fastify").FastifyReply; 48 | } 49 | ) => Promise>>; 50 | export type LoaderResolver = 51 | Loader | { 52 | loader: Loader; 53 | opts?:{ 54 | cache?:boolean 55 | }; 56 | } 57 | ` 58 | const loaders: Record> = {} 59 | 60 | function fieldToType( 61 | field: GraphQLField | GraphQLType, 62 | typeAcumStart: string = '', 63 | typeAcumEnd: string = '', 64 | ): string { 65 | let isNullable = true 66 | let isArray = false 67 | let nullableItems = true 68 | 69 | let fieldType: GraphQLType 70 | 71 | if ('args' in field) { 72 | fieldType = field.type 73 | } else { 74 | fieldType = field 75 | } 76 | 77 | if (isNonNullType(fieldType)) { 78 | isNullable = false 79 | fieldType = fieldType.ofType 80 | } 81 | 82 | if (isListType(fieldType)) { 83 | fieldType = fieldType.ofType 84 | isArray = true 85 | if (isNonNullType(fieldType)) { 86 | nullableItems = false 87 | fieldType = fieldType.ofType 88 | } 89 | } 90 | 91 | if (isNullable) { 92 | typeAcumStart += 'Maybe<' 93 | typeAcumEnd += '>' 94 | } 95 | 96 | if (isArray) { 97 | typeAcumStart += 'Array<' 98 | typeAcumEnd += '>' 99 | if (nullableItems && !(fieldType instanceof GraphQLList)) { 100 | typeAcumStart += 'Maybe<' 101 | typeAcumEnd += '>' 102 | } 103 | } 104 | 105 | if (isListType(fieldType)) { 106 | return fieldToType(fieldType.ofType, typeAcumStart, typeAcumEnd) 107 | } else if (fieldType instanceof GraphQLScalarType) { 108 | return typeAcumStart + `Scalars["${fieldType.name}"]` + typeAcumEnd 109 | } else { 110 | return typeAcumStart + fieldType.name + typeAcumEnd 111 | } 112 | } 113 | 114 | schemaConfig.types.forEach((type) => { 115 | switch (type) { 116 | case queryType: 117 | case mutationType: 118 | case subscriptionType: 119 | return 120 | } 121 | 122 | if (type.name.startsWith('__')) return 123 | 124 | if (type instanceof GraphQLObjectType) { 125 | const fields = type.getFields() 126 | 127 | const typeCode: Record = {} 128 | Object.entries(fields).forEach(([key, value]) => { 129 | const tsType = fieldToType(value) 130 | 131 | const hasArgs = value.args.length > 0 132 | 133 | typeCode[key] = 134 | `LoaderResolver<${tsType},${namespacedImportPrefix}${ 135 | config.loadersCustomParentTypes?.[type.name] || type.name 136 | },${ 137 | hasArgs 138 | ? `${namespacedImportPrefix}${type.name}${ 139 | config.addUnderscoreToArgsType ? '_' : '' 140 | }${value.name}Args` 141 | : '{}' 142 | }, TContext>` 143 | }) 144 | 145 | loaders[type.name] = typeCode 146 | } 147 | }) 148 | 149 | let hasLoaders = false 150 | 151 | code += `export interface Loaders {` 152 | Object.entries(loaders).map(([key, value]) => { 153 | hasLoaders = true 154 | code += ` 155 | ${key}?: { 156 | ${Object.entries(value).reduce((acum, [key, value]) => { 157 | acum += `${key}?: ${value};` 158 | return acum 159 | }, '')} 160 | }; 161 | ` 162 | }) 163 | 164 | code += `}` 165 | 166 | return hasLoaders ? code : 'export interface Loaders {};\n' 167 | }, 168 | } 169 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/src/outputSchema.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance } from 'fastify' 2 | import { resolve } from 'path' 3 | 4 | import { formatPrettier } from './prettier' 5 | import { writeFileIfChanged } from './write' 6 | 7 | export async function writeOutputSchema( 8 | app: FastifyInstance, 9 | config: string | boolean, 10 | ) { 11 | if (!config) return 12 | 13 | let targetPath: string 14 | if (typeof config === 'boolean') { 15 | targetPath = resolve('./schema.gql') 16 | } else { 17 | targetPath = resolve(config) 18 | } 19 | 20 | const { printSchemaWithDirectives } = await import('@graphql-tools/utils') 21 | 22 | const schema = await formatPrettier( 23 | printSchemaWithDirectives(app.graphql.schema), 24 | 'graphql', 25 | ) 26 | 27 | await writeFileIfChanged(targetPath, schema) 28 | } 29 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/src/prettier.ts: -------------------------------------------------------------------------------- 1 | import type { BuiltInParserName } from 'prettier' 2 | 3 | export async function formatPrettier(str: string, parser: BuiltInParserName) { 4 | const { format, resolveConfig } = await import('prettier') 5 | 6 | const prettierConfig = Object.assign({}, await resolveConfig(process.cwd())) 7 | 8 | return format(str, { 9 | parser, 10 | ...prettierConfig, 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/src/schema.ts: -------------------------------------------------------------------------------- 1 | import type { FSWatcher, WatchOptions as ChokidarOptions } from 'chokidar' 2 | import { existsSync } from 'fs' 3 | import { resolve } from 'path' 4 | 5 | import { formatPrettier } from './prettier' 6 | import { deferredPromise, toGraphQLString } from './utils' 7 | import { writeFileIfChanged } from './write' 8 | 9 | export interface PrebuildOptions { 10 | /** 11 | * Enable use pre-built schema if found. 12 | * 13 | * @default process.env.NODE_ENV === "production" 14 | */ 15 | enabled?: boolean 16 | 17 | /** 18 | * If "targetPath" is set to `null`, pre-built schema generation is disabled 19 | * @default 20 | * "./mercurius-schema.json" 21 | */ 22 | targetPath?: string | null 23 | } 24 | 25 | export interface WatchOptions { 26 | /** 27 | * Enable file watching 28 | * @default false 29 | */ 30 | enabled?: boolean 31 | /** 32 | * Custom function to be executed after schema change 33 | */ 34 | onChange?: (schema: string[]) => void 35 | /** 36 | * Extra Chokidar options to be passed 37 | */ 38 | chokidarOptions?: ChokidarOptions 39 | /** 40 | * Unique watch instance 41 | * 42 | * `Specially useful for hot module replacement environments, preventing memory leaks` 43 | * 44 | * @default true 45 | */ 46 | uniqueWatch?: boolean 47 | } 48 | 49 | export interface LoadSchemaOptions { 50 | /** 51 | * Watch options 52 | */ 53 | watchOptions?: WatchOptions 54 | /** 55 | * Pre-build options 56 | */ 57 | prebuild?: PrebuildOptions 58 | /** 59 | * Don't notify to the console 60 | */ 61 | silent?: boolean 62 | } 63 | 64 | declare const global: typeof globalThis & { 65 | mercuriusLoadSchemaWatchCleanup?: () => void 66 | } 67 | 68 | function isValidSchemaList(v: unknown): v is Array { 69 | return Array.isArray(v) && !!v.length && v.every((v) => typeof v === 'string') 70 | } 71 | 72 | export function loadSchemaFiles( 73 | schemaPath: string | string[], 74 | { watchOptions = {}, prebuild = {}, silent }: LoadSchemaOptions = {}, 75 | ) { 76 | const log = (...message: Parameters<(typeof console)['log']>) => 77 | silent ? undefined : console.log(...message) 78 | 79 | const { 80 | enabled: prebuildEnabled = process.env.NODE_ENV === 'production', 81 | targetPath: prebuiltSchemaPathOption = './mercurius-schema.json', 82 | } = prebuild 83 | 84 | const prebuiltSchemaPath = 85 | prebuiltSchemaPathOption != null ? resolve(prebuiltSchemaPathOption) : null 86 | 87 | function loadSchemaFiles() { 88 | const { 89 | loadFilesSync, 90 | }: typeof import('@graphql-tools/load-files') = require('@graphql-tools/load-files') 91 | 92 | const schema = loadFilesSync(schemaPath, {}) 93 | .map((v) => { 94 | return String(toGraphQLString(v)).trim().replace(/\r\n/g, '\n') 95 | }) 96 | .filter(Boolean) 97 | 98 | if (!schema.length) { 99 | const err = Error('No GraphQL Schema files found!') 100 | 101 | Error.captureStackTrace(err, loadSchemaFiles) 102 | 103 | throw err 104 | } 105 | 106 | if (prebuiltSchemaPath == null) return schema 107 | 108 | const schemaStringPromise = formatPrettier(JSON.stringify(schema), 'json') 109 | 110 | schemaStringPromise.then((schemaString) => { 111 | writeFileIfChanged(prebuiltSchemaPath, schemaString).catch(console.error) 112 | }, console.error) 113 | 114 | return schema 115 | } 116 | 117 | let schema: string[] | undefined 118 | 119 | if (prebuiltSchemaPath != null && prebuildEnabled) { 120 | if (existsSync(prebuiltSchemaPath)) { 121 | const prebuiltSchema = require(prebuiltSchemaPath) 122 | if (isValidSchemaList(prebuiltSchema)) { 123 | schema = prebuiltSchema 124 | } 125 | } 126 | } 127 | 128 | if (!schema) schema = loadSchemaFiles() 129 | 130 | let closeWatcher = async () => false 131 | 132 | const { 133 | enabled: watchEnabled = false, 134 | chokidarOptions, 135 | uniqueWatch = true, 136 | } = watchOptions 137 | 138 | let watcherPromise: Promise 139 | 140 | if (watchEnabled) { 141 | const { watch }: typeof import('chokidar') = require('chokidar') 142 | 143 | const watcher = watch( 144 | schemaPath, 145 | Object.assign({ useFsEvents: false } as ChokidarOptions, chokidarOptions), 146 | ) 147 | 148 | const watcherToResolve = deferredPromise() 149 | watcherPromise = watcherToResolve.promise 150 | 151 | let isReady = false 152 | 153 | let closed = false 154 | closeWatcher = async () => { 155 | if (closed) return false 156 | 157 | closed = true 158 | await watcher.close() 159 | return true 160 | } 161 | 162 | if (uniqueWatch) { 163 | if (typeof global.mercuriusLoadSchemaWatchCleanup === 'function') { 164 | global.mercuriusLoadSchemaWatchCleanup() 165 | } 166 | 167 | global.mercuriusLoadSchemaWatchCleanup = closeWatcher 168 | } 169 | 170 | watcher.on('ready', () => { 171 | isReady = true 172 | log(`[mercurius-codegen] Watching for changes in ${schemaPath}`) 173 | watcherToResolve.resolve(watcher) 174 | }) 175 | 176 | watcher.on('error', watcherToResolve.reject) 177 | 178 | const listener = ( 179 | eventName: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', 180 | changedPath: string, 181 | ) => { 182 | if (!isReady) return 183 | 184 | log( 185 | `[mercurius-codegen] ${changedPath} ${eventName}, loading new schema...`, 186 | ) 187 | 188 | try { 189 | const schema = loadSchemaFiles() 190 | 191 | if (watchOptions.onChange) watchOptions.onChange(schema) 192 | } catch (err) { 193 | console.error(err) 194 | } 195 | } 196 | 197 | watcher.on('all', listener) 198 | } else { 199 | watcherPromise = Promise.resolve(undefined) 200 | } 201 | 202 | return { 203 | schema, 204 | closeWatcher, 205 | watcher: watcherPromise, 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ASTNode, print } from 'graphql' 2 | 3 | export const toGraphQLString = (v: string | ASTNode) => 4 | typeof v === 'string' ? v : print(v) 5 | export class PLazy extends Promise { 6 | private _executor 7 | private _promise?: Promise 8 | 9 | constructor( 10 | executor: ( 11 | resolve: (value: ValueType) => void, 12 | reject: (err: unknown) => void, 13 | ) => void, 14 | ) { 15 | super((resolve: (v?: any) => void) => resolve()) 16 | 17 | this._executor = executor 18 | } 19 | 20 | then: Promise['then'] = (onFulfilled, onRejected) => { 21 | this._promise = this._promise || new Promise(this._executor) 22 | return this._promise.then(onFulfilled, onRejected) 23 | } 24 | 25 | catch: Promise['catch'] = (onRejected) => { 26 | this._promise = this._promise || new Promise(this._executor) 27 | return this._promise.catch(onRejected) 28 | } 29 | 30 | finally: Promise['finally'] = (onFinally) => { 31 | this._promise = this._promise || new Promise(this._executor) 32 | return this._promise.finally(onFinally) 33 | } 34 | } 35 | 36 | export function gql(chunks: TemplateStringsArray, ...variables: any[]): string { 37 | return chunks.reduce( 38 | (accumulator, chunk, index) => 39 | `${accumulator}${chunk}${index in variables ? variables[index] : ''}`, 40 | '', 41 | ) 42 | } 43 | 44 | export function deferredPromise() { 45 | let resolve: (value: T | PromiseLike) => void 46 | let reject: (reason?: any) => void 47 | const promise = new Promise((resolveFn, rejectFn) => { 48 | resolve = resolveFn 49 | reject = rejectFn 50 | }) 51 | 52 | return { 53 | promise, 54 | resolve: resolve!, 55 | reject: reject!, 56 | } 57 | } 58 | 59 | export function LazyPromise( 60 | fn: () => Value | Promise, 61 | ): Promise { 62 | return new PLazy((resolve, reject) => { 63 | try { 64 | const value = fn() 65 | if (value instanceof Promise) { 66 | value.then(resolve, (err) => { 67 | if (err instanceof Error) Error.captureStackTrace(err, LazyPromise) 68 | 69 | reject(err) 70 | }) 71 | } else resolve(value) 72 | } catch (err) { 73 | if (err instanceof Error) Error.captureStackTrace(err, LazyPromise) 74 | 75 | reject(err) 76 | } 77 | }) 78 | } 79 | 80 | type PossiblePromise = T | Promise 81 | 82 | export type DeepPartial = T extends Function 83 | ? T 84 | : T extends Array 85 | ? DeepPartialArray 86 | : T extends object 87 | ? DeepPartialObject 88 | : T | undefined 89 | 90 | interface DeepPartialArray 91 | extends Array>>> {} 92 | type DeepPartialObject = { 93 | [P in keyof T]?: PossiblePromise>> 94 | } 95 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/src/write.ts: -------------------------------------------------------------------------------- 1 | import { promises, existsSync } from 'fs' 2 | 3 | /** 4 | * Write the target file only if the content changed, 5 | * It always returns the target path 6 | */ 7 | export async function writeFileIfChanged(targetPath: string, content: string) { 8 | const fileExists = existsSync(targetPath) 9 | 10 | if (fileExists) { 11 | const existingContent = await promises.readFile(targetPath, { 12 | encoding: 'utf-8', 13 | }) 14 | 15 | if (existingContent === content) return targetPath 16 | } 17 | 18 | await promises.writeFile(targetPath, content, { 19 | encoding: 'utf-8', 20 | }) 21 | 22 | return targetPath 23 | } 24 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/test/generated.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GraphQLResolveInfo, 3 | GraphQLScalarType, 4 | GraphQLScalarTypeConfig, 5 | } from 'graphql' 6 | import type { MercuriusContext } from 'mercurius' 7 | import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' 8 | export type Maybe = T | null 9 | export type InputMaybe = Maybe 10 | export type Exact = { 11 | [K in keyof T]: T[K] 12 | } 13 | export type MakeOptional = Omit & { 14 | [SubKey in K]?: Maybe 15 | } 16 | export type MakeMaybe = Omit & { 17 | [SubKey in K]: Maybe 18 | } 19 | export type ResolverFn = ( 20 | parent: TParent, 21 | args: TArgs, 22 | context: TContext, 23 | info: GraphQLResolveInfo, 24 | ) => 25 | | Promise> 26 | | import('mercurius-codegen').DeepPartial 27 | export type RequireFields = Omit & { 28 | [P in K]-?: NonNullable 29 | } 30 | /** All built-in and custom scalars, mapped to their actual values */ 31 | export type Scalars = { 32 | ID: string 33 | String: string 34 | Boolean: boolean 35 | Int: number 36 | Float: number 37 | DateTime: Date 38 | _FieldSet: any 39 | } 40 | 41 | export type Query = { 42 | __typename?: 'Query' 43 | hello: Scalars['String'] 44 | aHuman: Human 45 | getNArray?: Maybe 46 | } 47 | 48 | export type QueryhelloArgs = { 49 | greetings?: InputMaybe 50 | } 51 | 52 | export type Human = { 53 | __typename?: 'Human' 54 | name: Scalars['String'] 55 | father?: Maybe 56 | hasSon?: Maybe 57 | sons: Array> 58 | confirmedSonsNullable?: Maybe> 59 | confirmedSonsNonNullItems: Array 60 | sonNames?: Maybe>> 61 | nonNullssonNames: Array 62 | } 63 | 64 | export type HumanhasSonArgs = { 65 | name?: InputMaybe 66 | } 67 | 68 | export type HumansonsArgs = { 69 | name: Scalars['String'] 70 | } 71 | 72 | export type HumanconfirmedSonsNullableArgs = { 73 | name: Scalars['String'] 74 | } 75 | 76 | export type HumanconfirmedSonsNonNullItemsArgs = { 77 | name: Scalars['String'] 78 | } 79 | 80 | export type NArray = { 81 | __typename?: 'NArray' 82 | nArray?: Maybe>>>>>> 83 | } 84 | 85 | export type ResolverTypeWrapper = Promise | T 86 | 87 | export type ResolverWithResolve = { 88 | resolve: ResolverFn 89 | } 90 | export type Resolver = 91 | | ResolverFn 92 | | ResolverWithResolve 93 | 94 | export type SubscriptionSubscribeFn = ( 95 | parent: TParent, 96 | args: TArgs, 97 | context: TContext, 98 | info: GraphQLResolveInfo, 99 | ) => AsyncIterable | Promise> 100 | 101 | export type SubscriptionResolveFn = ( 102 | parent: TParent, 103 | args: TArgs, 104 | context: TContext, 105 | info: GraphQLResolveInfo, 106 | ) => TResult | Promise 107 | 108 | export interface SubscriptionSubscriberObject< 109 | TResult, 110 | TKey extends string, 111 | TParent, 112 | TContext, 113 | TArgs, 114 | > { 115 | subscribe: SubscriptionSubscribeFn< 116 | { [key in TKey]: TResult }, 117 | TParent, 118 | TContext, 119 | TArgs 120 | > 121 | resolve?: SubscriptionResolveFn< 122 | TResult, 123 | { [key in TKey]: TResult }, 124 | TContext, 125 | TArgs 126 | > 127 | } 128 | 129 | export interface SubscriptionResolverObject { 130 | subscribe: SubscriptionSubscribeFn 131 | resolve: SubscriptionResolveFn 132 | } 133 | 134 | export type SubscriptionObject< 135 | TResult, 136 | TKey extends string, 137 | TParent, 138 | TContext, 139 | TArgs, 140 | > = 141 | | SubscriptionSubscriberObject 142 | | SubscriptionResolverObject 143 | 144 | export type SubscriptionResolver< 145 | TResult, 146 | TKey extends string, 147 | TParent = {}, 148 | TContext = {}, 149 | TArgs = {}, 150 | > = 151 | | (( 152 | ...args: any[] 153 | ) => SubscriptionObject) 154 | | SubscriptionObject 155 | 156 | export type TypeResolveFn = ( 157 | parent: TParent, 158 | context: TContext, 159 | info: GraphQLResolveInfo, 160 | ) => Maybe | Promise> 161 | 162 | export type IsTypeOfResolverFn = ( 163 | obj: T, 164 | context: TContext, 165 | info: GraphQLResolveInfo, 166 | ) => boolean | Promise 167 | 168 | export type NextResolverFn = () => Promise 169 | 170 | export type DirectiveResolverFn< 171 | TResult = {}, 172 | TParent = {}, 173 | TContext = {}, 174 | TArgs = {}, 175 | > = ( 176 | next: NextResolverFn, 177 | parent: TParent, 178 | args: TArgs, 179 | context: TContext, 180 | info: GraphQLResolveInfo, 181 | ) => TResult | Promise 182 | 183 | /** Mapping between all available schema types and the resolvers types */ 184 | export type ResolversTypes = { 185 | DateTime: ResolverTypeWrapper 186 | Query: ResolverTypeWrapper<{}> 187 | String: ResolverTypeWrapper 188 | Human: ResolverTypeWrapper 189 | Boolean: ResolverTypeWrapper 190 | NArray: ResolverTypeWrapper 191 | Int: ResolverTypeWrapper 192 | } 193 | 194 | /** Mapping between all available schema types and the resolvers parents */ 195 | export type ResolversParentTypes = { 196 | DateTime: Scalars['DateTime'] 197 | Query: {} 198 | String: Scalars['String'] 199 | Human: Human 200 | Boolean: Scalars['Boolean'] 201 | NArray: NArray 202 | Int: Scalars['Int'] 203 | } 204 | 205 | export interface DateTimeScalarConfig 206 | extends GraphQLScalarTypeConfig { 207 | name: 'DateTime' 208 | } 209 | 210 | export type QueryResolvers< 211 | ContextType = MercuriusContext, 212 | ParentType extends 213 | ResolversParentTypes['Query'] = ResolversParentTypes['Query'], 214 | > = { 215 | hello?: Resolver< 216 | ResolversTypes['String'], 217 | ParentType, 218 | ContextType, 219 | Partial 220 | > 221 | aHuman?: Resolver 222 | getNArray?: Resolver, ParentType, ContextType> 223 | } 224 | 225 | export type HumanResolvers< 226 | ContextType = MercuriusContext, 227 | ParentType extends 228 | ResolversParentTypes['Human'] = ResolversParentTypes['Human'], 229 | > = { 230 | name?: Resolver 231 | father?: Resolver, ParentType, ContextType> 232 | hasSon?: Resolver< 233 | Maybe, 234 | ParentType, 235 | ContextType, 236 | Partial 237 | > 238 | sons?: Resolver< 239 | Array>, 240 | ParentType, 241 | ContextType, 242 | RequireFields 243 | > 244 | confirmedSonsNullable?: Resolver< 245 | Maybe>, 246 | ParentType, 247 | ContextType, 248 | RequireFields 249 | > 250 | confirmedSonsNonNullItems?: Resolver< 251 | Array, 252 | ParentType, 253 | ContextType, 254 | RequireFields 255 | > 256 | sonNames?: Resolver< 257 | Maybe>>, 258 | ParentType, 259 | ContextType 260 | > 261 | nonNullssonNames?: Resolver< 262 | Array, 263 | ParentType, 264 | ContextType 265 | > 266 | isTypeOf?: IsTypeOfResolverFn 267 | } 268 | 269 | export type NArrayResolvers< 270 | ContextType = MercuriusContext, 271 | ParentType extends 272 | ResolversParentTypes['NArray'] = ResolversParentTypes['NArray'], 273 | > = { 274 | nArray?: Resolver< 275 | Maybe>>>>>>, 276 | ParentType, 277 | ContextType 278 | > 279 | isTypeOf?: IsTypeOfResolverFn 280 | } 281 | 282 | export type Resolvers = { 283 | DateTime?: GraphQLScalarType 284 | Query?: QueryResolvers 285 | Human?: HumanResolvers 286 | NArray?: NArrayResolvers 287 | } 288 | 289 | export type Loader = ( 290 | queries: Array<{ 291 | obj: TObj 292 | params: TParams 293 | }>, 294 | context: TContext & { 295 | reply: import('fastify').FastifyReply 296 | }, 297 | ) => Promise>> 298 | export type LoaderResolver = 299 | | Loader 300 | | { 301 | loader: Loader 302 | opts?: { 303 | cache?: boolean 304 | } 305 | } 306 | export interface Loaders< 307 | TContext = import('mercurius').MercuriusContext & { 308 | reply: import('fastify').FastifyReply 309 | }, 310 | > { 311 | Human?: { 312 | name?: LoaderResolver 313 | father?: LoaderResolver, Human, {}, TContext> 314 | hasSon?: LoaderResolver< 315 | Maybe, 316 | Human, 317 | HumanhasSonArgs, 318 | TContext 319 | > 320 | sons?: LoaderResolver>, Human, HumansonsArgs, TContext> 321 | confirmedSonsNullable?: LoaderResolver< 322 | Maybe>, 323 | Human, 324 | HumanconfirmedSonsNullableArgs, 325 | TContext 326 | > 327 | confirmedSonsNonNullItems?: LoaderResolver< 328 | Array, 329 | Human, 330 | HumanconfirmedSonsNonNullItemsArgs, 331 | TContext 332 | > 333 | sonNames?: LoaderResolver< 334 | Maybe>>, 335 | Human, 336 | {}, 337 | TContext 338 | > 339 | nonNullssonNames?: LoaderResolver< 340 | Array, 341 | Human, 342 | {}, 343 | TContext 344 | > 345 | } 346 | 347 | NArray?: { 348 | nArray?: LoaderResolver< 349 | Maybe>>>>, 350 | NArray, 351 | {}, 352 | TContext 353 | > 354 | } 355 | } 356 | export type AQueryVariables = Exact<{ [key: string]: never }> 357 | 358 | export type AQuery = { __typename?: 'Query'; hello: string } 359 | 360 | export const ADocument = { 361 | kind: 'Document', 362 | definitions: [ 363 | { 364 | kind: 'OperationDefinition', 365 | operation: 'query', 366 | name: { kind: 'Name', value: 'A' }, 367 | selectionSet: { 368 | kind: 'SelectionSet', 369 | selections: [{ kind: 'Field', name: { kind: 'Name', value: 'hello' } }], 370 | }, 371 | }, 372 | ], 373 | } as unknown as DocumentNode 374 | declare module 'mercurius' { 375 | interface IResolvers 376 | extends Resolvers {} 377 | interface MercuriusLoaders extends Loaders {} 378 | } 379 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/test/index.test.ts: -------------------------------------------------------------------------------- 1 | //import './generated' 2 | 3 | import assert from 'assert' 4 | import test from 'ava' 5 | import Fastify from 'fastify' 6 | import fs from 'fs' 7 | import { parse, print, buildSchema } from 'graphql' 8 | import mercurius, { IResolvers, MercuriusLoaders } from 'mercurius' 9 | import mkdirp from 'mkdirp' 10 | import path from 'path' 11 | import proxyquire from 'proxyquire' 12 | import rimraf from 'rimraf' 13 | import tmp from 'tmp-promise' 14 | import waitForExpect from 'wait-for-expect' 15 | 16 | import { 17 | codegenMercurius, 18 | generateCode, 19 | gql, 20 | loadSchemaFiles, 21 | writeGeneratedCode, 22 | plugin as loadersPlugin, 23 | PLazy, 24 | LazyPromise, 25 | } from '../src/index' 26 | import { writeOutputSchema } from '../src/outputSchema' 27 | import { formatPrettier } from '../src/prettier' 28 | 29 | const { readFile, writeFile, rm } = fs.promises 30 | 31 | const buildJSONPath = path.resolve('./mercurius-schema.json') 32 | 33 | test.after.always(async () => { 34 | await rm(buildJSONPath, { 35 | force: true, 36 | }) 37 | 38 | await new Promise((resolve, reject) => { 39 | rimraf('./tmp', (err) => { 40 | if (err) { 41 | return reject(err) 42 | } 43 | resolve() 44 | }) 45 | }) 46 | }) 47 | 48 | const appWithoutMercurius = Fastify() 49 | const app = Fastify() 50 | 51 | const schema = gql` 52 | scalar DateTime 53 | type Query { 54 | hello(greetings: String): String! 55 | aHuman: Human! 56 | getNArray: NArray 57 | } 58 | type Human { 59 | name: String! 60 | father: Human 61 | hasSon(name: String): Boolean 62 | sons(name: String!): [Human]! 63 | confirmedSonsNullable(name: String!): [Human!] 64 | confirmedSonsNonNullItems(name: String!): [Human!]! 65 | sonNames: [String] 66 | nonNullssonNames: [String!]! 67 | } 68 | 69 | type NArray { 70 | nArray: [[[Int]]] 71 | } 72 | ` 73 | 74 | const resolvers: IResolvers = { 75 | Query: { 76 | hello(_root, { greetings }, _ctx) { 77 | return greetings || 'Hello world!' 78 | }, 79 | }, 80 | } 81 | 82 | const loaders: MercuriusLoaders = { 83 | Human: { 84 | async name(queries, _ctx) { 85 | return queries.map(({ obj, params }) => { 86 | return 'name' 87 | }) 88 | }, 89 | async father(queries, _ctx) { 90 | queries.map(({ obj, params }) => {}) 91 | return [ 92 | { 93 | name: 'asd', 94 | }, 95 | ] 96 | }, 97 | hasSon: { 98 | async loader(queries, _ctx) { 99 | return queries.map((value, key) => { 100 | return true 101 | }) 102 | }, 103 | }, 104 | }, 105 | } 106 | 107 | app.register(mercurius, { 108 | schema, 109 | resolvers, 110 | loaders, 111 | }) 112 | 113 | let generatedCode: string 114 | 115 | test('generates code via plugin', async (t) => { 116 | t.plan(1) 117 | await app.ready() 118 | const pluginOutput = await loadersPlugin(app.graphql.schema, [], { 119 | namespacedImportName: 'TP_Types', 120 | }) 121 | 122 | t.snapshot(pluginOutput.toString(), 'pluginOutput') 123 | }) 124 | 125 | test.serial('generates code', async (t) => { 126 | await app.ready() 127 | generatedCode = await generateCode(app.graphql.schema) 128 | 129 | t.snapshot(generatedCode, 'code') 130 | }) 131 | test.serial('integrates with mercurius', async (t) => { 132 | t.plan(2) 133 | const tempTargetPath = await tmp.file() 134 | 135 | const prevConsoleLog = console.log 136 | 137 | const mockConsoleLog = (message: string) => { 138 | t.assert(message.includes('Code generated at')) 139 | } 140 | 141 | console.log = mockConsoleLog 142 | 143 | t.teardown(() => { 144 | console.log = prevConsoleLog 145 | }) 146 | 147 | t.teardown(async () => { 148 | await tempTargetPath.cleanup() 149 | }) 150 | 151 | await codegenMercurius(app, { 152 | targetPath: tempTargetPath.path, 153 | disable: false, 154 | silent: false, 155 | }) 156 | 157 | t.is( 158 | generatedCode, 159 | await readFile(tempTargetPath.path, { 160 | encoding: 'utf-8', 161 | }), 162 | ) 163 | }) 164 | 165 | test.serial('integrates with mercurius and respects silent', async (t) => { 166 | t.plan(1) 167 | const tempTargetPath = await tmp.file() 168 | 169 | const prevConsoleLog = console.log 170 | 171 | const mockConsoleLog = () => { 172 | t.fail("shouldn't reach it") 173 | } 174 | 175 | console.log = mockConsoleLog 176 | 177 | t.teardown(() => { 178 | console.log = prevConsoleLog 179 | }) 180 | 181 | t.teardown(async () => { 182 | await tempTargetPath.cleanup() 183 | }) 184 | 185 | await codegenMercurius(app, { 186 | targetPath: tempTargetPath.path, 187 | disable: false, 188 | silent: true, 189 | }) 190 | 191 | t.is( 192 | generatedCode, 193 | await readFile(tempTargetPath.path, { 194 | encoding: 'utf-8', 195 | }), 196 | ) 197 | }) 198 | 199 | test('writes generated code', async (t) => { 200 | t.plan(1) 201 | const code = 'console.log("hello world")' 202 | const tempTargetPath = await tmp.file() 203 | 204 | t.teardown(async () => { 205 | await tempTargetPath.cleanup() 206 | }) 207 | 208 | await writeGeneratedCode({ 209 | code, 210 | targetPath: tempTargetPath.path, 211 | }) 212 | 213 | t.is( 214 | code, 215 | await readFile(tempTargetPath.path, { 216 | encoding: 'utf-8', 217 | }), 218 | ) 219 | }) 220 | 221 | test('detects fastify instance without mercurius', async (t) => { 222 | t.plan(1) 223 | const tempTargetPath = await tmp.file() 224 | 225 | t.teardown(async () => { 226 | await tempTargetPath.cleanup() 227 | }) 228 | 229 | await codegenMercurius(appWithoutMercurius, { 230 | targetPath: tempTargetPath.path, 231 | }).catch((err) => { 232 | t.is(err.message, 'Mercurius is not registered in Fastify Instance!') 233 | }) 234 | }) 235 | 236 | test('respects "disable" flag', async (t) => { 237 | t.plan(1) 238 | const tempTargetPath = await tmp.file() 239 | 240 | t.teardown(async () => { 241 | await tempTargetPath.cleanup() 242 | }) 243 | 244 | const { closeWatcher } = await codegenMercurius(app, { 245 | targetPath: tempTargetPath.path, 246 | disable: true, 247 | }) 248 | 249 | closeWatcher() 250 | 251 | t.is( 252 | await readFile(tempTargetPath.path, { 253 | encoding: 'utf-8', 254 | }), 255 | '', 256 | ) 257 | }) 258 | 259 | test('warns about unsupported namingConvention, respecting silent', async (t) => { 260 | t.plan(2) 261 | 262 | const namingConvention = 'pascal-case' 263 | 264 | const tempTargetPath = await tmp.file() 265 | 266 | const prevConsoleLog = console.log 267 | 268 | const mockConsoleLog = (message: string) => { 269 | t.truthy(message) 270 | } 271 | 272 | console.log = mockConsoleLog 273 | 274 | t.teardown(() => { 275 | console.log = prevConsoleLog 276 | }) 277 | 278 | const prevConsoleWarn = console.warn 279 | 280 | const mockConsoleWarn = (message: string) => { 281 | t.is( 282 | message, 283 | `namingConvention "${namingConvention}" is not supported! it has been set to "keep" automatically.`, 284 | ) 285 | } 286 | 287 | console.warn = mockConsoleWarn 288 | 289 | t.teardown(() => { 290 | console.warn = prevConsoleWarn 291 | }) 292 | 293 | t.teardown(async () => { 294 | await tempTargetPath.cleanup() 295 | }) 296 | 297 | await codegenMercurius(app, { 298 | targetPath: tempTargetPath.path, 299 | disable: false, 300 | silent: false, 301 | codegenConfig: { 302 | namingConvention, 303 | }, 304 | }) 305 | 306 | await codegenMercurius(app, { 307 | targetPath: tempTargetPath.path, 308 | disable: false, 309 | silent: true, 310 | codegenConfig: { 311 | namingConvention, 312 | }, 313 | }) 314 | }) 315 | 316 | test('supports addUnderscoreToArgsType config option', async (t) => { 317 | t.plan(1) 318 | await app.ready() 319 | generatedCode = await generateCode(app.graphql.schema, { 320 | addUnderscoreToArgsType: true, 321 | }) 322 | 323 | t.snapshot(generatedCode, 'generated code') 324 | }) 325 | 326 | test('gql helper', (t) => { 327 | t.plan(2) 328 | 329 | const a = gql` 330 | query A { 331 | hello 332 | } 333 | ` 334 | 335 | const b = gql` 336 | query B { 337 | hello 338 | } 339 | ${a} 340 | ` 341 | 342 | t.snapshot(print(parse(a))) 343 | t.snapshot(print(parse(b))) 344 | }) 345 | 346 | test('p-lazy helper', async (t) => { 347 | let resolved = false 348 | const lazyPromise = new PLazy((resolve) => { 349 | resolved = true 350 | resolve(true) 351 | }) 352 | 353 | t.is(resolved, false) 354 | 355 | const value = await lazyPromise 356 | 357 | t.is(value, true) 358 | t.is(resolved, true) 359 | 360 | let normalResolved = false 361 | const normalPromise = new Promise((resolve) => { 362 | normalResolved = true 363 | resolve(true) 364 | }) 365 | 366 | t.is(normalResolved, true) 367 | 368 | const normalValue = await normalPromise 369 | 370 | t.is(normalValue, true) 371 | t.is(normalResolved, true) 372 | }) 373 | 374 | test('LazyPromise helper', async (t) => { 375 | let resolved = false 376 | const lazyPromise = LazyPromise(() => { 377 | resolved = true 378 | return true 379 | }) 380 | 381 | t.is(resolved, false) 382 | 383 | const value = await lazyPromise 384 | 385 | t.is(value, true) 386 | t.is(resolved, true) 387 | 388 | // 389 | 390 | let resolved2 = false 391 | const lazyPromise2 = LazyPromise(async () => { 392 | resolved2 = true 393 | return true 394 | }) 395 | 396 | t.is(resolved2, false) 397 | 398 | const value2 = await lazyPromise2 399 | 400 | t.is(value2, true) 401 | t.is(resolved2, true) 402 | 403 | // 404 | 405 | let resolved3 = false 406 | const lazyPromise3 = LazyPromise(async () => { 407 | resolved3 = true 408 | throw Error('OK') 409 | }) 410 | 411 | t.is(resolved3, false) 412 | 413 | try { 414 | await lazyPromise3 415 | } catch (err) { 416 | t.is(resolved3, true) 417 | t.is(err.message, 'OK') 418 | } 419 | 420 | // 421 | 422 | let resolved4 = false 423 | const lazyPromise4 = LazyPromise(() => { 424 | resolved4 = true 425 | throw Error('OK') 426 | }) 427 | 428 | t.is(resolved4, false) 429 | 430 | try { 431 | await lazyPromise4 432 | } catch (err) { 433 | t.is(resolved4, true) 434 | t.is(err.message, 'OK') 435 | } 436 | 437 | // 438 | 439 | let resolved5 = false 440 | const lazyPromise5 = LazyPromise(() => { 441 | resolved5 = true 442 | throw { message: 'OK' } 443 | }) 444 | 445 | t.is(resolved5, false) 446 | 447 | try { 448 | await lazyPromise5 449 | } catch (err) { 450 | t.is(resolved5, true) 451 | t.is(err.message, 'OK') 452 | } 453 | 454 | // 455 | 456 | let resolved6 = false 457 | const lazyPromise6 = LazyPromise(async () => { 458 | resolved6 = true 459 | throw { message: 'OK' } 460 | }) 461 | 462 | t.is(resolved6, false) 463 | 464 | try { 465 | await lazyPromise6 466 | } catch (err) { 467 | t.is(resolved6, true) 468 | t.is(err.message, 'OK') 469 | } 470 | }) 471 | 472 | test('non existing file', async (t) => { 473 | t.plan(1) 474 | 475 | const tempTargetDir = await tmp.dir({ 476 | unsafeCleanup: true, 477 | }) 478 | 479 | t.teardown(async () => { 480 | await tempTargetDir.cleanup() 481 | }) 482 | 483 | const targetPath = path.join(tempTargetDir.path, './genCode.ts') 484 | 485 | const code = ` 486 | console.log("hello world"); 487 | ` 488 | 489 | await writeGeneratedCode({ 490 | code, 491 | targetPath, 492 | }) 493 | 494 | const writtenCode = await readFile(targetPath, { 495 | encoding: 'utf-8', 496 | }) 497 | 498 | t.is(writtenCode, code) 499 | }) 500 | 501 | test('operations with watching', async (t) => { 502 | t.plan(7) 503 | 504 | const tempTargetPath = await tmp.file() 505 | 506 | t.teardown(async () => { 507 | await tempTargetPath.cleanup() 508 | }) 509 | const { closeWatcher } = await codegenMercurius(app, { 510 | targetPath: tempTargetPath.path, 511 | operationsGlob: ['./test/operations/*.gql'], 512 | silent: true, 513 | watchOptions: { 514 | enabled: true, 515 | }, 516 | }) 517 | 518 | t.teardown(async () => { 519 | await closeWatcher() 520 | }) 521 | 522 | const generatedCode = await readFile(tempTargetPath.path, { 523 | encoding: 'utf-8', 524 | }) 525 | 526 | t.snapshot(generatedCode) 527 | 528 | t.assert(generatedCode.includes('BDocument') === false) 529 | 530 | await fs.promises.writeFile( 531 | './test/operations/temp.gql', 532 | gql` 533 | query B { 534 | hello 535 | } 536 | `, 537 | ) 538 | 539 | t.teardown(() => { 540 | fs.rmSync('./test/operations/temp.gql') 541 | }) 542 | 543 | await waitForExpect(async () => { 544 | const generatedCode = await readFile(tempTargetPath.path, { 545 | encoding: 'utf-8', 546 | }) 547 | 548 | assert(generatedCode.includes('BDocument')) 549 | }) 550 | 551 | const generatedCode2 = await readFile(tempTargetPath.path, { 552 | encoding: 'utf-8', 553 | }) 554 | 555 | t.assert(generatedCode2.includes('BDocument')) 556 | 557 | t.snapshot(generatedCode2) 558 | 559 | // Closing previous watcher 560 | const { closeWatcher: closeWatcher2 } = await codegenMercurius(app, { 561 | targetPath: tempTargetPath.path, 562 | operationsGlob: ['./test/operations/*.gql'], 563 | silent: true, 564 | watchOptions: { 565 | enabled: true, 566 | }, 567 | }) 568 | 569 | t.is(await closeWatcher(), false) 570 | t.is(await closeWatcher2(), true) 571 | 572 | t.teardown(async () => { 573 | await closeWatcher2() 574 | }) 575 | 576 | const { closeWatcher: closeWatcher3 } = await codegenMercurius(app, { 577 | targetPath: tempTargetPath.path, 578 | operationsGlob: ['./test/operations/*.gql'], 579 | silent: true, 580 | watchOptions: { 581 | enabled: true, 582 | uniqueWatch: false, 583 | }, 584 | }) 585 | t.teardown(async () => { 586 | await closeWatcher3() 587 | }) 588 | 589 | t.is(await closeWatcher3(), true) 590 | }) 591 | 592 | test.serial('load schema files with watching', async (t) => { 593 | t.plan(3) 594 | 595 | await mkdirp(path.join(process.cwd(), 'tmp', 'load-schema')) 596 | 597 | const tempTargetDir = await tmp.dir({ 598 | unsafeCleanup: true, 599 | dir: 'load-schema', 600 | tmpdir: path.join(process.cwd(), 'tmp'), 601 | }) 602 | 603 | t.teardown(async () => { 604 | await tempTargetDir.cleanup() 605 | }) 606 | 607 | await writeFile( 608 | path.join(tempTargetDir.path, 'a.gql'), 609 | gql` 610 | type Query { 611 | hello: String! 612 | } 613 | `, 614 | ) 615 | 616 | let resolveChangePromise: (value: string[]) => void 617 | 618 | const changePromise = new Promise((resolve, reject) => { 619 | const timeout = setTimeout(() => { 620 | reject(Error('Change promise timed out')) 621 | }, 20000) 622 | resolveChangePromise = (value) => { 623 | clearTimeout(timeout) 624 | resolve(value) 625 | } 626 | }) 627 | const { schema, closeWatcher, watcher } = loadSchemaFiles( 628 | path.join(tempTargetDir.path, '*.gql'), 629 | { 630 | silent: true, 631 | watchOptions: { 632 | enabled: true, 633 | onChange(schema) { 634 | resolveChangePromise(schema) 635 | }, 636 | chokidarOptions: { 637 | // usePolling: true, 638 | }, 639 | }, 640 | }, 641 | ) 642 | 643 | t.teardown(async () => { 644 | await closeWatcher() 645 | }) 646 | 647 | const { closeWatcher: closeIsolatedWatcher } = loadSchemaFiles( 648 | path.join(tempTargetDir.path, '*.gql'), 649 | { 650 | silent: false, 651 | watchOptions: { 652 | enabled: true, 653 | uniqueWatch: false, 654 | }, 655 | }, 656 | ) 657 | 658 | t.teardown(async () => { 659 | await closeIsolatedWatcher() 660 | }) 661 | 662 | const { closeWatcher: closeNoWatcher } = loadSchemaFiles( 663 | path.join(tempTargetDir.path, '*.gql'), 664 | { 665 | silent: true, 666 | watchOptions: { 667 | enabled: false, 668 | uniqueWatch: false, 669 | }, 670 | }, 671 | ) 672 | 673 | t.teardown(async () => { 674 | await closeNoWatcher() 675 | }) 676 | 677 | t.snapshot(schema.join('\n')) 678 | 679 | await watcher 680 | 681 | await writeFile( 682 | path.join(tempTargetDir.path, 'b.gql'), 683 | gql` 684 | extend type Query { 685 | hello2: String! 686 | } 687 | `, 688 | ) 689 | 690 | const schema2 = await changePromise 691 | 692 | t.snapshot(schema2) 693 | 694 | const { closeWatcher: closeWatcher2 } = loadSchemaFiles( 695 | path.join(tempTargetDir.path, '*.gql'), 696 | { 697 | silent: true, 698 | watchOptions: { 699 | enabled: true, 700 | onChange(schema) { 701 | resolveChangePromise(schema) 702 | }, 703 | chokidarOptions: { 704 | // usePolling: true, 705 | }, 706 | }, 707 | }, 708 | ) 709 | 710 | t.teardown(async () => { 711 | await closeWatcher2() 712 | }) 713 | 714 | const noWatcher = loadSchemaFiles(path.join(tempTargetDir.path, '*.gql')) 715 | 716 | t.snapshot(noWatcher.schema.join('\n')) 717 | }) 718 | 719 | test.serial('load schema watching error handling', async (t) => { 720 | t.plan(4) 721 | 722 | await mkdirp(path.join(process.cwd(), 'tmp', 'load-schema-errors')) 723 | 724 | const tempTargetDir = await tmp.dir({ 725 | unsafeCleanup: true, 726 | dir: 'load-schema-errors', 727 | tmpdir: path.join(process.cwd(), 'tmp'), 728 | }) 729 | 730 | t.teardown(async () => { 731 | await tempTargetDir.cleanup() 732 | }) 733 | 734 | await writeFile( 735 | path.join(tempTargetDir.path, 'a.gql'), 736 | gql` 737 | type Query { 738 | hello: String! 739 | } 740 | `, 741 | ) 742 | 743 | let resolveChangePromise: (value: string[]) => void 744 | 745 | const changePromise = new Promise((resolve, reject) => { 746 | const timeout = setTimeout(() => { 747 | reject(Error('Change promise timed out')) 748 | }, 2000) 749 | resolveChangePromise = (value) => { 750 | clearTimeout(timeout) 751 | resolve(value) 752 | } 753 | }) 754 | const { schema, closeWatcher, watcher } = loadSchemaFiles( 755 | path.join(tempTargetDir.path, '*.gql'), 756 | { 757 | silent: true, 758 | watchOptions: { 759 | enabled: true, 760 | onChange(schema) { 761 | resolveChangePromise(schema) 762 | 763 | throw Error('expected error') 764 | }, 765 | }, 766 | }, 767 | ) 768 | 769 | t.teardown(() => void closeWatcher()) 770 | await watcher 771 | 772 | t.snapshot(schema.join('\n')) 773 | 774 | const prevConsoleError = console.error 775 | 776 | let called = false 777 | console.error = (err: any) => { 778 | t.deepEqual(err, Error('expected error')) 779 | called = true 780 | } 781 | 782 | t.teardown(() => { 783 | console.error = prevConsoleError 784 | }) 785 | 786 | await writeFile( 787 | path.join(tempTargetDir.path, 'b.gql'), 788 | gql` 789 | extend type Query { 790 | hello2: String! 791 | } 792 | `, 793 | ) 794 | 795 | const schema2 = await changePromise 796 | 797 | console.error = prevConsoleError 798 | 799 | t.snapshot(schema2.join('\n')) 800 | 801 | t.is(called, true) 802 | }) 803 | 804 | test.serial('load schema with no files', async (t) => { 805 | t.plan(1) 806 | await mkdirp(path.join(process.cwd(), 'tmp', 'load-schema-throw')) 807 | 808 | const tempTargetDir = await tmp.dir({ 809 | unsafeCleanup: true, 810 | dir: 'load-schema-throw', 811 | tmpdir: path.join(process.cwd(), 'tmp'), 812 | }) 813 | 814 | t.teardown(async () => { 815 | await tempTargetDir.cleanup() 816 | }) 817 | 818 | t.throws( 819 | () => { 820 | loadSchemaFiles(path.join(tempTargetDir.path, '*.gql')) 821 | }, 822 | { 823 | message: 'No GraphQL Schema files found!', 824 | }, 825 | ) 826 | }) 827 | 828 | test.serial('pre-built schema', async (t) => { 829 | rimraf.sync(buildJSONPath) 830 | 831 | await fs.promises.writeFile( 832 | buildJSONPath, 833 | JSON.stringify([ 834 | await formatPrettier( 835 | gql` 836 | type Query { 837 | hello: String! 838 | } 839 | `, 840 | 'graphql', 841 | ), 842 | ]).replace(/\r\n/g, '\n'), 843 | { 844 | encoding: 'utf-8', 845 | }, 846 | ) 847 | 848 | const { loadSchemaFiles }: typeof import('../src/schema') = 849 | proxyquire.noPreserveCache()('../src/schema', {}) 850 | 851 | const { schema } = loadSchemaFiles('./test/operations/*.gql', { 852 | prebuild: { 853 | enabled: true, 854 | }, 855 | }) 856 | 857 | t.snapshot(schema) 858 | 859 | const { 860 | loadSchemaFiles: loadSchemaFilesManipulated, 861 | }: typeof import('../src/schema') = proxyquire.noPreserveCache()( 862 | '../src/schema', 863 | { 864 | [buildJSONPath]: [123], 865 | }, 866 | ) 867 | 868 | const { schema: schemaPreloadManipulated } = loadSchemaFilesManipulated( 869 | './test/operations/*.gql', 870 | { 871 | prebuild: { 872 | enabled: true, 873 | }, 874 | }, 875 | ) 876 | 877 | t.snapshot(schemaPreloadManipulated) 878 | }) 879 | 880 | test('no entities loaders give empty loaders', async (t) => { 881 | const schema = buildSchema(` 882 | type Query { 883 | hello: String! 884 | } 885 | `) 886 | 887 | const emptyLoaders = await loadersPlugin(schema, [], {}) 888 | 889 | t.snapshot(emptyLoaders.toString()) 890 | }) 891 | 892 | test.serial('writes output schema', async (t) => { 893 | t.plan(4) 894 | await app.ready() 895 | const tempTarget = await tmp.file() 896 | 897 | t.teardown(() => { 898 | return tempTarget.cleanup() 899 | }) 900 | await writeOutputSchema(app, tempTarget.path) 901 | 902 | t.snapshot( 903 | await readFile(tempTarget.path, { 904 | encoding: 'utf8', 905 | }), 906 | ) 907 | 908 | t.is(fs.existsSync('schema.gql'), false) 909 | 910 | await writeOutputSchema(app, true) 911 | 912 | t.teardown(() => { 913 | return fs.promises.unlink('schema.gql') 914 | }) 915 | 916 | t.is(fs.existsSync('schema.gql'), true) 917 | 918 | t.snapshot( 919 | await readFile('schema.gql', { 920 | encoding: 'utf8', 921 | }), 922 | ) 923 | }) 924 | 925 | codegenMercurius(app, { 926 | targetPath: './test/generated.ts', 927 | operationsGlob: ['./test/operations/*.gql'], 928 | disable: false, 929 | silent: true, 930 | codegenConfig: { 931 | scalars: { 932 | DateTime: 'Date', 933 | }, 934 | }, 935 | }).catch(console.error) 936 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/test/operations/hello.gql: -------------------------------------------------------------------------------- 1 | query A { 2 | hello 3 | } 4 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/test/snapshots/index.test.ts.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mercurius-js/mercurius-typescript/977ec0e90638ca4f0863afc6aa08dd29fced899f/packages/mercurius-codegen/test/snapshots/index.test.ts.snap -------------------------------------------------------------------------------- /packages/mercurius-codegen/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noUnusedLocals": false, 5 | "noUnusedParameters": false 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/mercurius-codegen/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | "incremental": false /* Enable incremental compilation */, 7 | "target": "es2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true /* Generates corresponding '.d.ts' file. */, 14 | // "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist" /* Redirect output structure to the directory. */, 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./tsconfig.tsbuildinfo" /* Specify file to store incremental compilation information */, 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | "noUnusedLocals": true /* Report errors on unused locals. */, 39 | "noUnusedParameters": true /* Report errors on unused parameters. */, 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true /* Skip type checking of declaration files. */, 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 68 | "useUnknownInCatchVariables": false 69 | }, 70 | "include": ["src"] 71 | } 72 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'examples/**' 3 | - 'packages/**' 4 | - '!**/test/**' 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 4 | "rangeStrategy": "bump", 5 | "ignorePaths": ["**/node_modules/**"], 6 | "automerge": true, 7 | "ignoreDeps": [] 8 | } 9 | --------------------------------------------------------------------------------