├── .changeset ├── README.md └── config.json ├── .eslintignore ├── .eslintrc.js ├── .github ├── actions │ └── setup-deps │ │ └── action.yml └── workflows │ ├── main.yml │ ├── publish.yml │ ├── publish_preview.yml │ ├── publish_snapshot.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── babel.config.json ├── examples ├── basic-graphql-yoga │ ├── index.ts │ ├── package.json │ └── tsconfig.json └── drizzle-graphql-yoga │ ├── .gitignore │ ├── drizzle.config.ts │ ├── package.json │ ├── src │ ├── db-schema.ts │ ├── db.ts │ ├── g.ts │ ├── schema.ts │ └── server.ts │ └── tsconfig.json ├── package.json ├── packages ├── extend │ ├── CHANGELOG.md │ ├── package.json │ └── src │ │ ├── extend.test.ts │ │ └── index.ts └── schema │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ ├── g-for-doc-references.ts │ ├── index.ts │ ├── output.ts │ ├── types.d.ts │ └── types.js ├── patches └── @astrojs__starlight-tailwind.patch ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── site ├── astro.config.mjs ├── ec.config.mjs ├── package.json ├── src │ ├── Hero.astro │ ├── content.config.mts │ ├── content │ │ └── docs │ │ │ ├── examples │ │ │ ├── Example.astro │ │ │ ├── graphql-yoga.mdx │ │ │ └── styles.css │ │ │ ├── extend.mdx │ │ │ ├── index.mdx │ │ │ ├── installation.mdx │ │ │ └── types │ │ │ ├── enum.mdx │ │ │ ├── input-object.mdx │ │ │ ├── interface-union.mdx │ │ │ ├── list-non-null.mdx │ │ │ ├── object.mdx │ │ │ └── scalar.mdx │ ├── index.css │ └── show-schema.astro ├── tailwind.config.mjs └── tsconfig.json ├── test-project ├── CHANGELOG.md ├── example-2.ts ├── example.ts ├── index.test-d.ts ├── package.json ├── runtime.test.ts └── simple-example.ts └── tsconfig.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.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "Thinkmill/graphql-ts" } 6 | ], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 10 | "onlyUpdatePeerDependentsWhenOutOfRange": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["plugin:@typescript-eslint/recommended"], 3 | parser: "@typescript-eslint/parser", 4 | rules: { 5 | "@typescript-eslint/explicit-function-return-type": 0, 6 | "@typescript-eslint/ban-ts-ignore": 0, 7 | "@typescript-eslint/no-explicit-any": 0, 8 | "@typescript-eslint/no-empty-function": 0, 9 | "prefer-const": 0, 10 | "@typescript-eslint/no-non-null-assertion": 0, 11 | "@typescript-eslint/no-empty-interface": 0, 12 | "@typescript-eslint/no-use-before-define": 0, 13 | "@typescript-eslint/no-var-requires": 0, 14 | "@typescript-eslint/ban-types": 0, 15 | "@typescript-eslint/explicit-module-boundary-types": 0, 16 | "@typescript-eslint/ban-ts-comment": 0, 17 | // ts will check this 18 | "@typescript-eslint/no-unused-vars": 0, 19 | }, 20 | overrides: [ 21 | { 22 | files: ["test-project/index.test-d.ts"], 23 | rules: { 24 | "@typescript-eslint/no-unused-vars": 0, 25 | }, 26 | }, 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /.github/actions/setup-deps/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup Dependencies" 2 | runs: 3 | using: "composite" 4 | steps: 5 | - run: npm i -g corepack@0.31.0 6 | shell: bash 7 | 8 | - run: corepack enable pnpm 9 | shell: bash 10 | 11 | - name: Setup Node.js LTS 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: lts/* 15 | registry-url: "https://registry.npmjs.org" 16 | cache: pnpm 17 | 18 | - name: Install Dependencies 19 | run: pnpm install 20 | shell: bash 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | typescript: 11 | name: TypeScript 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@main 15 | 16 | - uses: ./.github/actions/setup-deps 17 | 18 | - name: Check Types 19 | run: pnpm run all:types 20 | linting: 21 | name: Linting 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@main 25 | 26 | - uses: ./.github/actions/setup-deps 27 | 28 | - name: Linting 29 | run: pnpm run lint 30 | tests: 31 | name: Tests 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@main 35 | 36 | - uses: ./.github/actions/setup-deps 37 | 38 | - name: Tests 39 | run: pnpm run test 40 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | id-token: write 9 | 10 | jobs: 11 | publish: 12 | name: Publish 13 | runs-on: ubuntu-latest 14 | environment: release 15 | steps: 16 | - uses: actions/checkout@main 17 | with: 18 | fetch-depth: 0 19 | persist-credentials: true # needed for git push 20 | 21 | - uses: ./.github/actions/setup-deps 22 | 23 | - run: pnpm build 24 | 25 | - name: git config 26 | run: | 27 | git config --global user.name 'Thinkmill Release Bot' 28 | git config --global user.email 'automation+github@thinkmill.com.au' 29 | 30 | - name: npm publish, git tag 31 | run: pnpm changeset publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | NPM_CONFIG_PROVENANCE: true 35 | 36 | - run: git push origin --follow-tags 37 | -------------------------------------------------------------------------------- /.github/workflows/publish_preview.yml: -------------------------------------------------------------------------------- 1 | name: Publish (Preview) 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@main 11 | 12 | - uses: ./.github/actions/setup-deps 13 | 14 | - name: Build 15 | run: pnpm build 16 | 17 | - run: pnpm dlx pkg-pr-new publish --comment=off './packages/*' 18 | -------------------------------------------------------------------------------- /.github/workflows/publish_snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Publish (Snapshot) 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | id-token: write 9 | 10 | jobs: 11 | publish_snapshot: 12 | name: Publish (Snapshot) 13 | runs-on: ubuntu-latest 14 | environment: release 15 | steps: 16 | - uses: actions/checkout@main 17 | with: 18 | persist-credentials: true # needed for git push 19 | 20 | - uses: ./.github/actions/setup-deps 21 | 22 | - name: version packages 23 | run: pnpm changeset version --snapshot test 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: git commit 28 | run: | 29 | git config --global user.name 'Thinkmill Release Bot' 30 | git config --global user.email 'automation+github@thinkmill.com.au' 31 | git commit -a -m 'snapshot release' 32 | 33 | - run: pnpm build 34 | 35 | - name: npm publish, git tag 36 | run: pnpm changeset publish --tag test 37 | env: 38 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 39 | NPM_CONFIG_PROVENANCE: true 40 | 41 | # reset, then push the dangling commit 42 | - name: git push 43 | run: | 44 | git reset HEAD~1 --hard 45 | git push origin --tags 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Version Packages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | versioning: 11 | name: Pull Request 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@main 15 | with: 16 | fetch-depth: 0 17 | persist-credentials: false 18 | 19 | - uses: ./.github/actions/setup-deps 20 | 21 | - uses: changesets/action@v1 22 | with: 23 | version: pnpm run version-packages 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.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 | .astro 107 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | link-workspace-packages=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | // inline snapshots don't play well with the plugin for some reason 3 | process.env.NODE_ENV !== "test" 4 | ? { 5 | plugins: [require.resolve("prettier-plugin-jsdoc")], 6 | trailingComma: "es5", 7 | } 8 | : { trailingComma: "es5" }; 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Thinkmill Labs Pty Ltd 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 | packages/schema/README.md -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "node": "12" } }], 4 | "@babel/preset-typescript" 5 | ], 6 | "plugins": ["@babel/plugin-transform-runtime"] 7 | } 8 | -------------------------------------------------------------------------------- /examples/basic-graphql-yoga/index.ts: -------------------------------------------------------------------------------- 1 | import { gWithContext } from "@graphql-ts/schema"; 2 | import { GraphQLSchema } from "graphql"; 3 | import { createYoga } from "graphql-yoga"; 4 | import { createServer } from "http"; 5 | 6 | type Context = {}; 7 | 8 | const g = gWithContext(); 9 | type g = gWithContext.infer; 10 | 11 | const Query = g.object()({ 12 | name: "Query", 13 | fields: { 14 | hello: g.field({ type: g.String, resolve: () => "world" }), 15 | }, 16 | }); 17 | 18 | const schema = new GraphQLSchema({ query: Query }); 19 | 20 | const yoga = createYoga({ 21 | schema, 22 | graphiql: { defaultQuery: `query {\n hello\n}` }, 23 | }); 24 | const server = createServer(yoga); 25 | server.listen(4000, () => { 26 | console.info("Server is running on http://localhost:4000/graphql"); 27 | }); 28 | -------------------------------------------------------------------------------- /examples/basic-graphql-yoga/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphql-ts/example-basic-graphql-http", 3 | "private": true, 4 | "dependencies": { 5 | "@graphql-ts/schema": "^1.0.2", 6 | "graphql": "^16.3.0", 7 | "graphql-yoga": "^5.13.1" 8 | }, 9 | "devDependencies": { 10 | "@types/node": "^22.13.10", 11 | "tsx": "^4.19.3", 12 | "typescript": "^5.8.2" 13 | }, 14 | "scripts": { 15 | "dev": "tsx --watch index.ts", 16 | "types": "tsc" 17 | }, 18 | "repository": "https://github.com/Thinkmill/graphql-ts/tree/main/examples/basic-graphql-yoga" 19 | } 20 | -------------------------------------------------------------------------------- /examples/basic-graphql-yoga/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "NodeNext", 5 | "noEmit": true, 6 | "isolatedModules": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["**/*"], 14 | "exclude": ["**/node_modules**/"] 15 | } 16 | -------------------------------------------------------------------------------- /examples/drizzle-graphql-yoga/.gitignore: -------------------------------------------------------------------------------- 1 | dev.db -------------------------------------------------------------------------------- /examples/drizzle-graphql-yoga/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | export default defineConfig({ 3 | dialect: "sqlite", 4 | schema: "./src/db-schema.ts", 5 | dbCredentials: { 6 | url: "./dev.db", 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /examples/drizzle-graphql-yoga/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphql-ts/example-drizzle-graphql-yoga", 3 | "private": true, 4 | "type": "module", 5 | "dependencies": { 6 | "@graphql-ts/schema": "^1.0.2", 7 | "better-sqlite3": "^11.8.1", 8 | "drizzle-orm": "^0.40.0", 9 | "graphql": "^16.3.0", 10 | "graphql-yoga": "^5.13.1" 11 | }, 12 | "devDependencies": { 13 | "@types/better-sqlite3": "^7.6.12", 14 | "@types/node": "^22.13.10", 15 | "drizzle-kit": "^0.30.5", 16 | "tsx": "^4.19.3", 17 | "typescript": "^5.8.2" 18 | }, 19 | "scripts": { 20 | "dev": "drizzle-kit push && tsx src/server.ts", 21 | "types": "tsc" 22 | }, 23 | "repository": "https://github.com/Thinkmill/graphql-ts/tree/main/examples/drizzle-graphql-yoga" 24 | } 25 | -------------------------------------------------------------------------------- /examples/drizzle-graphql-yoga/src/db-schema.ts: -------------------------------------------------------------------------------- 1 | import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; 2 | 3 | export const todo = sqliteTable("todo", { 4 | id: text() 5 | .primaryKey() 6 | .$defaultFn(() => crypto.randomUUID()), 7 | title: text().notNull(), 8 | done: integer({ mode: "boolean" }).notNull().default(false), 9 | }); 10 | -------------------------------------------------------------------------------- /examples/drizzle-graphql-yoga/src/db.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/better-sqlite3"; 2 | import * as dbSchema from "./db-schema.js"; 3 | 4 | export type DB = ReturnType; 5 | 6 | export function createDb(url: string) { 7 | return drizzle(url, { schema: dbSchema }); 8 | } 9 | -------------------------------------------------------------------------------- /examples/drizzle-graphql-yoga/src/g.ts: -------------------------------------------------------------------------------- 1 | import { gWithContext } from "@graphql-ts/schema"; 2 | import type { DB } from "./db.js"; 3 | 4 | export type Context = { 5 | db: DB; 6 | }; 7 | 8 | export const g = gWithContext(); 9 | export type g = gWithContext.infer; 10 | -------------------------------------------------------------------------------- /examples/drizzle-graphql-yoga/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from "graphql"; 2 | import { g } from "./g.js"; 3 | import * as dbSchema from "./db-schema.js"; 4 | import { eq } from "drizzle-orm"; 5 | 6 | const Todo = g.object()({ 7 | name: "Todo", 8 | fields: { 9 | id: g.field({ type: g.nonNull(g.ID) }), 10 | title: g.field({ type: g.nonNull(g.String) }), 11 | done: g.field({ type: g.nonNull(g.Boolean) }), 12 | }, 13 | }); 14 | 15 | const Query = g.object()({ 16 | name: "Query", 17 | fields: { 18 | todos: g.field({ 19 | type: g.list(g.nonNull(Todo)), 20 | resolve(_root, _args, ctx) { 21 | return ctx.db.query.todo.findMany(); 22 | }, 23 | }), 24 | todo: g.field({ 25 | type: Todo, 26 | args: { 27 | id: g.arg({ type: g.nonNull(g.ID) }), 28 | }, 29 | resolve(_root, args, ctx) { 30 | return ctx.db.query.todo.findFirst({ 31 | where: eq(dbSchema.todo.id, args.id), 32 | }); 33 | }, 34 | }), 35 | }, 36 | }); 37 | 38 | const Mutation = g.object()({ 39 | name: "Mutation", 40 | fields: { 41 | createTodo: g.field({ 42 | type: Todo, 43 | args: { 44 | title: g.arg({ type: g.nonNull(g.String) }), 45 | }, 46 | async resolve(_root, args, ctx) { 47 | const inserted = await ctx.db 48 | .insert(dbSchema.todo) 49 | .values({ title: args.title }) 50 | .returning(); 51 | return inserted[0]; 52 | }, 53 | }), 54 | updateTodo: g.field({ 55 | type: Todo, 56 | args: { 57 | id: g.arg({ type: g.nonNull(g.ID) }), 58 | title: g.arg({ type: g.String }), 59 | done: g.arg({ type: g.Boolean }), 60 | }, 61 | async resolve(_root, args, ctx) { 62 | const updated = await ctx.db 63 | .update(dbSchema.todo) 64 | .set({ 65 | title: args.title ?? undefined, 66 | done: args.done ?? undefined, 67 | }) 68 | .where(eq(dbSchema.todo.id, args.id)) 69 | .returning(); 70 | return updated[0]; 71 | }, 72 | }), 73 | deleteTodo: g.field({ 74 | type: Todo, 75 | args: { 76 | id: g.arg({ type: g.nonNull(g.ID) }), 77 | }, 78 | async resolve(_root, args, ctx) { 79 | const deleted = await ctx.db 80 | .delete(dbSchema.todo) 81 | .where(eq(dbSchema.todo.id, args.id)) 82 | .returning(); 83 | return deleted[0]; 84 | }, 85 | }), 86 | }, 87 | }); 88 | 89 | export const schema = new GraphQLSchema({ query: Query, mutation: Mutation }); 90 | -------------------------------------------------------------------------------- /examples/drizzle-graphql-yoga/src/server.ts: -------------------------------------------------------------------------------- 1 | import { createYoga } from "graphql-yoga"; 2 | import { createServer } from "http"; 3 | import { schema } from "./schema.js"; 4 | import type { Context } from "./g.js"; 5 | import { createDb } from "./db.js"; 6 | 7 | const yoga = createYoga({ 8 | schema, 9 | context: (): Context => ({ db: createDb("./dev.db") }), 10 | }); 11 | 12 | const server = createServer(yoga); 13 | server.listen(4000, () => { 14 | console.info("Server is running on http://localhost:4000/graphql"); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/drizzle-graphql-yoga/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "NodeNext", 5 | "noEmit": true, 6 | "isolatedModules": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["**/*"], 14 | "exclude": ["**/node_modules**/"] 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphql-ts/repo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "repository": "https://github.com/Thinkmill/graphql-ts", 6 | "license": "MIT", 7 | "packageManager": "pnpm@9.15.4", 8 | "devDependencies": { 9 | "@babel/core": "^7.14.3", 10 | "@babel/plugin-transform-runtime": "7.14.5", 11 | "@babel/preset-env": "7.14.7", 12 | "@babel/preset-typescript": "^7.13.0", 13 | "@changesets/changelog-github": "^0.4.0", 14 | "@changesets/cli": "^2.16.0", 15 | "@manypkg/cli": "^0.23.0", 16 | "@preconstruct/cli": "^2.8.10", 17 | "@types/jest": "26.0.24", 18 | "@typescript-eslint/eslint-plugin": "^4.26.1", 19 | "@typescript-eslint/parser": "^4.26.1", 20 | "babel-jest": "27.0.6", 21 | "eslint": "^7.28.0", 22 | "jest": "27.0.6", 23 | "prettier": "3.5.2", 24 | "prettier-plugin-jsdoc": "1.3.2", 25 | "ts-5-7": "npm:typescript@5.7.3", 26 | "typescript": "^5.8.2" 27 | }, 28 | "scripts": { 29 | "postinstall": "preconstruct dev && manypkg check", 30 | "version-packages": "changeset version && pnpm i --frozen-lockfile=false", 31 | "test": "jest", 32 | "types": "tsc", 33 | "all:types": "pnpm run /^types.*/ && pnpm -r run types", 34 | "lint": "eslint . && prettier --check .", 35 | "site": "cd site && pnpm dev", 36 | "build": "preconstruct build", 37 | "types:5-7": "./node_modules/ts-5-7/bin/tsc" 38 | }, 39 | "preconstruct": { 40 | "packages": [ 41 | "packages/*" 42 | ], 43 | "exports": true, 44 | "___experimentalFlags_WILL_CHANGE_IN_PATCH": { 45 | "importsConditions": true 46 | } 47 | }, 48 | "manypkg": { 49 | "defaultBranch": "main" 50 | }, 51 | "pnpm": { 52 | "patchedDependencies": { 53 | "@astrojs/starlight-tailwind": "patches/@astrojs__starlight-tailwind.patch" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/extend/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @graphql-ts/extend 2 | 3 | ## 2.0.0 4 | 5 | ### Major Changes 6 | 7 | - [#31](https://github.com/Thinkmill/graphql-ts/pull/31) [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085) Thanks [@emmatown](https://github.com/emmatown)! - `graphql@15` is no longer supported. `graphql@16.0.0` or newer is now required. 8 | 9 | - [#31](https://github.com/Thinkmill/graphql-ts/pull/31) [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085) Thanks [@emmatown](https://github.com/emmatown)! - The `wrap` export has been removed. Since the types in `@graphql-ts/schema@1.0.0` are compatible with the GraphQL.js types directly, these functions are no longer needed. 10 | 11 | - [#51](https://github.com/Thinkmill/graphql-ts/pull/51) [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81) Thanks [@emmatown](https://github.com/emmatown)! - The `Key` type parameter on `Field` has been replaced with a new type parameter (`SourceAtKey`) and represents essentially `Source[Key]` instead of `Key`. For example, a field like this can be written: 12 | 13 | ```ts 14 | const field = g.field({ 15 | type: g.String, 16 | }); 17 | ``` 18 | 19 | and `field` will be usable like this: 20 | 21 | ```ts 22 | const Something = g.object<{ 23 | name: string 24 | }>({ 25 | name: "Something" 26 | fields: { 27 | name: field, 28 | // @ts-expect-error 29 | other: field 30 | }, 31 | }); 32 | ``` 33 | 34 | The field is usable at `name` since the source type has an object with a `name` property that's a `string` but using it at `other` will result in a type error since the source type doesn't have a `other` property. 35 | 36 | Previously, using `g.field` outside a `g.object`/`g.fields` call would require specifying a resolver and fields written within `g.fields` would be bound to be used at a specific key rather than the new behaviour of any key with the right type. 37 | 38 | This also reduces the need for `g.fields`. For example, the example given in the previous JSDoc for `g.fields`: 39 | 40 | ```ts 41 | const nodeFields = g.fields<{ id: string }>()({ 42 | id: g.field({ type: g.ID }), 43 | }); 44 | const Node = g.interface<{ id: string }>()({ 45 | name: "Node", 46 | fields: nodeFields, 47 | }); 48 | const Person = g.object<{ 49 | __typename: "Person"; 50 | id: string; 51 | name: string; 52 | }>()({ 53 | name: "Person", 54 | interfaces: [Node], 55 | fields: { 56 | ...nodeFields, 57 | name: g.field({ type: g.String }), 58 | }, 59 | }); 60 | ``` 61 | 62 | Now the `g.fields` call is unnecessary and writing `nodeFields` will no longer error at the `g.field` call and will instead work as expected. 63 | 64 | ```ts 65 | const nodeFields = { 66 | id: g.field({ type: g.ID }), 67 | }; 68 | ``` 69 | 70 | There is still some use to `g.fields` for when you want to define a number of shared fields with resolvers and specify the source type just once in the `g.fields` call rathe than in every resolver. 71 | 72 | This change is unlikely to break existing code except where you explicitly use the `Field` type or explicitly pass type parameters to `g.field` (the latter of which you likely shouldn't do) but since it changes the meaning of a type parameter of `Field`, it's regarded as a breaking change. 73 | 74 | ### Patch Changes 75 | 76 | - Updated dependencies [[`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81), [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81), [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81), [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81), [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085), [`d7151bd2a6333327ac1a57e0c924bd4bfdbdf01f`](https://github.com/Thinkmill/graphql-ts/commit/d7151bd2a6333327ac1a57e0c924bd4bfdbdf01f), [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085), [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085), [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085), [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81)]: 77 | - @graphql-ts/schema@1.0.0 78 | 79 | ## 1.0.3 80 | 81 | ### Patch Changes 82 | 83 | - [#36](https://github.com/Thinkmill/graphql-ts/pull/36) [`692b08c5f7fdfb8e6aead74be2ea0841ba74dbad`](https://github.com/Thinkmill/graphql-ts/commit/692b08c5f7fdfb8e6aead74be2ea0841ba74dbad) Thanks [@emmatown](https://github.com/emmatown)! - Remove `@babel/runtime` dependency 84 | 85 | ## 1.0.2 86 | 87 | ### Patch Changes 88 | 89 | - [#24](https://github.com/Thinkmill/graphql-ts/pull/24) [`2dc7f48a07f4e235261891dd0ff3bc4ca2ec9858`](https://github.com/Thinkmill/graphql-ts/commit/2dc7f48a07f4e235261891dd0ff3bc4ca2ec9858) Thanks [@emmatown](https://github.com/emmatown)! - Add support for `isOneOf` in input object types 90 | 91 | - [`65f914c83a6438e8b326047c0dc3d83d47ba110c`](https://github.com/Thinkmill/graphql-ts/commit/65f914c83a6438e8b326047c0dc3d83d47ba110c) Thanks [@emmatown](https://github.com/emmatown)! - Simplify the build files in `dist` 92 | 93 | ## 1.0.1 94 | 95 | ### Patch Changes 96 | 97 | - [#19](https://github.com/Thinkmill/graphql-ts/pull/19) [`d79115e1007e6e47b425293122818e7d40edf8b5`](https://github.com/Thinkmill/graphql-ts/commit/d79115e1007e6e47b425293122818e7d40edf8b5) Thanks [@emmatown](https://github.com/emmatown)! - Documentation updates to align with the renaming of the `graphql` export to `g` in `@graphql-ts/schema` 98 | 99 | ## 1.0.0 100 | 101 | ### Minor Changes 102 | 103 | - [`012d84e`](https://github.com/Thinkmill/graphql-ts/commit/012d84e04bfe37c18aa0afdc541843586cf768bf) Thanks [@emmatown](https://github.com/emmatown)! - Added `exports` field 104 | 105 | ### Patch Changes 106 | 107 | - Updated dependencies [[`012d84e`](https://github.com/Thinkmill/graphql-ts/commit/012d84e04bfe37c18aa0afdc541843586cf768bf)]: 108 | - @graphql-ts/schema@0.6.0 109 | 110 | ## 0.4.1 111 | 112 | ### Patch Changes 113 | 114 | - [`ef18bba`](https://github.com/Thinkmill/graphql-ts/commit/ef18bba55773e38309f538b987099650ad66533d) Thanks [@emmatown](https://github.com/emmatown)! - Added declaration maps 115 | 116 | * [`65391d3`](https://github.com/Thinkmill/graphql-ts/commit/65391d30c7a56313325acb647110e8536008d82b) Thanks [@emmatown](https://github.com/emmatown)! - `graphql@16` is now allowed in `peerDependencies` 117 | 118 | ## 0.4.0 119 | 120 | ### Minor Changes 121 | 122 | - [`5d1c299`](https://github.com/Thinkmill/graphql-ts/commit/5d1c299ae50a8bafea8e409f9c2c1e5abedaa29a) Thanks [@emmatown](https://github.com/emmatown)! - Type parameters named `RootVal` have been renamed to `Source` and properties named `__rootVal` have been renamed to `__source`. This won't require code changes unless you've relied on the `__rootVal` properties(which you shouldn't). 123 | 124 | * [`232cec8`](https://github.com/Thinkmill/graphql-ts/commit/232cec81c04c3489c053e24cfe37ab7f3d8a4265) Thanks [@emmatown](https://github.com/emmatown)! - `BaseSchemaInfo` has been renamed to `BaseSchemaMeta` 125 | 126 | ## 0.3.0 127 | 128 | ### Minor Changes 129 | 130 | - [`c92bf61`](https://github.com/Thinkmill/graphql-ts/commit/c92bf61044af69d72003a076b2a191ff685633fb) Thanks [@emmatown](https://github.com/emmatown)! - An array of extensions can now be passed to `extend` 131 | 132 | ## 0.2.0 133 | 134 | ### Minor Changes 135 | 136 | - [`42f4abe`](https://github.com/Thinkmill/graphql-ts/commit/42f4abe6ad5e6b1bfec3eb7acfad0e54721c63cb) Thanks [@emmatown](https://github.com/emmatown)! - Added `wrap` export to wrap graphql-js types into @graphql-ts/schema types and expand the functions on `BaseSchemaInfo` to all GraphQL types 137 | 138 | ## 0.1.0 139 | 140 | ### Minor Changes 141 | 142 | - [`3b6d533`](https://github.com/Thinkmill/graphql-ts/commit/3b6d533f9e76c54341610346e1e7bcab29f6826b) Thanks [@emmatown](https://github.com/emmatown)! - Initial release. This package is very early and will have many breaking changes. 143 | -------------------------------------------------------------------------------- /packages/extend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphql-ts/extend", 3 | "version": "2.0.0", 4 | "description": "Utilities to extend existing GraphQL schemas with @graphql-ts/schema", 5 | "main": "dist/graphql-ts-extend.cjs.js", 6 | "module": "dist/graphql-ts-extend.esm.js", 7 | "exports": { 8 | ".": { 9 | "types": "./dist/graphql-ts-extend.cjs.js", 10 | "module": "./dist/graphql-ts-extend.esm.js", 11 | "default": "./dist/graphql-ts-extend.cjs.js" 12 | }, 13 | "./package.json": "./package.json" 14 | }, 15 | "files": [ 16 | "dist", 17 | "src" 18 | ], 19 | "license": "MIT", 20 | "peerDependencies": { 21 | "@graphql-ts/schema": "^1.0.0", 22 | "graphql": "16" 23 | }, 24 | "devDependencies": { 25 | "@graphql-ts/schema": "workspace:^", 26 | "graphql": "^16.3.0" 27 | }, 28 | "repository": "https://github.com/Thinkmill/graphql-ts/tree/main/packages/extend" 29 | } 30 | -------------------------------------------------------------------------------- /packages/extend/src/extend.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLScalarType, 3 | GraphQLSchema, 4 | graphqlSync, 5 | printSchema, 6 | } from "graphql"; 7 | import { gWithContext } from "@graphql-ts/schema"; 8 | import { extend } from "."; 9 | 10 | const g = gWithContext(); 11 | type g = gWithContext.infer; 12 | 13 | const getGql = 14 | (schema: GraphQLSchema) => 15 | ([source]: TemplateStringsArray) => 16 | graphqlSync({ schema, source }); 17 | 18 | expect.addSnapshotSerializer({ 19 | test(arg) { 20 | return arg instanceof GraphQLSchema; 21 | }, 22 | serialize(val) { 23 | return printSchema(val as GraphQLSchema).trim(); 24 | }, 25 | }); 26 | 27 | const onlyQuery = new GraphQLSchema({ 28 | query: g.object()({ 29 | name: "Query", 30 | fields: { 31 | thing: g.field({ 32 | type: g.String, 33 | resolve() { 34 | return "Thing"; 35 | }, 36 | }), 37 | }, 38 | }), 39 | }); 40 | 41 | test("basic query", () => { 42 | const extended = extend({ 43 | query: { 44 | hello: g.field({ 45 | type: g.String, 46 | resolve() { 47 | return "Hello!"; 48 | }, 49 | }), 50 | }, 51 | })(onlyQuery); 52 | expect(extended).toMatchInlineSnapshot(` 53 | type Query { 54 | thing: String 55 | hello: String 56 | } 57 | `); 58 | const gql = getGql(extended); 59 | expect(gql` 60 | query { 61 | hello 62 | thing 63 | } 64 | `).toMatchInlineSnapshot(` 65 | Object { 66 | "data": Object { 67 | "hello": "Hello!", 68 | "thing": "Thing", 69 | }, 70 | } 71 | `); 72 | }); 73 | 74 | test("basic mutation with no existing mutations", () => { 75 | const extended = extend({ 76 | mutation: { 77 | something: g.field({ 78 | type: g.String, 79 | resolve() { 80 | return ""; 81 | }, 82 | }), 83 | }, 84 | })(onlyQuery); 85 | expect(extended).toMatchInlineSnapshot(` 86 | type Mutation { 87 | something: String 88 | } 89 | 90 | type Query { 91 | thing: String 92 | } 93 | `); 94 | const gql = getGql(extended); 95 | expect(gql` 96 | mutation { 97 | something 98 | } 99 | `).toMatchInlineSnapshot(` 100 | Object { 101 | "data": Object { 102 | "something": "", 103 | }, 104 | } 105 | `); 106 | }); 107 | 108 | test("basic mutation with existing mutations", () => { 109 | const queryAndMutation = new GraphQLSchema({ 110 | query: g.object()({ 111 | name: "Query", 112 | fields: { 113 | thing: g.field({ 114 | type: g.String, 115 | resolve() { 116 | return "Thing"; 117 | }, 118 | }), 119 | }, 120 | }), 121 | mutation: g.object()({ 122 | name: "Mutation", 123 | fields: { 124 | thing: g.field({ 125 | type: g.String, 126 | resolve() { 127 | return "Thing"; 128 | }, 129 | }), 130 | }, 131 | }), 132 | }); 133 | const extended = extend({ 134 | mutation: { 135 | something: g.field({ 136 | type: g.String, 137 | resolve() { 138 | return ""; 139 | }, 140 | }), 141 | }, 142 | })(queryAndMutation); 143 | expect(extended).toMatchInlineSnapshot(` 144 | type Query { 145 | thing: String 146 | } 147 | 148 | type Mutation { 149 | thing: String 150 | something: String 151 | } 152 | `); 153 | const gql = getGql(extended); 154 | expect(gql` 155 | mutation { 156 | something 157 | } 158 | `).toMatchInlineSnapshot(` 159 | Object { 160 | "data": Object { 161 | "something": "", 162 | }, 163 | } 164 | `); 165 | }); 166 | 167 | test("errors when query type is used elsewhere in schema", () => { 168 | const Query: g> = g.object<{}>()({ 169 | name: "Query", 170 | fields: () => ({ 171 | thing: g.field({ 172 | type: g.String, 173 | resolve() { 174 | return "Thing"; 175 | }, 176 | }), 177 | other: g.field({ 178 | type: Query, 179 | resolve() { 180 | return {}; 181 | }, 182 | }), 183 | }), 184 | }); 185 | const initial = new GraphQLSchema({ 186 | query: Query, 187 | }); 188 | expect(() => { 189 | extend({ 190 | mutation: { 191 | something: g.field({ 192 | type: g.String, 193 | resolve() { 194 | return ""; 195 | }, 196 | }), 197 | }, 198 | })(initial); 199 | }).toThrowErrorMatchingInlineSnapshot(` 200 | "@graphql-ts/extend doesn't yet support using the query and mutation types in other types but 201 | - \\"Query\\" is used at \\"Query.other\\"" 202 | `); 203 | }); 204 | 205 | test("errors when query and mutation type is used elsewhere in schema", () => { 206 | const Query: g> = g.object<{}>()({ 207 | name: "Query", 208 | fields: () => ({ 209 | thing: g.field({ 210 | type: g.String, 211 | resolve() { 212 | return "Thing"; 213 | }, 214 | }), 215 | }), 216 | }); 217 | const Mutation: g> = g.object<{}>()({ 218 | name: "Mutation", 219 | fields: () => ({ 220 | thing: g.field({ 221 | type: g.String, 222 | resolve() { 223 | return "Thing"; 224 | }, 225 | }), 226 | other: g.field({ 227 | type: Mutation, 228 | resolve() { 229 | return {}; 230 | }, 231 | }), 232 | }), 233 | }); 234 | const initial = new GraphQLSchema({ 235 | query: Query, 236 | mutation: Mutation, 237 | }); 238 | expect(() => { 239 | extend({ 240 | mutation: { 241 | something: g.field({ 242 | type: g.String, 243 | resolve() { 244 | return ""; 245 | }, 246 | }), 247 | }, 248 | })(initial); 249 | }).toThrowErrorMatchingInlineSnapshot(` 250 | "@graphql-ts/extend doesn't yet support using the query and mutation types in other types but 251 | - \\"Mutation\\" is used at \\"Mutation.other\\"" 252 | `); 253 | }); 254 | 255 | test("errors when query and mutation type is used elsewhere in schema", () => { 256 | const Query: g> = g.object<{}>()({ 257 | name: "Query", 258 | fields: () => ({ 259 | thing: g.field({ 260 | type: g.String, 261 | resolve() { 262 | return "Thing"; 263 | }, 264 | }), 265 | other: g.field({ 266 | type: Query, 267 | resolve() { 268 | return {}; 269 | }, 270 | }), 271 | otherBlah: g.field({ 272 | type: Query, 273 | resolve() { 274 | return {}; 275 | }, 276 | }), 277 | }), 278 | }); 279 | const Mutation: g> = g.object<{}>()({ 280 | name: "Mutation", 281 | fields: () => ({ 282 | thing: g.field({ 283 | type: g.String, 284 | resolve() { 285 | return "Thing"; 286 | }, 287 | }), 288 | other: g.field({ 289 | type: Mutation, 290 | resolve() { 291 | return {}; 292 | }, 293 | }), 294 | }), 295 | }); 296 | const initial = new GraphQLSchema({ 297 | query: Query, 298 | mutation: Mutation, 299 | }); 300 | expect(() => { 301 | extend({ 302 | mutation: { 303 | something: g.field({ 304 | type: g.String, 305 | resolve() { 306 | return ""; 307 | }, 308 | }), 309 | }, 310 | })(initial); 311 | }).toThrowErrorMatchingInlineSnapshot(` 312 | "@graphql-ts/extend doesn't yet support using the query and mutation types in other types but 313 | - \\"Query\\" is used at \\"Query.other\\", \\"Query.otherBlah\\" 314 | - \\"Mutation\\" is used at \\"Mutation.other\\"" 315 | `); 316 | }); 317 | 318 | test("basic query with args", () => { 319 | const extended = extend({ 320 | query: { 321 | hello: g.field({ 322 | type: g.String, 323 | args: { 324 | thing: g.arg({ type: g.String }), 325 | }, 326 | resolve(_, { thing }) { 327 | return thing; 328 | }, 329 | }), 330 | }, 331 | })(onlyQuery); 332 | expect(extended).toMatchInlineSnapshot(` 333 | type Query { 334 | thing: String 335 | hello(thing: String): String 336 | } 337 | `); 338 | const gql = getGql(extended); 339 | expect(gql` 340 | query { 341 | hello(thing: "something") 342 | thing 343 | } 344 | `).toMatchInlineSnapshot(` 345 | Object { 346 | "data": Object { 347 | "hello": "something", 348 | "thing": "Thing", 349 | }, 350 | } 351 | `); 352 | }); 353 | 354 | test("using an existing object type", () => { 355 | const initial = new GraphQLSchema({ 356 | query: g.object()({ 357 | name: "Query", 358 | fields: { 359 | something: g.field({ 360 | type: g.object()({ 361 | name: "Something", 362 | fields: { 363 | something: g.field({ 364 | type: g.String, 365 | resolve() { 366 | return "Something"; 367 | }, 368 | }), 369 | }, 370 | }), 371 | resolve() { 372 | return {}; 373 | }, 374 | }), 375 | }, 376 | }), 377 | }); 378 | const extended = extend((base) => ({ 379 | query: { 380 | hello: g.field({ 381 | type: base.object("Something"), 382 | resolve() { 383 | return {}; 384 | }, 385 | }), 386 | }, 387 | }))(initial); 388 | expect(extended).toMatchInlineSnapshot(` 389 | type Query { 390 | something: Something 391 | hello: Something 392 | } 393 | 394 | type Something { 395 | something: String 396 | } 397 | `); 398 | const gql = getGql(extended); 399 | expect(gql` 400 | query { 401 | hello { 402 | something 403 | } 404 | something { 405 | something 406 | } 407 | } 408 | `).toMatchInlineSnapshot(` 409 | Object { 410 | "data": Object { 411 | "hello": Object { 412 | "something": "Something", 413 | }, 414 | "something": Object { 415 | "something": "Something", 416 | }, 417 | }, 418 | } 419 | `); 420 | }); 421 | 422 | test("errors when no type ", () => { 423 | expect(() => { 424 | extend((base) => ({ 425 | query: { 426 | hello: g.field({ 427 | type: base.object("Something"), 428 | resolve() { 429 | return {}; 430 | }, 431 | }), 432 | }, 433 | }))(onlyQuery); 434 | }).toThrowErrorMatchingInlineSnapshot( 435 | `"No type named \\"Something\\" exists in the schema that is being extended"` 436 | ); 437 | }); 438 | 439 | test("errors when the type isn't an object type", () => { 440 | const initial = new GraphQLSchema({ 441 | query: g.object()({ 442 | name: "Query", 443 | fields: { 444 | something: g.field({ 445 | type: g.String, 446 | args: { 447 | a: g.arg({ 448 | type: g.inputObject({ 449 | name: "Something", 450 | fields: { 451 | something: g.arg({ type: g.String }), 452 | }, 453 | }), 454 | }), 455 | }, 456 | resolve() { 457 | return ""; 458 | }, 459 | }), 460 | }, 461 | }), 462 | }); 463 | expect(() => { 464 | extend((base) => ({ 465 | query: { 466 | hello: g.field({ 467 | type: base.object("Something"), 468 | resolve() { 469 | return {}; 470 | }, 471 | }), 472 | }, 473 | }))(initial); 474 | }).toThrowErrorMatchingInlineSnapshot( 475 | `"There is a type named \\"Something\\" in the schema being extended but it is not an object type"` 476 | ); 477 | }); 478 | 479 | test(".scalar throws for built-in scalars", () => { 480 | const initial = new GraphQLSchema({ 481 | query: g.object()({ 482 | name: "Query", 483 | fields: { 484 | something: g.field({ 485 | type: g.String, 486 | resolve() { 487 | return ""; 488 | }, 489 | }), 490 | }, 491 | }), 492 | }); 493 | expect(() => { 494 | extend((base) => ({ 495 | query: { 496 | hello: g.field({ 497 | type: base.scalar("String"), 498 | resolve() { 499 | return {}; 500 | }, 501 | }), 502 | }, 503 | }))(initial); 504 | }).toThrowErrorMatchingInlineSnapshot( 505 | `"The names of built-in scalars cannot be passed to BaseSchemaInfo.scalar but String was passed"` 506 | ); 507 | }); 508 | 509 | test(".scalar works for custom scalars", () => { 510 | const Something = g.scalar( 511 | new GraphQLScalarType({ 512 | name: "Something", 513 | }) 514 | ); 515 | const initial = new GraphQLSchema({ 516 | query: g.object()({ 517 | name: "Query", 518 | fields: { 519 | something: g.field({ 520 | type: Something, 521 | resolve() { 522 | return ""; 523 | }, 524 | }), 525 | }, 526 | }), 527 | }); 528 | const extended = extend((base) => ({ 529 | query: { 530 | hello: g.field({ 531 | type: base.scalar("Something"), 532 | resolve() { 533 | return ""; 534 | }, 535 | }), 536 | }, 537 | }))(initial); 538 | expect(extended).toMatchInlineSnapshot(` 539 | type Query { 540 | something: Something 541 | hello: Something 542 | } 543 | 544 | scalar Something 545 | `); 546 | }); 547 | 548 | test("a good error when there is already a field with the same name in the original type", () => { 549 | const initial = new GraphQLSchema({ 550 | query: g.object()({ 551 | name: "Query", 552 | fields: { 553 | something: g.field({ 554 | type: g.String, 555 | resolve() { 556 | return ""; 557 | }, 558 | }), 559 | }, 560 | }), 561 | }); 562 | expect(() => { 563 | extend({ 564 | query: { 565 | something: g.field({ 566 | type: g.Int, 567 | resolve() { 568 | return 1; 569 | }, 570 | }), 571 | }, 572 | })(initial); 573 | }).toThrowErrorMatchingInlineSnapshot(` 574 | "The schema extension defines a field \\"something\\" on the \\"Query\\" type but that type already defines a field with that name. 575 | The original field: 576 | something: String 577 | The field added by the extension: 578 | something: Int" 579 | `); 580 | }); 581 | 582 | test("a good error when multiple extensions add a field with the same name", () => { 583 | const initial = new GraphQLSchema({ 584 | query: g.object()({ 585 | name: "Query", 586 | fields: { 587 | something: g.field({ 588 | type: g.String, 589 | resolve() { 590 | return ""; 591 | }, 592 | }), 593 | }, 594 | }), 595 | }); 596 | expect(() => { 597 | extend([ 598 | { 599 | query: { 600 | another: g.field({ type: g.Int, resolve: () => 1 }), 601 | }, 602 | }, 603 | { 604 | query: { 605 | another: g.field({ 606 | type: g.Boolean, 607 | resolve: () => true, 608 | }), 609 | }, 610 | }, 611 | ])(initial); 612 | }).toThrowErrorMatchingInlineSnapshot(` 613 | "More than one extension defines a field named \\"another\\" on the query type. 614 | The first field: 615 | another: Boolean 616 | The second field: 617 | another: Int" 618 | `); 619 | }); 620 | 621 | test("multiple extensions work", () => { 622 | const initial = new GraphQLSchema({ 623 | query: g.object()({ 624 | name: "Query", 625 | fields: { 626 | something: g.field({ 627 | type: g.String, 628 | resolve() { 629 | return ""; 630 | }, 631 | }), 632 | }, 633 | }), 634 | }); 635 | const extended = extend([ 636 | { 637 | query: { 638 | another: g.field({ type: g.Int, resolve: () => 1 }), 639 | }, 640 | }, 641 | { 642 | query: { 643 | alwaysTrue: g.field({ 644 | type: g.Boolean, 645 | resolve: () => true, 646 | }), 647 | }, 648 | }, 649 | ])(initial); 650 | 651 | expect(extended).toMatchInlineSnapshot(` 652 | type Query { 653 | something: String 654 | another: Int 655 | alwaysTrue: Boolean 656 | } 657 | `); 658 | }); 659 | -------------------------------------------------------------------------------- /packages/extend/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An API to extend an arbitrary {@link GraphQLSchema} with `@graphql-ts/schema`. 3 | * Note if you're building a schema entirely with `@graphql-ts/schema`, you 4 | * shouldn't use this package. This is useful when you have a 5 | * {@link GraphQLSchema} from somewhere else and you want to some fields to 6 | * various places in it. 7 | * 8 | * See {@link extend} for more details. 9 | * 10 | * @module 11 | */ 12 | 13 | import { 14 | GraphQLSchema, 15 | isInterfaceType, 16 | isObjectType, 17 | isUnionType, 18 | getNamedType, 19 | GraphQLObjectType, 20 | GraphQLFieldConfigMap, 21 | isInputObjectType, 22 | isEnumType, 23 | isScalarType, 24 | specifiedScalarTypes, 25 | printType, 26 | parse, 27 | ObjectTypeDefinitionNode, 28 | print, 29 | } from "graphql"; 30 | 31 | import { 32 | GField, 33 | GObjectType, 34 | GArg, 35 | GEnumType, 36 | GUnionType, 37 | GInterfaceType, 38 | GScalarType, 39 | GOutputType, 40 | GInputObjectType, 41 | } from "@graphql-ts/schema"; 42 | 43 | const builtinScalars = new Set(specifiedScalarTypes.map((x) => x.name)); 44 | 45 | /** 46 | * `extend` allows you to extend a {@link GraphQLSchema} with 47 | * `@graphql-ts/schema`. 48 | * 49 | * ```ts 50 | * const originalSchema = new GraphQLSchema({ ...etc }); 51 | * 52 | * const extendedSchema = extend({ 53 | * query: { 54 | * hello: g.field({ 55 | * type: g.String, 56 | * resolve() { 57 | * return "Hello!"; 58 | * }, 59 | * }), 60 | * }, 61 | * })(originalSchema); 62 | * ``` 63 | * 64 | * To use existing types from the schema you're extending, you can provide a 65 | * function and use the {@link BaseSchemaMeta} passed into the function to use 66 | * existing types in the schema. 67 | * 68 | * ```ts 69 | * const originalSchema = new GraphQLSchema({ ...etc }); 70 | * 71 | * const extendedSchema = extend((base) => ({ 72 | * query: { 73 | * something: g.field({ 74 | * type: base.object("Something"), 75 | * resolve() { 76 | * return { something: true }; 77 | * }, 78 | * }), 79 | * }, 80 | * }))(originalSchema); 81 | * ``` 82 | * 83 | * See {@link BaseSchemaMeta} for how to get other types from the schema 84 | * 85 | * `extend` will currently throw an error if the query or mutation types are 86 | * used in other types like this. This will be allowed in a future version. 87 | * 88 | * ```graphql 89 | * type Query { 90 | * thing: Query 91 | * } 92 | * ``` 93 | */ 94 | export function extend( 95 | extension: 96 | | Extension 97 | | readonly Extension[] 98 | | ((base: BaseSchemaMeta) => Extension | readonly Extension[]) 99 | ): (schema: GraphQLSchema) => GraphQLSchema { 100 | return (schema) => { 101 | const getType = (name: string) => { 102 | const graphQLType = schema.getType(name); 103 | if (graphQLType == null) { 104 | throw new Error( 105 | `No type named ${JSON.stringify( 106 | name 107 | )} exists in the schema that is being extended` 108 | ); 109 | } 110 | return graphQLType; 111 | }; 112 | const resolvedExtension = flattenExtensions( 113 | typeof extension === "function" 114 | ? extension({ 115 | schema, 116 | object(name) { 117 | const graphQLType = getType(name); 118 | if (!isObjectType(graphQLType)) { 119 | throw new Error( 120 | `There is a type named ${JSON.stringify( 121 | name 122 | )} in the schema being extended but it is not an object type` 123 | ); 124 | } 125 | return graphQLType; 126 | }, 127 | inputObject(name) { 128 | const graphQLType = getType(name); 129 | if (!isInputObjectType(graphQLType)) { 130 | throw new Error( 131 | `There is a type named ${JSON.stringify( 132 | name 133 | )} in the schema being extended but it is not an input object type` 134 | ); 135 | } 136 | return graphQLType; 137 | }, 138 | enum(name) { 139 | const graphQLType = getType(name); 140 | if (!isEnumType(graphQLType)) { 141 | throw new Error( 142 | `There is a type named ${JSON.stringify( 143 | name 144 | )} in the schema being extended but it is not an enum type` 145 | ); 146 | } 147 | return graphQLType; 148 | }, 149 | interface(name) { 150 | const graphQLType = getType(name); 151 | if (!isInterfaceType(graphQLType)) { 152 | throw new Error( 153 | `There is a type named ${JSON.stringify( 154 | name 155 | )} in the schema being extended but it is not an interface type` 156 | ); 157 | } 158 | return graphQLType; 159 | }, 160 | scalar(name) { 161 | if (builtinScalars.has(name)) { 162 | throw new Error( 163 | `The names of built-in scalars cannot be passed to BaseSchemaInfo.scalar but ${name} was passed` 164 | ); 165 | } 166 | const graphQLType = getType(name); 167 | if (!isScalarType(graphQLType)) { 168 | throw new Error( 169 | `There is a type named ${JSON.stringify( 170 | name 171 | )} in the schema being extended but it is not a scalar type` 172 | ); 173 | } 174 | return graphQLType; 175 | }, 176 | union(name) { 177 | const graphQLType = getType(name); 178 | if (!isUnionType(graphQLType)) { 179 | throw new Error( 180 | `There is a type named ${JSON.stringify( 181 | name 182 | )} in the schema being extended but it is not a union type` 183 | ); 184 | } 185 | return graphQLType; 186 | }, 187 | }) 188 | : extension 189 | ); 190 | const queryType = schema.getQueryType(); 191 | const mutationType = schema.getMutationType(); 192 | const typesToFind = new Set(); 193 | if (queryType) { 194 | typesToFind.add(queryType); 195 | } 196 | if (mutationType) { 197 | typesToFind.add(mutationType); 198 | } 199 | const usages = findObjectTypeUsages(schema, typesToFind); 200 | if (usages.size) { 201 | throw new Error( 202 | `@graphql-ts/extend doesn't yet support using the query and mutation types in other types but\n${[ 203 | ...usages, 204 | ] 205 | .map(([type, usages]) => { 206 | return `- ${JSON.stringify(type)} is used at ${usages 207 | .map((x) => JSON.stringify(x)) 208 | .join(", ")}`; 209 | }) 210 | .join("\n")}` 211 | ); 212 | } 213 | if (!resolvedExtension.mutation && !resolvedExtension.query) { 214 | return schema; 215 | } 216 | const newQueryType = extendObjectType( 217 | queryType, 218 | resolvedExtension.query || {}, 219 | "Query" 220 | ); 221 | const newMutationType = extendObjectType( 222 | mutationType, 223 | resolvedExtension.mutation || {}, 224 | "Mutation" 225 | ); 226 | const schemaConfig = schema.toConfig(); 227 | let types = [ 228 | ...(queryType || !newQueryType ? [] : [newQueryType]), 229 | ...(mutationType || !newMutationType ? [] : [newMutationType]), 230 | ...schemaConfig.types.map((type) => { 231 | if (newQueryType && type.name === queryType?.name) { 232 | return newQueryType; 233 | } 234 | if (newMutationType && type.name === mutationType?.name) { 235 | return newMutationType; 236 | } 237 | return type; 238 | }), 239 | ]; 240 | const updatedSchema = new GraphQLSchema({ 241 | ...schemaConfig, 242 | query: newQueryType, 243 | mutation: newMutationType, 244 | types, 245 | }); 246 | return updatedSchema; 247 | }; 248 | } 249 | 250 | function printFieldOnType(type: GraphQLObjectType, fieldName: string) { 251 | const printed = printType(type); 252 | const document = parse(printed); 253 | const parsed = document.definitions[0] as ObjectTypeDefinitionNode; 254 | const parsedField = parsed.fields!.find((x) => x.name.value === fieldName)!; 255 | return print(parsedField); 256 | } 257 | 258 | function extendObjectType( 259 | existingType: GraphQLObjectType | ExistingType, 260 | fieldsToAdd: FieldsOnAnything, 261 | defaultName: string 262 | ): GraphQLObjectType | ExistingType { 263 | const hasNewFields = Object.entries(fieldsToAdd).length; 264 | if (!hasNewFields) { 265 | return existingType; 266 | } 267 | const existingTypeConfig = existingType?.toConfig(); 268 | const newFields: GraphQLFieldConfigMap = { 269 | ...existingTypeConfig?.fields, 270 | }; 271 | for (const [key, val] of Object.entries(fieldsToAdd)) { 272 | if (newFields[key]) { 273 | throw new Error( 274 | `The schema extension defines a field ${JSON.stringify( 275 | key 276 | )} on the ${JSON.stringify( 277 | existingType!.name ?? defaultName 278 | )} type but that type already defines a field with that name.\nThe original field:\n${printFieldOnType( 279 | existingType!, 280 | key 281 | )}\nThe field added by the extension:\n${printFieldOnType( 282 | new GraphQLObjectType({ 283 | name: "ForError", 284 | fields: { 285 | [key]: val, 286 | }, 287 | }), 288 | key 289 | )}` 290 | ); 291 | } 292 | newFields[key] = val; 293 | } 294 | return new GraphQLObjectType({ 295 | name: defaultName, 296 | ...existingTypeConfig, 297 | fields: newFields, 298 | }); 299 | } 300 | 301 | // https://github.com/microsoft/TypeScript/issues/17002 302 | const isReadonlyArray: (arr: any) => arr is readonly any[] = 303 | Array.isArray as any; 304 | 305 | const operations = ["query", "mutation"] as const; 306 | 307 | function flattenExtensions( 308 | extensions: Extension | readonly Extension[] 309 | ): Extension { 310 | if (isReadonlyArray(extensions)) { 311 | const resolvedExtension: Required = { 312 | mutation: {}, 313 | query: {}, 314 | }; 315 | for (const extension of extensions) { 316 | for (const operation of operations) { 317 | const fields = extension[operation]; 318 | if (fields) { 319 | for (const [key, val] of Object.entries(fields)) { 320 | if (resolvedExtension[operation][key]) { 321 | throw new Error( 322 | `More than one extension defines a field named ${JSON.stringify( 323 | key 324 | )} on the ${operation} type.\nThe first field:\n${printFieldOnType( 325 | new GObjectType({ 326 | name: "ForError", 327 | fields: { [key]: val }, 328 | }), 329 | key 330 | )}\nThe second field:\n${printFieldOnType( 331 | new GObjectType({ 332 | name: "ForError", 333 | fields: { [key]: resolvedExtension[operation][key] }, 334 | }), 335 | key 336 | )}` 337 | ); 338 | } 339 | resolvedExtension[operation][key] = val; 340 | } 341 | } 342 | } 343 | } 344 | return resolvedExtension; 345 | } 346 | return extensions; 347 | } 348 | 349 | /** 350 | * Any 351 | * 352 | * Note the distinct usages of `any` vs `unknown` is intentional. 353 | * 354 | * - The `unknown` used for the source type is because the source isn't known and 355 | * it shouldn't generally be used here because these fields are on the query 356 | * and mutation types 357 | * - The first `any` used for the `Args` type parameter is used because `Args` is 358 | * invariant so only `Record>` would work with 359 | * it. The arguable unsafety here doesn't really matter because people will 360 | * always use `g.field` 361 | * - The `any` in `OutputType` and the last type argument mean that a field that 362 | * requires any context can be provided. This is unsafe, the only way this 363 | * could arguably be made more "safe" is by making this unknown which would 364 | * requiring casting or make `extend` and etc. generic over a `Context` but 365 | * given this is immediately used on an arbitrary {@link GraphQLSchema} so the 366 | * type would immediately be thrown away, it would be pretty much pointless. 367 | */ 368 | type FieldsOnAnything = { 369 | [key: string]: GField, unknown, any>; 370 | }; 371 | 372 | /** 373 | * An extension to a GraphQL schema. This currently only supports adding fields 374 | * to the query and mutation types. Extending other types will be supported in 375 | * the future. 376 | */ 377 | export type Extension = { 378 | /** 379 | * Extra fields to be added to the query type. 380 | * 381 | * ```ts 382 | * const extension: Extension = { 383 | * query: { 384 | * isLoggedIn: g.field({ 385 | * type: g.Boolean, 386 | * resolve(source, args, context, info) { 387 | * // ... 388 | * }, 389 | * }), 390 | * }, 391 | * }; 392 | * ``` 393 | */ 394 | // Note this is distinct from using `object.Query` because the query 395 | // type of a schema doesn't have to be named `Query` even though it 396 | // generally is. By using `query` instead of `object.Query`, these 397 | // fields will be added to the query type regardless of what it is called. 398 | query?: FieldsOnAnything; 399 | /** 400 | * Extra fields to be added to the mutation type. 401 | * 402 | * ```ts 403 | * const extension: Extension = { 404 | * mutation: { 405 | * createPost: g.field({ 406 | * type: g.Boolean, 407 | * resolve(source, args, context, info) { 408 | * // ... 409 | * }, 410 | * }), 411 | * }, 412 | * }; 413 | * ``` 414 | */ 415 | // Note this is distinct from using `object.Mutation` because the mutation 416 | // type of a schema doesn't have to be named `Mutation` even though it 417 | // generally is. By using `mutation` instead of `object.Mutation`, these 418 | // fields will be added to the mutation type regardless of what it is called. 419 | mutation?: FieldsOnAnything; 420 | // /** Extra fields to be added to an abitrary object type */ 421 | // object?: Record>; 422 | // /** Extra fields to be added to an abitrary type */ 423 | // inputObject?: Record>; 424 | /** Extra variants to add to a */ 425 | // /** If you define an object implementation of an interface but never use the object type */ 426 | // unreferencedConcreteInterfaceImplementations?: ObjectType[]; 427 | }; 428 | 429 | /** 430 | * This object contains the schema being extended and functions to get GraphQL 431 | * types from the schema. 432 | */ 433 | export type BaseSchemaMeta = { 434 | schema: GraphQLSchema; 435 | /** 436 | * Gets an {@link GObjectType object type} from the existing GraphQL schema. If 437 | * there is no object type in the existing schema with the name passed, an 438 | * error will be thrown. 439 | * 440 | * ```ts 441 | * const originalSchema = new GraphQLSchema({ ...etc }); 442 | * 443 | * const extendedSchema = extend((base) => ({ 444 | * query: { 445 | * something: g.field({ 446 | * type: base.object("Something"), 447 | * resolve() { 448 | * return { something: true }; 449 | * }, 450 | * }), 451 | * }, 452 | * }))(originalSchema); 453 | * ``` 454 | */ 455 | object(name: string): GObjectType; 456 | /** 457 | * Gets an {@link GInputObjectType input object type} from the existing GraphQL 458 | * schema. If there is no input object type in the existing schema with the 459 | * name passed, an error will be thrown. 460 | * 461 | * ```ts 462 | * const originalSchema = new GraphQLSchema({ ...etc }); 463 | * 464 | * const extendedSchema = extend((base) => ({ 465 | * query: { 466 | * something: g.field({ 467 | * type: g.String, 468 | * args: { 469 | * something: g.field({ 470 | * type: base.inputObject("Something"), 471 | * }), 472 | * }, 473 | * resolve(source, { something }) { 474 | * console.log(something); 475 | * return ""; 476 | * }, 477 | * }), 478 | * }, 479 | * }))(originalSchema); 480 | * ``` 481 | */ 482 | inputObject( 483 | name: string 484 | ): GInputObjectType<{ [key: string]: GArg }, boolean>; 485 | /** 486 | * Gets an {@link GEnumType enum type} from the existing GraphQL schema. If 487 | * there is no enum type in the existing schema with the name passed, an error 488 | * will be thrown. 489 | * 490 | * ```ts 491 | * const originalSchema = new GraphQLSchema({ ...etc }); 492 | * 493 | * const extendedSchema = extend((base) => ({ 494 | * query: { 495 | * something: g.field({ 496 | * type: base.enum("Something"), 497 | * args: { 498 | * something: g.field({ 499 | * type: base.enum("Something"), 500 | * }), 501 | * }, 502 | * resolve(source, { something }) { 503 | * return something; 504 | * }, 505 | * }), 506 | * }, 507 | * }))(originalSchema); 508 | * ``` 509 | */ 510 | enum(name: string): GEnumType>; 511 | /** 512 | * Gets a {@link GUnionType union type} from the existing GraphQL schema. If 513 | * there is no union type in the existing schema with the name passed, an 514 | * error will be thrown. 515 | * 516 | * ```ts 517 | * const originalSchema = new GraphQLSchema({ ...etc }); 518 | * 519 | * const extendedSchema = extend((base) => ({ 520 | * query: { 521 | * something: g.field({ 522 | * type: base.union("Something"), 523 | * resolve() { 524 | * return { something: true }; 525 | * }, 526 | * }), 527 | * }, 528 | * }))(originalSchema); 529 | * ``` 530 | */ 531 | union(name: string): GUnionType; 532 | /** 533 | * Gets an {@link GInterfaceType interface type} from the existing GraphQL 534 | * schema. If there is no interface type in the existing schema with the name 535 | * passed, an error will be thrown. 536 | * 537 | * ```ts 538 | * const originalSchema = new GraphQLSchema({ ...etc }); 539 | * 540 | * const extendedSchema = extend((base) => ({ 541 | * query: { 542 | * something: g.field({ 543 | * type: base.interface("Something"), 544 | * resolve() { 545 | * return { something: true }; 546 | * }, 547 | * }), 548 | * }, 549 | * }))(originalSchema); 550 | * ``` 551 | */ 552 | interface(name: string): GInterfaceType; 553 | /** 554 | * Gets a {@link GScalarType scalar type} from the existing GraphQL schema. If 555 | * there is no scalar type in the existing schema with the name passed, an 556 | * error will be thrown. 557 | * 558 | * If the name of a built-in scalar type is passed, an error will also be 559 | * thrown. 560 | * 561 | * ```ts 562 | * const originalSchema = new GraphQLSchema({ ...etc }); 563 | * 564 | * const extendedSchema = extend((base) => ({ 565 | * query: { 566 | * something: g.field({ 567 | * type: base.scalar("JSON"), 568 | * args: { 569 | * something: g.field({ 570 | * type: base.scalar("JSON"), 571 | * }), 572 | * }, 573 | * resolve(source, { something }) { 574 | * return something; 575 | * }, 576 | * }), 577 | * }, 578 | * }))(originalSchema); 579 | * ``` 580 | */ 581 | scalar(name: string): GScalarType; 582 | }; 583 | 584 | function findObjectTypeUsages( 585 | schema: GraphQLSchema, 586 | types: Set 587 | ) { 588 | const usages = new Map(); 589 | for (const [name, type] of Object.entries(schema.getTypeMap())) { 590 | if (isInterfaceType(type) || isObjectType(type)) { 591 | for (const [fieldName, field] of Object.entries(type.getFields())) { 592 | const namedType = getNamedType(field.type); 593 | if (isObjectType(namedType) && types.has(namedType)) { 594 | getOrDefault(usages, namedType, []).push(`${name}.${fieldName}`); 595 | } 596 | } 597 | } 598 | if (isUnionType(type)) { 599 | for (const member of type.getTypes()) { 600 | if (types.has(member)) { 601 | getOrDefault(usages, member, [])!.push(name); 602 | } 603 | } 604 | } 605 | } 606 | return usages; 607 | } 608 | 609 | function getOrDefault(input: Map, key: K, defaultValue: V): V { 610 | if (!input.has(key)) { 611 | input.set(key, defaultValue); 612 | return defaultValue; 613 | } 614 | return input.get(key)!; 615 | } 616 | -------------------------------------------------------------------------------- /packages/schema/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @graphql-ts/schema 2 | 3 | ## 1.0.2 4 | 5 | ### Patch Changes 6 | 7 | - [#59](https://github.com/Thinkmill/graphql-ts/pull/59) [`85c667e3a6affe4651dcfe31151e56d39c78411e`](https://github.com/Thinkmill/graphql-ts/commit/85c667e3a6affe4651dcfe31151e56d39c78411e) Thanks [@emmatown](https://github.com/emmatown)! - Add `const` to `Values` type parameter on `g.enum` to improve inference 8 | 9 | ## 1.0.1 10 | 11 | ### Patch Changes 12 | 13 | - [#55](https://github.com/Thinkmill/graphql-ts/pull/55) [`11ee5523d046a91c8e091580cc0954f9e2b108ae`](https://github.com/Thinkmill/graphql-ts/commit/11ee5523d046a91c8e091580cc0954f9e2b108ae) Thanks [@emmatown](https://github.com/emmatown)! - Improve internals of `GField` and `GInterfaceField` types 14 | 15 | ## 1.0.0 16 | 17 | ### Major Changes 18 | 19 | - [#51](https://github.com/Thinkmill/graphql-ts/pull/51) [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81) Thanks [@emmatown](https://github.com/emmatown)! - The `g` and `graphql` exports from `@graphql-ts/schema` have been removed. You should now use `gWithContext` to bind `g` to a specific context type. 20 | 21 | ```ts 22 | import { GraphQLSchema } from "graphql"; 23 | import { gWithContext } from "@graphql-ts/schema"; 24 | 25 | type Context = { 26 | something: string; 27 | }; 28 | 29 | const g = gWithContext(); 30 | type g = gWithContext.infer; 31 | 32 | const Query = g.object()({ 33 | name: "Query", 34 | fields: { 35 | something: g.field({ 36 | type: g.String, 37 | resolve(_, __, context) { 38 | return context.something; 39 | }, 40 | }), 41 | }, 42 | }); 43 | 44 | const schema = new GraphQLSchema({ 45 | query: Query, 46 | }); 47 | ``` 48 | 49 | All types previously available at `g.*` like `g.ObjectType` can written instead like `g>` or from types imported from `@graphql-ts/schema` instead of being accessible directly on `g`. 50 | 51 | - [#51](https://github.com/Thinkmill/graphql-ts/pull/51) [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81) Thanks [@emmatown](https://github.com/emmatown)! - The following types have been replaced as follows: 52 | 53 | - `ObjectType` -> `GObjectType` 54 | - `EnumType` -> `GEnumType` 55 | - `ScalarType` -> `GScalarType` 56 | - `InputObjectType` -> `GInputObjectType` 57 | - `InterfaceType` -> `GInterfaceType` 58 | - `UnionType` -> `GUnionType` 59 | - `ListType` -> `GList` 60 | - `NonNullType` -> `GNonNull` 61 | - `Arg` -> `GArg` 62 | - `Field` -> `GField` 63 | - `FieldResolver` -> `GFieldResolver` 64 | - `InterfaceField` -> `GInterfaceField` 65 | - `NullableInputType` -> `GNullableInputType` 66 | - `NullableOutputType` -> `GNullableOutputType` 67 | - `NullableType` -> `GNullableType` 68 | - `InputType` -> `GInputType` 69 | - `OutputType` -> `GOutputType` 70 | - `Type` -> `GType` 71 | 72 | They are all exactly adding `G` before the previous name except for `ListType` and `NonNullType` which are now `GList` and `GNonNull` respectively without `Type` at the end. 73 | 74 | - [#51](https://github.com/Thinkmill/graphql-ts/pull/51) [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81) Thanks [@emmatown](https://github.com/emmatown)! - All of the GraphQL types returned by `@graphql-ts/schema` are now directly runtime compatible with the equivalent types from GraphQL.js instead of being on `.graphQLType`. 75 | 76 | Handling this change should generally just require removing `.graphQLType` from where `@graphql-ts/schema` types are used with GraphQL.js, like this: 77 | 78 | ```diff 79 | import { GraphQLSchema } from "graphql"; 80 | 81 | const Query = g.object()({ 82 | name: "Query", 83 | fields: { 84 | hello: g.field({ 85 | type: g.String, 86 | resolve() { 87 | return "Hello!"; 88 | }, 89 | }), 90 | }, 91 | }); 92 | 93 | const schema = new GraphQLSchema({ 94 | - query: Query.graphQLType, 95 | + query: Query, 96 | }); 97 | ``` 98 | 99 | The types returned by `@graphql-ts/schema` are internally now extended classes of the equivalent types from GraphQL.js (though only in the types, at runtime they are re-exports). These new classes are exported from `@graphql-ts/schema` as `GObjectType` and etc. The constructors of the `G*` types can be used directly safely in place of the `g.*` functions in **some cases** though some are not safe and it's still recommended to use `g.*` to also have binding to the same context type without needed to provide it manually. 100 | 101 | - [#51](https://github.com/Thinkmill/graphql-ts/pull/51) [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81) Thanks [@emmatown](https://github.com/emmatown)! - `bindGraphQLSchemaAPIToContext` and `GraphQLSchemaAPIWithContext` have been replaced with the `gWithContext` function and `GWithContext` type respectively. They now also include all the APIs that aren't specifically bound to a context. 102 | 103 | - [#31](https://github.com/Thinkmill/graphql-ts/pull/31) [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085) Thanks [@emmatown](https://github.com/emmatown)! - `graphql@15` is no longer supported. `graphql@16.0.0` or newer is now required. 104 | 105 | - [#53](https://github.com/Thinkmill/graphql-ts/pull/53) [`d7151bd2a6333327ac1a57e0c924bd4bfdbdf01f`](https://github.com/Thinkmill/graphql-ts/commit/d7151bd2a6333327ac1a57e0c924bd4bfdbdf01f) Thanks [@emmatown](https://github.com/emmatown)! - The `@graphql-ts/schema/api-with-context` and `@graphql-ts/schema/api-without-context` entrypoints have been removed. You should use `gWithContext` and the types exported from `@graphql-ts/schema` directly instead. 106 | 107 | - [#31](https://github.com/Thinkmill/graphql-ts/pull/31) [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085) Thanks [@emmatown](https://github.com/emmatown)! - The `ObjectTypeFunc` and other `*TypeFunc` types are no longer exported. Use `GWithContext['object']`/etc. instead 108 | 109 | - [#31](https://github.com/Thinkmill/graphql-ts/pull/31) [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085) Thanks [@emmatown](https://github.com/emmatown)! - The `EnumValue` type no longer exists. The type parameter for `EnumType` is now `Record` instead of `Record>`. 110 | 111 | - [#31](https://github.com/Thinkmill/graphql-ts/pull/31) [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085) Thanks [@emmatown](https://github.com/emmatown)! - TypeScript 5.7 or newer is now required 112 | 113 | - [#51](https://github.com/Thinkmill/graphql-ts/pull/51) [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81) Thanks [@emmatown](https://github.com/emmatown)! - The `Key` type parameter on `Field` has been replaced with a new type parameter (`SourceAtKey`) and represents essentially `Source[Key]` instead of `Key`. For example, a field like this can be written: 114 | 115 | ```ts 116 | const field = g.field({ 117 | type: g.String, 118 | }); 119 | ``` 120 | 121 | and `field` will be usable like this: 122 | 123 | ```ts 124 | const Something = g.object<{ 125 | name: string 126 | }>({ 127 | name: "Something" 128 | fields: { 129 | name: field, 130 | // @ts-expect-error 131 | other: field 132 | }, 133 | }); 134 | ``` 135 | 136 | The field is usable at `name` since the source type has an object with a `name` property that's a `string` but using it at `other` will result in a type error since the source type doesn't have a `other` property. 137 | 138 | Previously, using `g.field` outside a `g.object`/`g.fields` call would require specifying a resolver and fields written within `g.fields` would be bound to be used at a specific key rather than the new behaviour of any key with the right type. 139 | 140 | This also reduces the need for `g.fields`. For example, the example given in the previous JSDoc for `g.fields`: 141 | 142 | ```ts 143 | const nodeFields = g.fields<{ id: string }>()({ 144 | id: g.field({ type: g.ID }), 145 | }); 146 | const Node = g.interface<{ id: string }>()({ 147 | name: "Node", 148 | fields: nodeFields, 149 | }); 150 | const Person = g.object<{ 151 | __typename: "Person"; 152 | id: string; 153 | name: string; 154 | }>()({ 155 | name: "Person", 156 | interfaces: [Node], 157 | fields: { 158 | ...nodeFields, 159 | name: g.field({ type: g.String }), 160 | }, 161 | }); 162 | ``` 163 | 164 | Now the `g.fields` call is unnecessary and writing `nodeFields` will no longer error at the `g.field` call and will instead work as expected. 165 | 166 | ```ts 167 | const nodeFields = { 168 | id: g.field({ type: g.ID }), 169 | }; 170 | ``` 171 | 172 | There is still some use to `g.fields` for when you want to define a number of shared fields with resolvers and specify the source type just once in the `g.fields` call rathe than in every resolver. 173 | 174 | This change is unlikely to break existing code except where you explicitly use the `Field` type or explicitly pass type parameters to `g.field` (the latter of which you likely shouldn't do) but since it changes the meaning of a type parameter of `Field`, it's regarded as a breaking change. 175 | 176 | ## 0.6.4 177 | 178 | ### Patch Changes 179 | 180 | - [#36](https://github.com/Thinkmill/graphql-ts/pull/36) [`692b08c5f7fdfb8e6aead74be2ea0841ba74dbad`](https://github.com/Thinkmill/graphql-ts/commit/692b08c5f7fdfb8e6aead74be2ea0841ba74dbad) Thanks [@emmatown](https://github.com/emmatown)! - Remove `@babel/runtime` dependency 181 | 182 | ## 0.6.3 183 | 184 | ### Patch Changes 185 | 186 | - [#32](https://github.com/Thinkmill/graphql-ts/pull/32) [`91a28cb0b8eedf4a66dca624afc71de07d1c3a11`](https://github.com/Thinkmill/graphql-ts/commit/91a28cb0b8eedf4a66dca624afc71de07d1c3a11) Thanks [@emmatown](https://github.com/emmatown)! - Revert changes to internal `FieldFuncResolve` type which broke parts of inference in a very small number of cases 187 | 188 | - [#29](https://github.com/Thinkmill/graphql-ts/pull/29) [`7756410781a21ba77616c8fbf6b36e7ab211200f`](https://github.com/Thinkmill/graphql-ts/commit/7756410781a21ba77616c8fbf6b36e7ab211200f) Thanks [@emmatown](https://github.com/emmatown)! - Improve types that enforce correct fields are provided when `interfaces` are passed to `g.object`/`g.interface` 189 | 190 | - [#29](https://github.com/Thinkmill/graphql-ts/pull/29) [`7756410781a21ba77616c8fbf6b36e7ab211200f`](https://github.com/Thinkmill/graphql-ts/commit/7756410781a21ba77616c8fbf6b36e7ab211200f) Thanks [@emmatown](https://github.com/emmatown)! - Deprecate `InterfaceToInterfaceFields` and `InterfacesToOutputFields` types. 191 | 192 | ## 0.6.2 193 | 194 | ### Patch Changes 195 | 196 | - [#24](https://github.com/Thinkmill/graphql-ts/pull/24) [`2dc7f48a07f4e235261891dd0ff3bc4ca2ec9858`](https://github.com/Thinkmill/graphql-ts/commit/2dc7f48a07f4e235261891dd0ff3bc4ca2ec9858) Thanks [@emmatown](https://github.com/emmatown)! - Add support for `isOneOf` in input object types 197 | 198 | - [#25](https://github.com/Thinkmill/graphql-ts/pull/25) [`974bdd8f2d1ca72b04dfde471fa4e119b2d1a7b1`](https://github.com/Thinkmill/graphql-ts/commit/974bdd8f2d1ca72b04dfde471fa4e119b2d1a7b1) Thanks [@emmatown](https://github.com/emmatown)! - Simplify internal `FieldFuncResolve` type 199 | 200 | - [`65f914c83a6438e8b326047c0dc3d83d47ba110c`](https://github.com/Thinkmill/graphql-ts/commit/65f914c83a6438e8b326047c0dc3d83d47ba110c) Thanks [@emmatown](https://github.com/emmatown)! - Simplify the build files in `dist` 201 | 202 | ## 0.6.1 203 | 204 | ### Patch Changes 205 | 206 | - [#19](https://github.com/Thinkmill/graphql-ts/pull/19) [`d79115e1007e6e47b425293122818e7d40edf8b5`](https://github.com/Thinkmill/graphql-ts/commit/d79115e1007e6e47b425293122818e7d40edf8b5) Thanks [@emmatown](https://github.com/emmatown)! - The `graphql` export has been renamed to `g` to make reading/writing schemas more concise and to avoid conflicts with exports named `graphql` from libraries such as Relay, GraphQL Code Generator and gql.tada that use the variable name `graphql` to declare GraphQL operations. 207 | 208 | The `graphql` export still exists but is now deprecated and may be removed in a future release. 209 | 210 | To quickly update usages of the `graphql` export in your project to use `g`: 211 | 212 | 1. Navigate to `node_modules/@graphql-ts/schema/dist/declarations/src/schema-api-alias.d.ts` in your editor ("Go to Definition" will not take you to the correct file) 213 | 2. Use "Rename Symbol" to rename `graphql` to `g`, this will update usages of `graphql` to `g` 214 | 3. Change this deprecated alias back from `g` to `graphql` (avoid using "Rename Symbol" again because you want to preserve the updates you made in step 2) 215 | 216 | You can similarly use "Rename Symbol" to quickly rename your own custom defined `graphql` to `g`. 217 | 218 | ## 0.6.0 219 | 220 | ### Minor Changes 221 | 222 | - [`012d84e`](https://github.com/Thinkmill/graphql-ts/commit/012d84e04bfe37c18aa0afdc541843586cf768bf) Thanks [@emmatown](https://github.com/emmatown)! - Added `exports` field 223 | 224 | ## 0.5.3 225 | 226 | ### Patch Changes 227 | 228 | - [`3663690`](https://github.com/Thinkmill/graphql-ts/commit/3663690f9063c72933465a6ee369795f8d1d864e) Thanks [@emmatown](https://github.com/emmatown)! - Fixed having a union of `Arg`s where at least one is nullable without a default value not resulting in an inferred input type that includes `undefined` in the union. 229 | 230 | ```ts 231 | graphql.field({ 232 | type: graphql.String, 233 | args: { 234 | something: graphql.arg({ 235 | type: 236 | Math.random() > 0.5 237 | ? graphql.nonNull(graphql.String) 238 | : graphql.String, 239 | }), 240 | }, 241 | resolve(source, { something }) { 242 | const previouslyIncorrectlyAllowedNowError: string | null = something; 243 | const correct: string | null | undefined = something; 244 | return ""; 245 | }, 246 | }); 247 | ``` 248 | 249 | ## 0.5.2 250 | 251 | ### Patch Changes 252 | 253 | - [`3e4909f`](https://github.com/Thinkmill/graphql-ts/commit/3e4909f3885a0edf3d989f8ce598f91473f97446) Thanks [@emmatown](https://github.com/emmatown)! - Fixed `Field<_, _, _, string, _>` not being assignable to `Field<_, _, _, "literal", _>` 254 | 255 | ## 0.5.1 256 | 257 | ### Patch Changes 258 | 259 | - [`ef18bba`](https://github.com/Thinkmill/graphql-ts/commit/ef18bba55773e38309f538b987099650ad66533d) Thanks [@emmatown](https://github.com/emmatown)! - Added declaration maps 260 | 261 | * [`65391d3`](https://github.com/Thinkmill/graphql-ts/commit/65391d30c7a56313325acb647110e8536008d82b) Thanks [@emmatown](https://github.com/emmatown)! - `graphql@16` is now allowed in `peerDependencies` 262 | 263 | ## 0.5.0 264 | 265 | ### Minor Changes 266 | 267 | - [`5d1c299`](https://github.com/Thinkmill/graphql-ts/commit/5d1c299ae50a8bafea8e409f9c2c1e5abedaa29a) Thanks [@emmatown](https://github.com/emmatown)! - Type parameters named `RootVal` have been renamed to `Source` and properties named `__rootVal` have been renamed to `__source`. This won't require code changes unless you've relied on the `__rootVal` properties(which you shouldn't). 268 | 269 | ## 0.4.0 270 | 271 | ### Minor Changes 272 | 273 | - [`910d1ed`](https://github.com/Thinkmill/graphql-ts/commit/910d1edc596f4a17b0a3dec3e3df8ebd94a5cb80) Thanks [@emmatown](https://github.com/emmatown)! - Replaced `fields` property on `InterfaceType` with `__fields` that does not exist at runtime to align with other types 274 | 275 | ### Patch Changes 276 | 277 | - [`6c85396`](https://github.com/Thinkmill/graphql-ts/commit/6c85396eee29d6eea75c43f54e50b90a3e63a266) Thanks [@emmatown](https://github.com/emmatown)! - Updated the definition of `graphql.union` so that the `Context` of the `UnionType` returned and the `Context` passed to `resolveType` are determined by the `Context` of the `graphql` object rather than a union of the `Context`s of the union's member types. 278 | 279 | ## 0.3.1 280 | 281 | ### Patch Changes 282 | 283 | - [`1e10a22`](https://github.com/Thinkmill/graphql-ts/commit/1e10a228e59b206f86e963d423567486fa590aab) Thanks [@emmatown](https://github.com/emmatown)! - Fixed broken links in JSDoc comments 284 | 285 | ## 0.3.0 286 | 287 | ### Minor Changes 288 | 289 | - [`6e9a2fb`](https://github.com/Thinkmill/graphql-ts/commit/6e9a2fb1b5dd2965bc9e2783dfddd8a2bacf88f6) Thanks [@emmatown](https://github.com/emmatown)! - Renamed the following exports: 290 | 291 | - `schema` → `graphql` 292 | - `bindSchemaAPIToContext` → `bindGraphQLSchemaAPIToContext` 293 | - `SchemaAPIWithContext` → `GraphQLSchemaAPIWithContext` 294 | 295 | ## 0.2.0 296 | 297 | ### Minor Changes 298 | 299 | - [#5](https://github.com/Thinkmill/graphql-ts/pull/5) [`9f2e0fa`](https://github.com/Thinkmill/graphql-ts/commit/9f2e0fab2c7c483c3f4c13b285d6a33e75bb563c) Thanks [@emmatown](https://github.com/emmatown)! - Removed `MaybeFunc` export 300 | 301 | * [#5](https://github.com/Thinkmill/graphql-ts/pull/5) [`9f2e0fa`](https://github.com/Thinkmill/graphql-ts/commit/9f2e0fab2c7c483c3f4c13b285d6a33e75bb563c) Thanks [@emmatown](https://github.com/emmatown)! - Changed the second type parameter of `Arg` from `DefaultValue extends InferValueFromInputType | undefined = InferValueFromInputType | undefined` to `HasDefaultValue extends boolean = boolean`. This makes it easier to type circular input objects because you TypeScript won't emit a circularity error and require you to provide a value for the second type parameter. For example, previously to type a circular input object, it looked like this where `undefined` had to be passed: 302 | 303 | ```ts 304 | type CircularInputType = schema.InputObjectType<{ 305 | circular: schema.Arg; 306 | }>; 307 | 308 | const Circular: CircularInputType = schema.inputObject({ 309 | name: "Circular", 310 | fields: () => ({ 311 | circular: schema.arg({ type: Circular }), 312 | }), 313 | }); 314 | ``` 315 | 316 | Now, the `undefined` type parameter can be removed 317 | 318 | ```ts 319 | type CircularInputType = schema.InputObjectType<{ 320 | circular: schema.Arg; 321 | }>; 322 | 323 | const Circular: CircularInputType = schema.inputObject({ 324 | name: "Circular", 325 | fields: () => ({ 326 | circular: schema.arg({ type: Circular }), 327 | }), 328 | }); 329 | ``` 330 | 331 | ### Patch Changes 332 | 333 | - [#5](https://github.com/Thinkmill/graphql-ts/pull/5) [`9f2e0fa`](https://github.com/Thinkmill/graphql-ts/commit/9f2e0fab2c7c483c3f4c13b285d6a33e75bb563c) Thanks [@emmatown](https://github.com/emmatown)! - Fixed list types that contain output types being assignable to `NullableInputType`/`InputType` 334 | 335 | * [#5](https://github.com/Thinkmill/graphql-ts/pull/5) [`9f2e0fa`](https://github.com/Thinkmill/graphql-ts/commit/9f2e0fab2c7c483c3f4c13b285d6a33e75bb563c) Thanks [@emmatown](https://github.com/emmatown)! - JSDoc improvements 336 | 337 | - [#5](https://github.com/Thinkmill/graphql-ts/pull/5) [`9f2e0fa`](https://github.com/Thinkmill/graphql-ts/commit/9f2e0fab2c7c483c3f4c13b285d6a33e75bb563c) Thanks [@emmatown](https://github.com/emmatown)! - Resolvers are now allowed to return `undefined` in addition to `null` for fields with nullable output types along with optional properties on a `RootVal` without a resolver being allowed. Resolvers still cannot return `void`(no return) and a property being missing from a `RootVal` without a resolver is still disallowed. 338 | 339 | ## 0.1.2 340 | 341 | ### Patch Changes 342 | 343 | - [`6e4562f`](https://github.com/Thinkmill/graphql-ts/commit/6e4562fbeaf0c3e1b2dfba215bfe08cd957ae8fd) Thanks [@emmatown](https://github.com/emmatown)! - Updated mentions of `types.` to `schema.` in JSDoc comments 344 | 345 | ## 0.1.1 346 | 347 | ### Patch Changes 348 | 349 | - [`25e4344`](https://github.com/Thinkmill/graphql-ts/commit/25e4344764f509152e1bae47f09b9633026db517) Thanks [@emmatown](https://github.com/emmatown)! - When using list types, you can now return iterables instead of only arrays from resolvers 350 | 351 | * [`25e4344`](https://github.com/Thinkmill/graphql-ts/commit/25e4344764f509152e1bae47f09b9633026db517) Thanks [@emmatown](https://github.com/emmatown)! - Added JSDoc comments to most fields on `SchemaAPIWithContext` 352 | 353 | ## 0.1.0 354 | 355 | ### Minor Changes 356 | 357 | - [`2c9d25a`](https://github.com/Thinkmill/graphql-ts/commit/2c9d25ab7724a8a460b337a4a529accc0d3169ec) Thanks [@emmatown](https://github.com/emmatown)! - Initial release 358 | -------------------------------------------------------------------------------- /packages/schema/README.md: -------------------------------------------------------------------------------- 1 | # @graphql-ts/schema 2 | 3 | `@graphql-ts/schema` is a thin wrapper around 4 | [GraphQL.js](https://github.com/graphql/graphql-js) providing type-safety for 5 | constructing GraphQL Schemas while avoiding type-generation, [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) 6 | and [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html). 7 | 8 | ```ts 9 | import { gWithContext } from "@graphql-ts/schema"; 10 | import { GraphQLSchema, graphql } from "graphql"; 11 | 12 | type Context = { 13 | loadPerson: (id: string) => Person | undefined; 14 | loadFriends: (id: string) => Person[]; 15 | }; 16 | const g = gWithContext(); 17 | type g = gWithContext.infer; 18 | 19 | type Person = { 20 | id: string; 21 | name: string; 22 | }; 23 | 24 | const Person: g> = g.object()({ 25 | name: "Person", 26 | fields: () => ({ 27 | id: g.field({ type: g.nonNull(g.ID) }), 28 | name: g.field({ type: g.nonNull(g.String) }), 29 | friends: g.field({ 30 | type: g.list(g.nonNull(Person)), 31 | resolve(source, _, context) { 32 | return context.loadFriends(source.id); 33 | }, 34 | }), 35 | }), 36 | }); 37 | 38 | const Query = g.object()({ 39 | name: "Query", 40 | fields: { 41 | person: g.field({ 42 | type: Person, 43 | args: { 44 | id: g.arg({ type: g.nonNull(g.ID) }), 45 | }, 46 | resolve(_, args, context) { 47 | return context.loadPerson(args.id); 48 | }, 49 | }), 50 | }, 51 | }); 52 | 53 | const schema = new GraphQLSchema({ 54 | query: Query, 55 | }); 56 | 57 | { 58 | const people = new Map([ 59 | ["1", { id: "1", name: "Alice" }], 60 | ["2", { id: "2", name: "Bob" }], 61 | ]); 62 | const friends = new Map([ 63 | ["1", ["2"]], 64 | ["2", ["1"]], 65 | ]); 66 | const contextValue: Context = { 67 | loadPerson: (id) => people.get(id), 68 | loadFriends: (id) => { 69 | return (friends.get(id) ?? []) 70 | .map((id) => people.get(id)) 71 | .filter((person) => person !== undefined) as Person[]; 72 | }, 73 | }; 74 | graphql({ 75 | source: ` 76 | query { 77 | person(id: "1") { 78 | id 79 | name 80 | friends { 81 | id 82 | name 83 | } 84 | } 85 | } 86 | `, 87 | schema, 88 | contextValue, 89 | }).then((result) => { 90 | console.log(result); 91 | }); 92 | } 93 | ``` 94 | -------------------------------------------------------------------------------- /packages/schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphql-ts/schema", 3 | "version": "1.0.2", 4 | "description": "Type-safety for constructing GraphQL schemas in TypeScript", 5 | "main": "dist/graphql-ts-schema.cjs.js", 6 | "module": "dist/graphql-ts-schema.esm.js", 7 | "exports": { 8 | ".": { 9 | "types": "./dist/graphql-ts-schema.cjs.js", 10 | "module": "./dist/graphql-ts-schema.esm.js", 11 | "default": "./dist/graphql-ts-schema.cjs.js" 12 | }, 13 | "./package.json": "./package.json" 14 | }, 15 | "files": [ 16 | "dist", 17 | "api-without-context", 18 | "api-with-context", 19 | "types", 20 | "src" 21 | ], 22 | "license": "MIT", 23 | "peerDependencies": { 24 | "graphql": "16" 25 | }, 26 | "devDependencies": { 27 | "graphql": "^16.3.0" 28 | }, 29 | "repository": "https://github.com/Thinkmill/graphql-ts/tree/main/packages/schema" 30 | } 31 | -------------------------------------------------------------------------------- /packages/schema/src/g-for-doc-references.ts: -------------------------------------------------------------------------------- 1 | import { GWithContext } from "./output"; 2 | 3 | export declare const g: GWithContext; 4 | -------------------------------------------------------------------------------- /packages/schema/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `@graphql-ts/schema` is a thin wrapper around 3 | * [GraphQL.js](https://github.com/graphql/graphql-js) providing type-safety for 4 | * constructing GraphQL Schemas while avoiding type-generation, [declaration 5 | * merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) 6 | * and 7 | * [decorators](https://www.typescriptlang.org/docs/handbook/decorators.html). 8 | * 9 | * ```ts 10 | * import { gWithContext } from "@graphql-ts/schema"; 11 | * import { GraphQLSchema, graphql } from "graphql"; 12 | * 13 | * type Context = { 14 | * loadPerson: (id: string) => Person | undefined; 15 | * loadFriends: (id: string) => Person[]; 16 | * }; 17 | * const g = gWithContext(); 18 | * type g = gWithContext.infer; 19 | * 20 | * type Person = { 21 | * id: string; 22 | * name: string; 23 | * }; 24 | * 25 | * const Person: g> = g.object()({ 26 | * name: "Person", 27 | * fields: () => ({ 28 | * id: g.field({ type: g.nonNull(g.ID) }), 29 | * name: g.field({ type: g.nonNull(g.String) }), 30 | * friends: g.field({ 31 | * type: g.list(g.nonNull(Person)), 32 | * resolve(source, _, context) { 33 | * return context.loadFriends(source.id); 34 | * }, 35 | * }), 36 | * }), 37 | * }); 38 | * 39 | * const Query = g.object()({ 40 | * name: "Query", 41 | * fields: { 42 | * person: g.field({ 43 | * type: Person, 44 | * args: { 45 | * id: g.arg({ type: g.nonNull(g.ID) }), 46 | * }, 47 | * resolve(_, args, context) { 48 | * return context.loadPerson(args.id); 49 | * }, 50 | * }), 51 | * }, 52 | * }); 53 | * 54 | * const schema = new GraphQLSchema({ 55 | * query: Query, 56 | * }); 57 | * 58 | * { 59 | * const people = new Map([ 60 | * ["1", { id: "1", name: "Alice" }], 61 | * ["2", { id: "2", name: "Bob" }], 62 | * ]); 63 | * const friends = new Map([ 64 | * ["1", ["2"]], 65 | * ["2", ["1"]], 66 | * ]); 67 | * const contextValue: Context = { 68 | * loadPerson: (id) => people.get(id), 69 | * loadFriends: (id) => { 70 | * return (friends.get(id) ?? []) 71 | * .map((id) => people.get(id)) 72 | * .filter((person) => person !== undefined) as Person[]; 73 | * }, 74 | * }; 75 | * graphql({ 76 | * source: ` 77 | * query { 78 | * person(id: "1") { 79 | * id 80 | * name 81 | * friends { 82 | * id 83 | * name 84 | * } 85 | * } 86 | * } 87 | * `, 88 | * schema, 89 | * contextValue, 90 | * }).then((result) => { 91 | * console.log(result); 92 | * }); 93 | * } 94 | * ``` 95 | * 96 | * @module 97 | */ 98 | import { gWithContext, type GWithContext } from "./output"; 99 | export { gWithContext }; 100 | export type { GWithContext }; 101 | 102 | export { 103 | GObjectType, 104 | GEnumType, 105 | GScalarType, 106 | GInputObjectType, 107 | GInterfaceType, 108 | GUnionType, 109 | GList, 110 | GNonNull, 111 | type GArg, 112 | type GField, 113 | type GFieldResolver, 114 | type GInterfaceField, 115 | type GNullableInputType, 116 | type GNullableOutputType, 117 | type GNullableType, 118 | type GInputType, 119 | type GOutputType, 120 | type GType, 121 | type InferValueFromOutputType, 122 | type InferValueFromArg, 123 | type InferValueFromArgs, 124 | type InferValueFromInputType, 125 | } from "./types"; 126 | -------------------------------------------------------------------------------- /packages/schema/src/output.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GraphQLFieldExtensions, 3 | GraphQLObjectTypeConfig, 4 | GraphQLResolveInfo, 5 | GraphQLUnionTypeConfig, 6 | GraphQLArgumentConfig, 7 | GraphQLInputFieldConfig, 8 | GraphQLScalarTypeConfig, 9 | FieldDefinitionNode, 10 | } from "graphql"; 11 | import { 12 | GArg, 13 | GEnumType, 14 | GEnumTypeConfig, 15 | GEnumValueConfig, 16 | GField, 17 | GInputObjectType, 18 | GInputObjectTypeConfig, 19 | GInputType, 20 | GInterfaceField, 21 | GInterfaceType, 22 | GInterfaceTypeConfig, 23 | GList, 24 | GNonNull, 25 | GNullableInputType, 26 | GNullableType, 27 | GObjectType, 28 | GOutputType, 29 | GScalarType, 30 | GType, 31 | GUnionType, 32 | InferValueFromOutputType, 33 | InferValueFromArgs, 34 | InferValueFromInputType, 35 | } from "./types"; 36 | import { 37 | GraphQLBoolean, 38 | GraphQLID, 39 | GraphQLInt, 40 | GraphQLFloat, 41 | GraphQLString, 42 | } from "graphql"; 43 | import type { g } from "./g-for-doc-references"; 44 | 45 | type SomeTypeThatIsntARecordOfArgs = string; 46 | 47 | type ImpliedResolver< 48 | Args extends { [Key in keyof Args]: GArg }, 49 | Type extends GOutputType, 50 | Context, 51 | > = 52 | | InferValueFromOutputType 53 | | (( 54 | args: InferValueFromArgs, 55 | context: Context, 56 | info: GraphQLResolveInfo 57 | ) => InferValueFromOutputType); 58 | 59 | type Maybe = T | null | undefined; 60 | 61 | type FieldFuncArgs< 62 | Source, 63 | Args extends { [Key in keyof Args]: GArg }, 64 | Type extends GOutputType, 65 | Context, 66 | > = { 67 | args?: Args; 68 | type: Type; 69 | description?: Maybe; 70 | deprecationReason?: Maybe; 71 | extensions?: Maybe>>; 72 | astNode?: Maybe; 73 | }; 74 | 75 | /** @deprecated */ 76 | export type InterfaceToInterfaceFields< 77 | Interface extends GInterfaceType, 78 | > = Interface extends GInterfaceType ? Fields : never; 79 | 80 | type InterfaceFieldsToOutputFields< 81 | Source, 82 | Context, 83 | Fields extends { [key: string]: GInterfaceField }, 84 | > = { 85 | [Key in keyof Fields]: Fields[Key] extends GInterfaceField< 86 | infer Args, 87 | infer OutputType, 88 | any 89 | > 90 | ? GField< 91 | Source, 92 | Args, 93 | OutputType, 94 | Key extends keyof Source ? Source[Key] : unknown, 95 | Context 96 | > 97 | : never; 98 | }; 99 | 100 | /** @deprecated */ 101 | export type _InterfacesToOutputFields< 102 | Source, 103 | Context, 104 | Interfaces extends readonly GInterfaceType[], 105 | > = InterfacesToOutputFields; 106 | export type { _InterfacesToOutputFields as InterfacesToOutputFields }; 107 | 108 | type InterfacesToOutputFields< 109 | Source, 110 | Context, 111 | Interfaces extends readonly GInterfaceType[], 112 | > = MergeTuple< 113 | { 114 | [Key in keyof Interfaces]: Interfaces[Key] extends GInterfaceType< 115 | Source, 116 | infer Fields, 117 | any 118 | > 119 | ? InterfaceFieldsToOutputFields 120 | : never; 121 | }, 122 | {} 123 | >; 124 | 125 | export type InterfacesToInterfaceFields< 126 | Interfaces extends readonly GInterfaceType[], 127 | > = MergeTuple< 128 | { 129 | [Key in keyof Interfaces]: Interfaces[Key] extends GInterfaceType< 130 | any, 131 | infer Fields, 132 | any 133 | > 134 | ? Fields 135 | : never; 136 | }, 137 | {} 138 | >; 139 | 140 | type MergeTuple = T extends readonly [infer U, ...infer Rest] 141 | ? MergeTuple 142 | : Merged; 143 | 144 | export type GWithContext = { 145 | /** 146 | * Creates a GraphQL object type. 147 | * 148 | * Note this is an **output** type, if you want an input object, use 149 | * `g.inputObject`. 150 | * 151 | * When calling `g.object`, you must provide a type parameter that is the 152 | * source of the object type. The source is what you receive as the first 153 | * argument of resolvers on this type and what you must return from resolvers 154 | * of fields that return this type. 155 | * 156 | * ```ts 157 | * const Person = g.object<{ name: string }>()({ 158 | * name: "Person", 159 | * fields: { 160 | * name: g.field({ type: g.String }), 161 | * }, 162 | * }); 163 | * // == 164 | * graphql` 165 | * type Person { 166 | * name: String 167 | * } 168 | * `; 169 | * ``` 170 | * 171 | * ## Writing resolvers 172 | * 173 | * To do anything other than just return a field from the source type, you 174 | * need to provide a resolver. 175 | * 176 | * Note: TypeScript will force you to provide a resolve function if the field 177 | * in the source type and the GraphQL field don't match 178 | * 179 | * ```ts 180 | * const Person = g.object<{ name: string }>()({ 181 | * name: "Person", 182 | * fields: { 183 | * name: g.field({ type: g.String }), 184 | * excitedName: g.field({ 185 | * type: g.String, 186 | * resolve(source, args, context, info) { 187 | * return `${source.name}!`; 188 | * }, 189 | * }), 190 | * }, 191 | * }); 192 | * ``` 193 | * 194 | * ## Circularity 195 | * 196 | * GraphQL types will often contain references to themselves and to make 197 | * TypeScript allow that, you need have an explicit type annotation of 198 | * `g>` along with making `fields` a function that 199 | * returns the object. 200 | * 201 | * ```ts 202 | * type PersonSource = { name: string; friends: PersonSource[] }; 203 | * 204 | * const Person: g> = 205 | * g.object()({ 206 | * name: "Person", 207 | * fields: () => ({ 208 | * name: g.field({ type: g.String }), 209 | * friends: g.field({ type: g.list(Person) }), 210 | * }), 211 | * }); 212 | * ``` 213 | */ 214 | object: < 215 | Source, 216 | >(youOnlyNeedToPassATypeParameterToThisFunctionYouPassTheActualRuntimeArgsOnTheResultOfThisFunction?: { 217 | youOnlyNeedToPassATypeParameterToThisFunctionYouPassTheActualRuntimeArgsOnTheResultOfThisFunction: true; 218 | }) => < 219 | Fields extends { 220 | [Key in keyof Fields]: GField< 221 | Source, 222 | any, 223 | any, 224 | Key extends keyof Source ? Source[Key] : unknown, 225 | Context 226 | >; 227 | } & InterfaceFieldsToOutputFields< 228 | Source, 229 | Context, 230 | InterfacesToInterfaceFields 231 | >, 232 | const Interfaces extends readonly GInterfaceType< 233 | Source, 234 | any, 235 | Context 236 | >[] = [], 237 | >( 238 | config: { 239 | fields: Fields | (() => Fields); 240 | interfaces?: [...Interfaces]; 241 | } & Omit, "fields" | "interfaces"> 242 | ) => GObjectType; 243 | /** 244 | * Create a GraphQL union type. 245 | * 246 | * A union type represents an object that could be one of a list of types. 247 | * Note it is similar to an {@link GInterfaceType interface type} except that a 248 | * union doesn't imply having a common set of fields among the member types. 249 | * 250 | * ```ts 251 | * const A = g.object<{ __typename: "A" }>()({ 252 | * name: "A", 253 | * fields: { 254 | * something: g.field({ type: g.String }), 255 | * }, 256 | * }); 257 | * const B = g.object<{ __typename: "B" }>()({ 258 | * name: "B", 259 | * fields: { 260 | * differentThing: g.field({ type: g.String }), 261 | * }, 262 | * }); 263 | * const AOrB = g.union({ 264 | * name: "AOrB", 265 | * types: [A, B], 266 | * }); 267 | * ``` 268 | */ 269 | union: >( 270 | config: Flatten< 271 | Omit< 272 | GraphQLUnionTypeConfig< 273 | Type extends GObjectType ? Source : never, 274 | Context 275 | >, 276 | "types" 277 | > & { 278 | types: readonly Type[] | (() => readonly Type[]); 279 | } 280 | > 281 | ) => GUnionType< 282 | Type extends GObjectType ? Source : never, 283 | Context 284 | >; 285 | /** 286 | * Creates a GraphQL field. 287 | * 288 | * These will generally be passed directly to the `fields` object in a 289 | * `g.object` call. 290 | * 291 | * ```ts 292 | * const Something = g.object<{ thing: string }>()({ 293 | * name: "Something", 294 | * fields: { 295 | * thing: g.field({ type: g.String }), 296 | * }, 297 | * }); 298 | * ``` 299 | */ 300 | field: < 301 | Source, 302 | Type extends GOutputType, 303 | Resolve, 304 | Args extends { [Key in keyof Args]: GArg } = {}, 305 | >( 306 | field: FieldFuncArgs & 307 | (Resolve extends {} 308 | ? { 309 | resolve: (( 310 | source: Source, 311 | args: InferValueFromArgs< 312 | SomeTypeThatIsntARecordOfArgs extends Args ? {} : Args 313 | >, 314 | context: Context, 315 | info: GraphQLResolveInfo 316 | ) => InferValueFromOutputType) & 317 | Resolve; 318 | } 319 | : { 320 | resolve?: (( 321 | source: Source, 322 | args: InferValueFromArgs< 323 | SomeTypeThatIsntARecordOfArgs extends Args ? {} : Args 324 | >, 325 | context: Context, 326 | info: GraphQLResolveInfo 327 | ) => InferValueFromOutputType) & 328 | Resolve; 329 | }) 330 | ) => GField< 331 | Source, 332 | Args, 333 | Type, 334 | Resolve extends {} ? unknown : ImpliedResolver, 335 | Context 336 | >; 337 | /** 338 | * A helper to declare fields while providing the source type a single time 339 | * rather than in every resolver. 340 | * 341 | * ```ts 342 | * const nodeFields = g.fields<{ id: string }>()({ 343 | * id: g.field({ type: g.ID }), 344 | * relatedIds: g.field({ 345 | * type: g.list(g.ID), 346 | * resolve(source) { 347 | * return loadRelatedIds(source.id); 348 | * }, 349 | * }), 350 | * otherRelatedIds: g.field({ 351 | * type: g.list(g.ID), 352 | * resolve(source) { 353 | * return loadOtherRelatedIds(source.id); 354 | * }, 355 | * }), 356 | * }); 357 | * 358 | * const Person = g.object<{ 359 | * id: string; 360 | * name: string; 361 | * }>()({ 362 | * name: "Person", 363 | * fields: { 364 | * ...nodeFields, 365 | * name: g.field({ type: g.String }), 366 | * }, 367 | * }); 368 | * ``` 369 | */ 370 | fields: < 371 | Source, 372 | >(youOnlyNeedToPassATypeParameterToThisFunctionYouPassTheActualRuntimeArgsOnTheResultOfThisFunction?: { 373 | youOnlyNeedToPassATypeParameterToThisFunctionYouPassTheActualRuntimeArgsOnTheResultOfThisFunction: true; 374 | }) => < 375 | Fields extends Record< 376 | string, 377 | GField, any, Context> 378 | > & { 379 | [Key in keyof Source]?: GField< 380 | Source, 381 | any, 382 | GOutputType, 383 | Source[Key], 384 | Context 385 | >; 386 | }, 387 | >( 388 | fields: Fields 389 | ) => Fields; 390 | /** 391 | * Creates a GraphQL interface field. 392 | * 393 | * These will generally be passed directly to the `fields` object in a 394 | * {@link g.interface} call. Interfaces fields are similar to 395 | * {@link GField regular fields} except that they **don't define how the field 396 | * is resolved**. 397 | * 398 | * ```ts 399 | * const Entity = g.interface()({ 400 | * name: "Entity", 401 | * fields: { 402 | * name: g.interfaceField({ type: g.String }), 403 | * }, 404 | * }); 405 | * ``` 406 | * 407 | * Note that {@link GField regular fields} are assignable to 408 | * {@link GInterfaceField interface fields} but the opposite is not true. This 409 | * means that you can use a regular field in an 410 | * {@link GInterfaceType interface type}. 411 | */ 412 | interfaceField: < 413 | Args extends { [Key in keyof Args]: GArg }, 414 | Type extends GOutputType, 415 | >( 416 | field: GInterfaceField 417 | ) => GInterfaceField; 418 | /** 419 | * Creates a GraphQL interface type that can be implemented by other GraphQL 420 | * object and interface types. 421 | * 422 | * ```ts 423 | * const Entity = g.interface()({ 424 | * name: "Entity", 425 | * fields: { 426 | * name: g.interfaceField({ type: g.String }), 427 | * }, 428 | * }); 429 | * 430 | * type PersonSource = { __typename: "Person"; name: string }; 431 | * 432 | * const Person = g.object()({ 433 | * name: "Person", 434 | * interfaces: [Entity], 435 | * fields: { 436 | * name: g.field({ type: g.String }), 437 | * }, 438 | * }); 439 | * 440 | * type OrganisationSource = { 441 | * __typename: "Organisation"; 442 | * name: string; 443 | * }; 444 | * 445 | * const Organisation = g.object()({ 446 | * name: "Organisation", 447 | * interfaces: [Entity], 448 | * fields: { 449 | * name: g.field({ type: g.String }), 450 | * }, 451 | * }); 452 | * ``` 453 | * 454 | * ## Resolving Types 455 | * 456 | * When using GraphQL interface and union types, there needs to a way to 457 | * determine which concrete object type has been returned from a resolver. 458 | * With `graphql-js` and `@graphql-ts/schema`, this is done with `isTypeOf` on 459 | * object types and `resolveType` on interface and union types. Note 460 | * `@graphql-ts/schema` **does not aim to strictly type the implementation of 461 | * `resolveType` and `isTypeOf`**. If you don't provide `resolveType` or 462 | * `isTypeOf`, a `__typename` property on the source type will be used, if 463 | * that fails, an error will be thrown at runtime. 464 | * 465 | * ## Fields vs Interface Fields 466 | * 467 | * You might have noticed that `g.interfaceField` was used instead of 468 | * `g.field` for the fields on the interfaces. This is because **interfaces 469 | * aren't defining implementation of fields** which means that fields on an 470 | * interface don't need define resolvers. 471 | * 472 | * ## Sharing field implementations 473 | * 474 | * Even though interfaces don't contain field implementations, you may still 475 | * want to share field implementations between interface implementations. You 476 | * can use `g.fields` to do that. See `g.fields` for more information about 477 | * why you should use `g.fields` instead of just defining an object the fields 478 | * and spreading that. 479 | * 480 | * ```ts 481 | * const nodeFields = g.fields<{ id: string }>({ 482 | * id: g.field({ type: g.ID }), 483 | * }); 484 | * 485 | * const Node = g.field({ 486 | * name: "Node", 487 | * fields: nodeFields, 488 | * }); 489 | * 490 | * const Person = g.object<{ 491 | * __typename: "Person"; 492 | * id: string; 493 | * name: string; 494 | * }>()({ 495 | * name: "Person", 496 | * interfaces: [Node], 497 | * fields: { 498 | * ...nodeFields, 499 | * name: g.field({ type: g.String }), 500 | * }, 501 | * }); 502 | * ``` 503 | */ 504 | interface: < 505 | Source, 506 | >(youOnlyNeedToPassATypeParameterToThisFunctionYouPassTheActualRuntimeArgsOnTheResultOfThisFunction?: { 507 | youOnlyNeedToPassATypeParameterToThisFunctionYouPassTheActualRuntimeArgsOnTheResultOfThisFunction: true; 508 | }) => < 509 | Fields extends { 510 | [Key in keyof Fields]: GInterfaceField< 511 | any, 512 | GOutputType, 513 | Context 514 | >; 515 | } & InterfacesToInterfaceFields, 516 | const Interfaces extends readonly GInterfaceType< 517 | Source, 518 | any, 519 | Context 520 | >[] = [], 521 | >( 522 | config: GInterfaceTypeConfig 523 | ) => GInterfaceType; 524 | /** 525 | * A shorthand to easily create {@link GEnumValueConfig enum values} to pass to 526 | * {@link g.enum}. 527 | * 528 | * If you need to set a `description` or `deprecationReason` for an enum 529 | * variant, you should pass values directly to `g.enum` without using 530 | * `g.enumValues`. 531 | * 532 | * ```ts 533 | * const MyEnum = g.enum({ 534 | * name: "MyEnum", 535 | * values: g.enumValues(["a", "b"]), 536 | * }); 537 | * ``` 538 | * 539 | * ```ts 540 | * const values = g.enumValues(["a", "b"]); 541 | * 542 | * assertDeepEqual(values, { 543 | * a: { value: "a" }, 544 | * b: { value: "b" }, 545 | * }); 546 | * ``` 547 | */ 548 | enumValues: ( 549 | values: readonly [...Values] 550 | ) => { 551 | [Key in Values[number]]: GEnumValueConfig; 552 | }; 553 | 554 | /** 555 | * Creates an {@link GEnumType enum type} with a number of 556 | * {@link GEnumValueConfig enum values}. 557 | * 558 | * ```ts 559 | * const MyEnum = g.enum({ 560 | * name: "MyEnum", 561 | * values: g.enumValues(["a", "b"]), 562 | * }); 563 | * // == 564 | * graphql` 565 | * enum MyEnum { 566 | * a 567 | * b 568 | * } 569 | * `; 570 | * ``` 571 | * 572 | * ```ts 573 | * const MyEnum = g.enum({ 574 | * name: "MyEnum", 575 | * description: "My enum does things", 576 | * values: { 577 | * something: { 578 | * description: "something something", 579 | * value: "something", 580 | * }, 581 | * thing: { 582 | * description: "thing thing", 583 | * deprecationReason: "something should be used instead of thing", 584 | * value: "thing", 585 | * }, 586 | * }, 587 | * }); 588 | * // == 589 | * graphql` 590 | * """ 591 | * My enum does things 592 | * """ 593 | * enum MyEnum { 594 | * """ 595 | * something something 596 | * """ 597 | * something 598 | * """ 599 | * thing thing 600 | * """ 601 | * thing @\deprecated(reason: "something should be used instead of thing") 602 | * } 603 | * `;) 604 | * ``` 605 | */ 606 | enum: >( 607 | config: GEnumTypeConfig 608 | ) => GEnumType; 609 | /** 610 | * Creates a {@link GArg GraphQL argument}. 611 | * 612 | * Args can can be used as arguments on output fields: 613 | * 614 | * ```ts 615 | * g.field({ 616 | * type: g.String, 617 | * args: { 618 | * something: g.arg({ type: g.String }), 619 | * }, 620 | * resolve(source, { something }) { 621 | * return something || somethingElse; 622 | * }, 623 | * }); 624 | * // == 625 | * graphql`(something: String): String`; 626 | * ``` 627 | * 628 | * Or as fields on input objects: 629 | * 630 | * ```ts 631 | * const Something = g.inputObject({ 632 | * name: "Something", 633 | * fields: { 634 | * something: g.arg({ type: g.String }), 635 | * }, 636 | * }); 637 | * // == 638 | * graphql` 639 | * input Something { 640 | * something: String 641 | * } 642 | * `; 643 | * ``` 644 | */ 645 | arg: < 646 | Type extends GInputType, 647 | DefaultValue extends InferValueFromInputType | undefined = undefined, 648 | >( 649 | arg: Flatten< 650 | { 651 | type: Type; 652 | } & Omit< 653 | GraphQLInputFieldConfig & GraphQLArgumentConfig, 654 | "type" | "defaultValue" 655 | > 656 | > & 657 | (undefined extends DefaultValue 658 | ? { defaultValue?: DefaultValue } 659 | : { defaultValue: DefaultValue }) 660 | ) => GArg; 661 | /** 662 | * Creates an {@link GInputObjectType input object type} 663 | * 664 | * ```ts 665 | * const Something = g.inputObject({ 666 | * name: "Something", 667 | * fields: { 668 | * something: g.arg({ type: g.String }), 669 | * }, 670 | * }); 671 | * // == 672 | * graphql` 673 | * input Something { 674 | * something: String 675 | * } 676 | * `; 677 | * ``` 678 | * 679 | * ### Handling circular objects 680 | * 681 | * Circular input objects require explicitly specifying the fields on the 682 | * object in the type because of TypeScript's limits with circularity. 683 | * 684 | * ```ts 685 | * import { GInputObjectType } from "@graphql-ts/schema"; 686 | * 687 | * type SomethingInputType = GInputObjectType<{ 688 | * something: g>; 689 | * }>; 690 | * const Something: SomethingInputType = g.inputObject({ 691 | * name: "Something", 692 | * fields: () => ({ 693 | * something: g.arg({ type: Something }), 694 | * }), 695 | * }); 696 | * ``` 697 | * 698 | * You can specify all of your non-circular fields outside of the fields 699 | * object and then use `typeof` to get the type to avoid writing the 700 | * non-circular fields as types again. 701 | * 702 | * ```ts 703 | * import { GInputObjectType } from "@graphql-ts/schema"; 704 | * 705 | * const nonCircularFields = { 706 | * thing: g.arg({ type: g.String }), 707 | * }; 708 | * type SomethingInputType = GInputObjectType< 709 | * typeof nonCircularFields & { 710 | * something: g>; 711 | * } 712 | * >; 713 | * const Something: SomethingInputType = g.inputObject({ 714 | * name: "Something", 715 | * fields: () => ({ 716 | * ...nonCircularFields, 717 | * something: g.arg({ type: Something }), 718 | * }), 719 | * }); 720 | * ``` 721 | */ 722 | inputObject: < 723 | Fields extends { 724 | [key: string]: IsOneOf extends true 725 | ? GArg 726 | : GArg; 727 | }, 728 | IsOneOf extends boolean = false, 729 | >( 730 | config: GInputObjectTypeConfig 731 | ) => GInputObjectType; 732 | /** 733 | * Wraps any GraphQL type in a {@link GList list type}. 734 | * 735 | * ```ts 736 | * const stringListType = g.list(g.String); 737 | * // == 738 | * graphql`[String]`; 739 | * ``` 740 | * 741 | * When used as an input type, you will recieve an array of the inner type. 742 | * 743 | * ```ts 744 | * g.field({ 745 | * type: g.String, 746 | * args: { thing: g.arg({ type: g.list(g.String) }) }, 747 | * resolve(source, { thing }) { 748 | * const theThing: undefined | null | Array = thing; 749 | * return ""; 750 | * }, 751 | * }); 752 | * ``` 753 | * 754 | * When used as an output type, you can return an iterable of the inner type 755 | * that also matches `typeof val === 'object'` so for example, you'll probably 756 | * return an Array most of the time but you could also return a Set you 757 | * couldn't return a string though, even though a string is an iterable, it 758 | * doesn't match `typeof val === 'object'`. 759 | * 760 | * ```ts 761 | * g.field({ 762 | * type: g.list(g.String), 763 | * resolve() { 764 | * return [""]; 765 | * }, 766 | * }); 767 | * ``` 768 | * 769 | * ```ts 770 | * g.field({ 771 | * type: g.list(g.String), 772 | * resolve() { 773 | * return new Set([""]); 774 | * }, 775 | * }); 776 | * ``` 777 | * 778 | * ```ts 779 | * g.field({ 780 | * type: g.list(g.String), 781 | * resolve() { 782 | * // this will not be allowed 783 | * return "some things"; 784 | * }, 785 | * }); 786 | * ``` 787 | */ 788 | list: >(of: Of) => GList; 789 | /** 790 | * Wraps a {@link GNullableType nullable type} with a 791 | * {@link GNonNull non-nullable type}. 792 | * 793 | * Types in GraphQL are always nullable by default so if you want to enforce 794 | * that a type must always be there, you can use the non-null type. 795 | * 796 | * ```ts 797 | * const nonNullableString = g.nonNull(g.String); 798 | * // == 799 | * graphql`String!`; 800 | * ``` 801 | * 802 | * When using a non-null type as an input type, your resolver will never 803 | * recieve null and consumers of your GraphQL API **must** provide a value for 804 | * it unless you provide a default value. 805 | * 806 | * ```ts 807 | * g.field({ 808 | * args: { 809 | * someNonNullAndRequiredArg: g.arg({ 810 | * type: g.nonNull(g.String), 811 | * }), 812 | * someNonNullButOptionalArg: g.arg({ 813 | * type: g.nonNull(g.String), 814 | * defaultValue: "some default", 815 | * }), 816 | * }, 817 | * type: g.String, 818 | * resolve(source, args) { 819 | * // both of these will always be a string 820 | * args.someNonNullAndRequiredArg; 821 | * args.someNonNullButOptionalArg; 822 | * 823 | * return ""; 824 | * }, 825 | * }); 826 | * // == 827 | * graphql` 828 | * fieldName( 829 | * someNonNullAndRequiredArg: String! 830 | * someNonNullButOptionalArg: String! = "some default" 831 | * ): String 832 | * `; 833 | * ``` 834 | * 835 | * When using a non-null type as an output type, your resolver must never 836 | * return null. If you do return null(which unless you do 837 | * type-casting/ts-ignore/etc. `@graphql-ts/schema` will not let you do) 838 | * graphql-js will return an error to consumers of your GraphQL API. 839 | * 840 | * Non-null types should be used very carefully on output types. If you have 841 | * to do a fallible operation like a network request or etc. to get the value, 842 | * it probably shouldn't be non-null. If you make a field non-null and doing 843 | * the fallible operation fails, consumers of your GraphQL API will be unable 844 | * to see any of the other fields on the object that the non-null field was 845 | * on. For example, an id on some type is a good candidate for being non-null 846 | * because if you have the item, you will already have the id so getting the 847 | * id will never fail but fetching a related item from a database would be 848 | * fallible so even if it will never be null in the success case, you should 849 | * make it nullable. 850 | * 851 | * ```ts 852 | * g.field({ 853 | * type: g.nonNull(g.String), 854 | * resolve(source, args) { 855 | * return "something"; 856 | * }, 857 | * }); 858 | * // == 859 | * graphql` 860 | * fieldName: String! 861 | * `; 862 | * ``` 863 | * 864 | * If you try to wrap another non-null type in a non-null type again, you will 865 | * get a type error. 866 | * 867 | * ```ts 868 | * // Argument of type 'NonNullType>' 869 | * // is not assignable to parameter of type 'NullableType'. 870 | * g.nonNull(g.nonNull(g.String)); 871 | * ``` 872 | */ 873 | nonNull: >(of: Of) => GNonNull; 874 | /** 875 | * Creates a {@link GScalarType scalar type}. 876 | * 877 | * ```ts 878 | * const BigInt = g.scalar({ 879 | * name: "BigInt", 880 | * serialize(value) { 881 | * if (typeof value !== "bigint") 882 | * throw new GraphQLError( 883 | * `unexpected value provided to BigInt scalar: ${value}` 884 | * ); 885 | * return value.toString(); 886 | * }, 887 | * parseLiteral(value) { 888 | * if (value.kind !== "StringValue") 889 | * throw new GraphQLError("BigInt only accepts values as strings"); 890 | * return globalThis.BigInt(value.value); 891 | * }, 892 | * parseValue(value) { 893 | * if (typeof value === "bigint") return value; 894 | * if (typeof value !== "string") 895 | * throw new GraphQLError("BigInt only accepts values as strings"); 896 | * return globalThis.BigInt(value); 897 | * }, 898 | * }); 899 | * // for fields on output types 900 | * g.field({ type: someScalar }); 901 | * 902 | * // for args on output fields or fields on input types 903 | * g.arg({ type: someScalar }); 904 | * ``` 905 | * 906 | * Note, while graphql-js allows you to express scalar types like the `ID` 907 | * type which accepts integers and strings as both input values and return 908 | * values from resolvers which are transformed into strings before calling 909 | * resolvers and returning the query respectively, the type you use should be 910 | * `string` for `ID` since that is what it is transformed into. 911 | * `@graphql-ts/schema` doesn't currently express the coercion of scalars, you 912 | * should instead convert values to the canonical form yourself before 913 | * returning from resolvers. 914 | */ 915 | scalar: ( 916 | config: GraphQLScalarTypeConfig 917 | ) => GScalarType; 918 | ID: GScalarType; 919 | String: GScalarType; 920 | Float: GScalarType; 921 | Int: GScalarType; 922 | Boolean: GScalarType; 923 | }; 924 | 925 | /** 926 | * The `gWithContext` function accepts a `Context` type parameter which binds 927 | * the returned functions so they can be used to compose GraphQL types into a 928 | * GraphQL schema. 929 | * 930 | * A simple schema with only a query type looks like this: 931 | * 932 | * ```ts 933 | * import { gWithContext } from "@graphql-ts/schema"; 934 | * import { GraphQLSchema, graphql } from "graphql"; 935 | * 936 | * type Context = {}; 937 | * 938 | * const g = gWithContext(); 939 | * type g = gWithContext.infer; 940 | * 941 | * const Query = g.object()({ 942 | * name: "Query", 943 | * fields: { 944 | * hello: g.field({ 945 | * type: g.String, 946 | * resolve() { 947 | * return "Hello!"; 948 | * }, 949 | * }), 950 | * }, 951 | * }); 952 | * 953 | * const schema = new GraphQLSchema({ 954 | * query: Query, 955 | * }); 956 | * 957 | * graphql({ 958 | * source: ` 959 | * query { 960 | * hello 961 | * } 962 | * `, 963 | * schema, 964 | * }).then((result) => { 965 | * console.log(result); 966 | * }); 967 | * ``` 968 | * 969 | * You can use pass the `schema` to `ApolloServer` and other GraphQL servers. 970 | * 971 | * You can also create a more advanced schema with other object types, circular 972 | * types, args, and mutations. See {@link GWithContext} for what the other 973 | * functions on `g` do. 974 | * 975 | * ```ts 976 | * import { gWithContext } from "@graphql-ts/schema"; 977 | * import { GraphQLSchema, graphql } from "graphql"; 978 | * import { deepEqual } from "node:assert"; 979 | * 980 | * type Context = { 981 | * todos: Map; 982 | * }; 983 | * 984 | * const g = gWithContext(); 985 | * type g = gWithContext.infer; 986 | * 987 | * type TodoItem = { 988 | * id: string; 989 | * title: string; 990 | * relatedTodos: string[]; 991 | * }; 992 | * 993 | * const Todo: g> = g.object()({ 994 | * name: "Todo", 995 | * fields: () => ({ 996 | * id: g.field({ type: g.nonNull(g.ID) }), 997 | * title: g.field({ type: g.nonNull(g.String) }), 998 | * relatedTodos: g.field({ 999 | * type: g.list(Todo), 1000 | * resolve(source, _args, context) { 1001 | * return source.relatedTodos 1002 | * .map((id) => context.todos.get(id)) 1003 | * .filter((todo) => todo !== undefined); 1004 | * }, 1005 | * }), 1006 | * }), 1007 | * }); 1008 | * 1009 | * const Query = g.object()({ 1010 | * name: "Query", 1011 | * fields: { 1012 | * todos: g.field({ 1013 | * type: g.list(Todo), 1014 | * resolve(_source, _args, context) { 1015 | * return context.todos.values(); 1016 | * }, 1017 | * }), 1018 | * }, 1019 | * }); 1020 | * 1021 | * const Mutation = g.object()({ 1022 | * name: "Mutation", 1023 | * fields: { 1024 | * createTodo: g.field({ 1025 | * args: { 1026 | * title: g.arg({ type: g.nonNull(g.String) }), 1027 | * relatedTodos: g.arg({ 1028 | * type: g.nonNull(g.list(g.nonNull(g.ID))), 1029 | * defaultValue: [], 1030 | * }), 1031 | * }, 1032 | * type: Todo, 1033 | * resolve(_source, { title, relatedTodos }, context) { 1034 | * const todo = { title, relatedTodos, id: crypto.randomUUID() }; 1035 | * context.todos.set(todo.id, todo); 1036 | * return todo; 1037 | * }, 1038 | * }), 1039 | * }, 1040 | * }); 1041 | * 1042 | * const schema = new GraphQLSchema({ 1043 | * query: Query, 1044 | * mutation: Mutation, 1045 | * }); 1046 | * 1047 | * (async () => { 1048 | * const contextValue: Context = { todos: new Map() }; 1049 | * { 1050 | * const result = await graphql({ 1051 | * source: ` 1052 | * query { 1053 | * todos { 1054 | * title 1055 | * } 1056 | * } 1057 | * `, 1058 | * schema, 1059 | * contextValue, 1060 | * }); 1061 | * deepEqual(result, { data: { todos: [] } }); 1062 | * } 1063 | * 1064 | * { 1065 | * const result = await graphql({ 1066 | * source: ` 1067 | * mutation { 1068 | * createTodo(title: "Try graphql-ts") { 1069 | * title 1070 | * } 1071 | * } 1072 | * `, 1073 | * schema, 1074 | * contextValue, 1075 | * }); 1076 | * deepEqual(result, { 1077 | * data: { createTodo: { title: "Try graphql-ts" } }, 1078 | * }); 1079 | * } 1080 | * { 1081 | * const result = await graphql({ 1082 | * source: ` 1083 | * query { 1084 | * todos { 1085 | * title 1086 | * } 1087 | * } 1088 | * `, 1089 | * schema, 1090 | * contextValue, 1091 | * }); 1092 | * deepEqual(result, { 1093 | * data: { todos: [{ title: "Try graphql-ts" }] }, 1094 | * }); 1095 | * } 1096 | * })(); 1097 | * ``` 1098 | */ 1099 | export function gWithContext(): GWithContext { 1100 | return { 1101 | scalar(config) { 1102 | return new GScalarType(config); 1103 | }, 1104 | list(of) { 1105 | return new GList(of); 1106 | }, 1107 | nonNull(of) { 1108 | return new GNonNull(of); 1109 | }, 1110 | inputObject(config) { 1111 | return new GInputObjectType(config); 1112 | }, 1113 | enum(config) { 1114 | return new GEnumType(config); 1115 | }, 1116 | union(config) { 1117 | return new GUnionType(config as any); 1118 | }, 1119 | object() { 1120 | return function objectInner(config) { 1121 | return new GObjectType(config); 1122 | }; 1123 | }, 1124 | interface() { 1125 | return function interfaceInner(config) { 1126 | return new GInterfaceType(config); 1127 | }; 1128 | }, 1129 | fields() { 1130 | return function fieldsInner(fields) { 1131 | return fields; 1132 | }; 1133 | }, 1134 | field(field) { 1135 | if (!field.type) { 1136 | throw new Error("A type must be passed to g.field()"); 1137 | } 1138 | return field as any; 1139 | }, 1140 | interfaceField(field) { 1141 | if (!field.type) { 1142 | throw new Error("A type must be passed to g.interfaceField()"); 1143 | } 1144 | return field; 1145 | }, 1146 | arg(arg) { 1147 | if (!arg.type) { 1148 | throw new Error("A type must be passed to g.arg()"); 1149 | } 1150 | return arg as any; 1151 | }, 1152 | enumValues(values) { 1153 | return Object.fromEntries( 1154 | values.map((value) => [value, { value }]) 1155 | ) as any; 1156 | }, 1157 | Int: GraphQLInt, 1158 | Float: GraphQLFloat, 1159 | String: GraphQLString, 1160 | Boolean: GraphQLBoolean, 1161 | ID: GraphQLID, 1162 | }; 1163 | } 1164 | 1165 | // eslint-disable-next-line @typescript-eslint/no-namespace 1166 | export declare namespace gWithContext { 1167 | /** 1168 | * The `gWithContext.infer` type is useful particularly when defining circular 1169 | * types to resolve errors from TypeScript because of the circularity. 1170 | * 1171 | * We recommend aliasing `gWithContext.infer` to your `g` like this to make it 1172 | * easier to use: 1173 | * 1174 | * ```ts 1175 | * import { gWithContext } from "@graphql-ts/schema"; 1176 | * type Context = {}; 1177 | * 1178 | * const g = gWithContext(); 1179 | * type g = gWithContext.infer; 1180 | * 1181 | * type PersonSource = { name: string; friends: PersonSource[] }; 1182 | * 1183 | * const Person: g> = 1184 | * g.object()({ 1185 | * name: "Person", 1186 | * fields: () => ({ 1187 | * name: g.field({ type: g.String }), 1188 | * friends: g.field({ type: g.list(Person) }), 1189 | * }), 1190 | * }); 1191 | * ``` 1192 | */ 1193 | export type infer = T extends () => (args: any) => infer R 1194 | ? R 1195 | : T extends (args: any) => infer R 1196 | ? R 1197 | : never; 1198 | } 1199 | 1200 | type Flatten = { 1201 | [Key in keyof T]: T[Key]; 1202 | } & {}; 1203 | -------------------------------------------------------------------------------- /packages/schema/src/types.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module exports modified versions of the GraphQL types from the `graphql` 3 | * package that add more type-safety but are still at runtime exactly the same 4 | * as the original types. Some of the constructors 5 | * 6 | * @module 7 | */ 8 | import { 9 | GraphQLArgumentExtensions, 10 | GraphQLEnumType, 11 | type GraphQLEnumTypeConfig, 12 | type GraphQLEnumValueConfig, 13 | GraphQLFieldExtensions, 14 | GraphQLInputField, 15 | GraphQLInputFieldExtensions, 16 | GraphQLInputObjectType, 17 | type GraphQLInputObjectTypeConfig, 18 | GraphQLInterfaceType, 19 | GraphQLInterfaceTypeConfig, 20 | GraphQLList, 21 | GraphQLNonNull, 22 | GraphQLObjectType, 23 | GraphQLObjectTypeConfig, 24 | GraphQLResolveInfo, 25 | GraphQLScalarType, 26 | type GraphQLTypeResolver, 27 | GraphQLUnionType, 28 | type GraphQLUnionTypeConfig, 29 | type FieldDefinitionNode, 30 | type InputValueDefinitionNode, 31 | } from "graphql"; 32 | import type { g } from "./g-for-doc-references"; 33 | 34 | type Maybe = T | null | undefined; 35 | 36 | export type GNullableOutputType = 37 | | GScalarType 38 | | GObjectType 39 | | GInterfaceType 40 | | GUnionType 41 | | GEnumType> 42 | | GList>; 43 | 44 | export type GOutputType = 45 | | GNullableOutputType 46 | | GNonNull>; 47 | 48 | export type GNullableInputType = 49 | | GScalarType 50 | | GEnumType> 51 | | GInputObjectType 52 | | GList; 53 | 54 | export type GInputType = GNullableInputType | GNonNull; 55 | 56 | export type GNullableType = 57 | | GNullableOutputType 58 | | GNullableInputType; 59 | 60 | export type GType = GOutputType | GInputType; 61 | 62 | export type GFieldResolver< 63 | Source, 64 | Args extends Record>, 65 | Type extends GOutputType, 66 | Context, 67 | > = ( 68 | source: Source, 69 | args: InferValueFromArgs, 70 | context: Context, 71 | info: GraphQLResolveInfo 72 | ) => InferValueFromOutputType; 73 | 74 | type InferValueFromOutputTypeWithoutAddingNull> = 75 | Type extends GraphQLScalarType 76 | ? Value 77 | : Type extends GraphQLEnumType 78 | ? Type extends GEnumType 79 | ? Values[keyof Values] 80 | : never 81 | : Type extends GList> 82 | ? // the `object` bit is here because graphql checks `typeof maybeIterable === 'object'` 83 | // which means that things like `string` won't be allowed 84 | // (which is probably a good thing because returning a string from a resolver that needs 85 | // a graphql list of strings is almost definitely not what you want and if it is, use Array.from) 86 | // sadly functions that are iterables will be allowed by this type but not allowed by graphql-js 87 | // (though tbh, i think the chance of that causing problems is quite low) 88 | object & Iterable> 89 | : Type extends GraphQLObjectType 90 | ? Source 91 | : Type extends GraphQLUnionType | GraphQLInterfaceType 92 | ? Type extends 93 | | GUnionType 94 | | GInterfaceType 95 | ? Source 96 | : unknown 97 | : never; 98 | 99 | export type InferValueFromOutputType> = 100 | MaybePromise< 101 | Type extends GNonNull> 102 | ? InferValueFromOutputTypeWithoutAddingNull 103 | : InferValueFromOutputTypeWithoutAddingNull | null | undefined 104 | >; 105 | 106 | type MaybePromise = Promise | T; 107 | 108 | type InferValueFromNullableInputType = 109 | Type extends GraphQLScalarType 110 | ? Value 111 | : Type extends GraphQLEnumType 112 | ? Type extends GEnumType 113 | ? Values[keyof Values] 114 | : unknown 115 | : Type extends GList 116 | ? InferValueFromInputType[] 117 | : Type extends GraphQLInputObjectType 118 | ? Type extends GInputObjectType 119 | ? IsOneOf extends true 120 | ? InferValueForOneOf 121 | : InferValueFromArgs 122 | : Record 123 | : never; 124 | 125 | type InferValueForOneOf< 126 | T extends { [key: string]: { type: GInputType } }, 127 | Key extends keyof T = keyof T, 128 | > = Flatten< 129 | Key extends unknown 130 | ? { 131 | readonly [K in Key]: InferValueFromNullableInputType; 132 | } & { 133 | readonly [K in Exclude]?: never; 134 | } 135 | : never 136 | >; 137 | 138 | export type InferValueFromArgs>> = 139 | { 140 | readonly [Key in keyof Args]: InferValueFromArg; 141 | } & {}; 142 | 143 | export type InferValueFromArg> = 144 | // the distribution technically only needs to be around the AddUndefined 145 | // but having it here instead of inside the union 146 | // means that TypeScript will print the resulting type 147 | // when you use it rather than keep the alias and 148 | // the resulting type is generally far more readable 149 | Arg extends unknown 150 | ? 151 | | InferValueFromInputType 152 | | AddUndefined 153 | : never; 154 | 155 | type AddUndefined = 156 | TInputType extends GNonNull ? never : DefaultValue & undefined; 157 | 158 | export type InferValueFromInputType = 159 | Type extends GNonNull 160 | ? InferValueFromNullableInputType 161 | : InferValueFromNullableInputType | null; 162 | 163 | /** 164 | * A GraphQL output field for an {@link GObjectType object type} which should be 165 | * created using {@link g.field}. 166 | */ 167 | export type GField< 168 | Source, 169 | Args extends { [Key in keyof Args]: GArg }, 170 | Type extends GOutputType, 171 | SourceAtKey, 172 | Context, 173 | > = { 174 | args?: Args; 175 | type: Type; 176 | resolve?: GFieldResolver; 177 | description?: Maybe; 178 | deprecationReason?: Maybe; 179 | extensions?: Maybe>>; 180 | astNode?: Maybe; 181 | __missingResolve: undefined | ((arg: SourceAtKey) => void); 182 | }; 183 | 184 | /** 185 | * A GraphQL object type. This should generally be constructed with 186 | * {@link g.object}. 187 | * 188 | * Note this is an **output** type, if you want an input object, use 189 | * {@link GInputObjectType}. 190 | * 191 | * If you use the `GObjectType` constructor directly, all fields will need 192 | * explicit resolvers so you should use `g.object` instead. 193 | */ 194 | export class GObjectType extends GraphQLObjectType< 195 | Source, 196 | Context 197 | > { 198 | constructor( 199 | config: Readonly< 200 | GObjectTypeConfig< 201 | Source, 202 | Context, 203 | Record>, 204 | readonly GInterfaceType[] 205 | > 206 | > 207 | ); 208 | } 209 | 210 | export type GObjectTypeConfig< 211 | Source, 212 | Context, 213 | Fields extends Record>, 214 | Interfaces extends readonly GInterfaceType[], 215 | > = { 216 | fields: Fields | (() => Fields); 217 | interfaces?: [...Interfaces]; 218 | } & Omit, "fields" | "interfaces">; 219 | 220 | /** 221 | * A GraphQL union type. This should generally be constructed with 222 | * {@link g.union}. 223 | * 224 | * A union type represents an object that could be one of a list of types. Note 225 | * it is similar to an {@link GInterfaceType} except that a union doesn't imply 226 | * having a common set of fields among the member types. 227 | * 228 | * While this constructor will work, you should generally use `g.union` because 229 | * you will need to explicitly provide the source type parameter as TypeScript 230 | * is unable to infer it correctly. Note this is only required for this 231 | * constructor, this is not required when using `g.union`. 232 | */ 233 | export class GUnionType extends GraphQLUnionType { 234 | constructor( 235 | config: Readonly< 236 | GUnionTypeConfig< 237 | Source extends any ? GObjectType : never, 238 | Context 239 | > 240 | > 241 | ); 242 | resolveType: Maybe>; 243 | } 244 | 245 | export type GUnionTypeConfig< 246 | ObjectType extends GObjectType, 247 | Context, 248 | > = Flatten< 249 | { 250 | types: readonly ObjectType[] | (() => readonly ObjectType[]); 251 | } & Omit< 252 | GraphQLUnionTypeConfig< 253 | ObjectType extends GObjectType ? Source : never, 254 | Context 255 | >, 256 | "types" 257 | > 258 | >; 259 | 260 | export type GInterfaceField< 261 | Args extends Record>, 262 | Type extends GOutputType, 263 | Context, 264 | > = { 265 | description?: Maybe; 266 | type: Type; 267 | args?: Args; 268 | deprecationReason?: Maybe; 269 | extensions?: Maybe>>; 270 | astNode?: Maybe; 271 | }; 272 | 273 | /** 274 | * A GraphQL interface type that can be implemented by other 275 | * {@link GObjectType GraphQL object} and interface types. This should generally 276 | * be constructed with {@link g.interface}. 277 | * 278 | * If you use the `GInterfaceType` constructor directly, all fields will need 279 | * explicit resolvers so you should use `g.interface` instead. 280 | */ 281 | export class GInterfaceType< 282 | Source, 283 | Fields extends Record< 284 | string, 285 | GInterfaceField, Context> 286 | >, 287 | Context, 288 | > extends GraphQLInterfaceType { 289 | declare resolveType: Maybe>; 290 | constructor( 291 | config: Readonly< 292 | GInterfaceTypeConfig< 293 | Source, 294 | Fields, 295 | readonly GInterfaceType[], 296 | Context 297 | > 298 | > 299 | ); 300 | toConfig(): Omit, "fields"> & { 301 | fields: Fields; 302 | }; 303 | } 304 | 305 | export type GInterfaceTypeConfig< 306 | Source, 307 | Fields extends Record< 308 | string, 309 | GInterfaceField, Context> 310 | >, 311 | Interfaces extends readonly GInterfaceType[], 312 | Context, 313 | > = Flatten< 314 | { 315 | fields: Fields | (() => Fields); 316 | interfaces?: [...Interfaces]; 317 | } & Omit, "interfaces" | "fields"> 318 | >; 319 | 320 | /** 321 | * A GraphQL argument. These should be created with {@link g.arg} 322 | * 323 | * Args can can be used as arguments on output fields: 324 | * 325 | * ```ts 326 | * g.field({ 327 | * type: g.String, 328 | * args: { 329 | * something: g.arg({ type: g.String }), 330 | * }, 331 | * resolve(source, { something }) { 332 | * return something || somethingElse; 333 | * }, 334 | * }); 335 | * // == 336 | * graphql`fieldName(something: String): String`; 337 | * ``` 338 | * 339 | * Or as fields on input objects: 340 | * 341 | * ```ts 342 | * g.inputObject({ 343 | * name: "Something", 344 | * fields: { 345 | * something: g.arg({ type: g.String }), 346 | * }, 347 | * }); 348 | * // == 349 | * graphql` 350 | * input Something { 351 | * something: String 352 | * } 353 | * `; 354 | * ``` 355 | * 356 | * When the type of an arg is {@link GNonNull non-null}, the value will always 357 | * exist. 358 | * 359 | * ```ts 360 | * g.field({ 361 | * type: g.String, 362 | * args: { 363 | * something: g.arg({ type: g.nonNull(g.String) }), 364 | * }, 365 | * resolve(source, { something }) { 366 | * // `something` will always be a string 367 | * return something; 368 | * }, 369 | * }); 370 | * // == 371 | * graphql`fieldName(something: String!): String`; 372 | * ``` 373 | */ 374 | export type GArg< 375 | Type extends GInputType, 376 | HasDefaultValue extends boolean = boolean, 377 | > = { 378 | type: Type; 379 | defaultValue: { 380 | true: {} | null; 381 | false: undefined; 382 | }[`${HasDefaultValue}`]; 383 | description?: Maybe; 384 | deprecationReason?: Maybe; 385 | extensions?: Maybe; 386 | astNode?: Maybe; 387 | }; 388 | 389 | export type GInputObjectTypeConfig< 390 | Fields extends { 391 | [key: string]: IsOneOf extends true 392 | ? GArg 393 | : GArg; 394 | }, 395 | IsOneOf extends boolean = false, 396 | > = Flatten< 397 | Omit & { 398 | fields: Fields | (() => Fields); 399 | isOneOf?: IsOneOf; 400 | } 401 | > & 402 | (true extends IsOneOf ? { isOneOf: unknown } : unknown); 403 | 404 | /** 405 | * A GraphQL input object type. This should generally be constructed with 406 | * {@link g.inputObject}. 407 | * 408 | * Unlike some other constructors in this module, this constructor functions 409 | * exactly the same as it's counterpart `g.inputObject` so it is safe to use 410 | * directly if desired. 411 | */ 412 | export class GInputObjectType< 413 | Fields extends { 414 | [key: string]: IsOneOf extends true 415 | ? GArg 416 | : GArg; 417 | }, 418 | IsOneOf extends boolean = false, 419 | > extends GraphQLInputObjectType { 420 | isOneOf: IsOneOf; 421 | constructor(config: Readonly>); 422 | getFields(): { 423 | [K in keyof Fields]: GraphQLInputField & { 424 | type: Fields[K]["type"]; 425 | defaultValue: Fields[K]["defaultValue"]; 426 | }; 427 | }; 428 | } 429 | 430 | export type GEnumValueConfig = GraphQLEnumValueConfig & { 431 | value: Value; 432 | }; 433 | 434 | export type GEnumTypeConfig = 435 | Flatten< 436 | { 437 | values: { 438 | [Name in keyof Values]: GEnumValueConfig; 439 | }; 440 | } & Omit 441 | >; 442 | 443 | /** 444 | * A GraphQL enum type. This should generally be constructed with {@link g.enum}. 445 | * 446 | * Unlike some other constructors in this module, this constructor functions 447 | * exactly the same as it's counterpart `g.enum` so it is safe to use directly 448 | * if desired. 449 | */ 450 | export class GEnumType< 451 | const Values extends { [key: string]: unknown }, 452 | > extends GraphQLEnumType { 453 | constructor(config: Readonly>); 454 | toConfig(): Omit, "values"> & { 455 | values: { 456 | [Name in keyof Values]: Partial>; 457 | }; 458 | }; 459 | } 460 | 461 | /** 462 | * A GraphQL enum type. This should generally be constructed with 463 | * {@link g.scalar}. 464 | * 465 | * Unlike some other constructors in this module, this constructor functions 466 | * exactly the same as it's counterpart `g.scalar` so it is safe to use directly 467 | * if desired. 468 | * 469 | * Also unlike some other types in this module, this type is exactly equivalent 470 | * to the original {@link GraphQLScalarType `GraphQLScalarType`} type from the 471 | * `graphql` package. 472 | */ 473 | export class GScalarType< 474 | Internal = unknown, 475 | External = Internal, 476 | > extends GraphQLScalarType {} 477 | 478 | type Flatten = { 479 | [K in keyof T]: T[K]; 480 | } & {}; 481 | 482 | /** 483 | * A GraphQL non-null type. This should generally be constructed with 484 | * {@link g.nonNull}. 485 | * 486 | * Unlike some other constructors in this module, this constructor functions 487 | * exactly the same as it's counterpart `g.nonNull` so it is safe to use 488 | * directly if desired. 489 | * 490 | * Also unlike the named types in this module, the original 491 | * {@link GraphQLNonNull `GraphQLNonNull`} type from the `graphql` package cannot 492 | * be assigned to a variable of type `GNonNull`. Though `GNonNull` _is_ 493 | * assignable to `GraphQLNonNull`. 494 | * 495 | * For example, the following code will not compile: 496 | * 497 | * ```ts 498 | * const nonNull: GNonNull> = new GraphQLNonNull( 499 | * GraphQLString 500 | * ); 501 | * ``` 502 | * 503 | * But the following code will compile: 504 | * 505 | * ```ts 506 | * const nonNull: GraphQLNonNull> = new GNonNull( 507 | * GraphQLString 508 | * ); 509 | * ``` 510 | * 511 | * This is due to the lack of a discriminating property between the 512 | * `GraphQLNonNull` and `GraphQLList` types. 513 | */ 514 | export class GNonNull< 515 | Of extends GNullableType, 516 | > extends GraphQLNonNull { 517 | get [Symbol.toStringTag](): "GraphQLNonNull"; 518 | } 519 | 520 | /** 521 | * A GraphQL list type. This should generally be constructed with {@link g.list}. 522 | * 523 | * Unlike some other constructors in this module, this constructor functions 524 | * exactly the same as it's counterpart `g.list` so it is safe to use directly 525 | * if desired. 526 | * 527 | * Also unlike the named types in this module, the original 528 | * {@link GraphQLList `GraphQLList`} type from the `graphql` package cannot be 529 | * assigned to a variable of type `GList`. Though `GList` _is_ assignable to 530 | * `GraphQLList`. 531 | * 532 | * For example, the following code will not compile: 533 | * 534 | * ```ts 535 | * const list: GList> = new GraphQLList(GraphQLString); 536 | * ``` 537 | * 538 | * But the following code will compile: 539 | * 540 | * ```ts 541 | * const list: GraphQLList> = new GList( 542 | * GraphQLString 543 | * ); 544 | * ``` 545 | * 546 | * This is due to the lack of a discriminating property between the 547 | * `GraphQLNonNull` and `GraphQLList` types. 548 | */ 549 | export class GList> extends GraphQLList { 550 | get [Symbol.toStringTag](): "GraphQLList"; 551 | } 552 | 553 | export {}; 554 | -------------------------------------------------------------------------------- /packages/schema/src/types.js: -------------------------------------------------------------------------------- 1 | export { 2 | GraphQLEnumType as GEnumType, 3 | GraphQLInputObjectType as GInputObjectType, 4 | GraphQLInterfaceType as GInterfaceType, 5 | GraphQLObjectType as GObjectType, 6 | GraphQLScalarType as GScalarType, 7 | GraphQLUnionType as GUnionType, 8 | GraphQLList as GList, 9 | GraphQLNonNull as GNonNull, 10 | } from "graphql"; 11 | -------------------------------------------------------------------------------- /patches/@astrojs__starlight-tailwind.patch: -------------------------------------------------------------------------------- 1 | diff --git a/index.ts b/index.ts 2 | index 8253e70a9d7aba3f76d198d52a0a0e91be592a6d..1b9f6a4e7958bf19589205e4e3096091a8dd6bd8 100644 3 | --- a/index.ts 4 | +++ b/index.ts 5 | @@ -33,6 +33,7 @@ import plugin from 'tailwindcss/plugin'; 6 | const StarlightTailwindPlugin = () => 7 | plugin( 8 | ({ addBase, theme, config }) => { 9 | + // @ts-ignore 10 | if (config('prefix') === 'sl-') { 11 | console.warn( 12 | 'A Tailwind prefix of "sl-" will clash with Starlight’s built-in styles.\n' + 13 | @@ -110,6 +111,7 @@ const StarlightTailwindPlugin = () => 14 | { 15 | // Starlight uses a `data-theme` attribute to power its dark mode. 16 | darkMode: ['class', '[data-theme="dark"]'], 17 | + // @ts-ignore 18 | corePlugins: { 19 | // Disable Tailwind’s default reset styles which conflict with Starlight. 20 | preflight: false, 21 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "test-project" 4 | - "site" 5 | - "examples/*" 6 | -------------------------------------------------------------------------------- /site/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import starlight from "@astrojs/starlight"; 3 | import esbuild from "esbuild"; 4 | import * as graphql from "graphql"; 5 | import graphqlTsSchemaDefault, * as _graphqlTsSchema from "@graphql-ts/schema"; 6 | import tailwindcss from "@tailwindcss/vite"; 7 | import "mdast-util-mdx-jsx"; 8 | 9 | const graphqlTsSchema = graphqlTsSchemaDefault || _graphqlTsSchema; 10 | 11 | // https://astro.build/config 12 | export default defineConfig({ 13 | integrations: [ 14 | starlight({ 15 | title: "graphql-ts", 16 | social: { github: "https://github.com/Thinkmill/graphql-ts" }, 17 | customCss: ["./src/index.css"], 18 | components: { 19 | Hero: "./src/Hero.astro", 20 | }, 21 | sidebar: [ 22 | { 23 | label: "Get Started", 24 | items: [{ slug: "installation", label: "Installation" }], 25 | }, 26 | { 27 | label: "Types", 28 | autogenerate: { directory: "types" }, 29 | }, 30 | { 31 | label: "Examples", 32 | autogenerate: { directory: "examples" }, 33 | }, 34 | { 35 | label: "Extra", 36 | items: [ 37 | // { label: "Design", link: "/design" }, 38 | { label: "Schema Extension", link: "/extend" }, 39 | ], 40 | }, 41 | { 42 | label: "API Reference", 43 | items: [ 44 | { 45 | label: "@graphql-ts/schema", 46 | link: "https://docsmill.dev/npm/@graphql-ts/schema", 47 | }, 48 | { 49 | label: "@graphql-ts/extend", 50 | link: "https://docsmill.dev/npm/@graphql-ts/extend", 51 | }, 52 | ], 53 | }, 54 | ], 55 | }), 56 | ], 57 | 58 | markdown: { 59 | remarkPlugins: [ 60 | () => (tree, file) => { 61 | /** @param {import("mdast").Nodes} node */ 62 | function visitNode(node) { 63 | if (node.type === "mdxJsxFlowElement" && node.name === "ShowSchema") { 64 | const code = node.children.find( 65 | (child) => child.type === "code" 66 | )?.value; 67 | if (!code) { 68 | throw new Error("Expected code block inside "); 69 | } 70 | let compiledCode; 71 | try { 72 | compiledCode = esbuild.transformSync( 73 | "\n".repeat(node.position?.start.line ?? 0) + code, 74 | { format: "cjs", loader: "ts", sourcefile: file.path } 75 | ).code; 76 | } catch (e) { 77 | console.error(e); 78 | throw new Error(`Failed to compile schema:\n${code}`, { 79 | cause: e, 80 | }); 81 | } 82 | const requireForCode = (/** @type {string} */ mod) => { 83 | if (mod === "graphql") { 84 | return graphql; 85 | } 86 | if (mod === "@graphql-ts/schema") { 87 | return graphqlTsSchema; 88 | } 89 | throw new Error(`Unexpected require('${mod}')`); 90 | }; 91 | const func = new Function( 92 | "require", 93 | "module", 94 | compiledCode + "\n;return schema" 95 | ); 96 | let schema; 97 | try { 98 | schema = func(requireForCode, { exports: {} }); 99 | } catch (e) { 100 | console.error(e); 101 | throw new Error( 102 | `Failed to create schema from compiled code:\n${compiledCode}`, 103 | { cause: e } 104 | ); 105 | } 106 | node.attributes.push({ 107 | type: "mdxJsxAttribute", 108 | name: "schema", 109 | value: graphql.printSchema(schema), 110 | }); 111 | } 112 | 113 | if ("children" in node && node.children) { 114 | node.children.forEach(visitNode); 115 | } 116 | } 117 | tree.children.forEach(visitNode); 118 | }, 119 | ], 120 | }, 121 | 122 | vite: { 123 | plugins: [tailwindcss()], 124 | }, 125 | }); 126 | -------------------------------------------------------------------------------- /site/ec.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineEcConfig } from "@astrojs/starlight/expressive-code"; 2 | import ecTwoSlash from "expressive-code-twoslash"; 3 | 4 | export default defineEcConfig({ 5 | themes: ["github-dark", "github-light"], 6 | plugins: [ 7 | ecTwoSlash({ 8 | // including the jsdoc makes the table of contents include the headings from the table of contents 9 | includeJsDoc: false, 10 | twoslashOptions: { 11 | compilerOptions: { 12 | lib: undefined, 13 | }, 14 | }, 15 | }), 16 | { 17 | name: "remove-trailing-empty-line", 18 | hooks: { 19 | preprocessCode(context) { 20 | const lines = context.codeBlock.getLines(); 21 | if (lines[lines.length - 1]?.text === "") { 22 | context.codeBlock.deleteLine(lines.length - 1); 23 | } 24 | }, 25 | }, 26 | }, 27 | ], 28 | }); 29 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphql-ts/site", 3 | "private": true, 4 | "repository": "https://github.com/Thinkmill/graphql-ts/tree/main/site", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "build": "astro build", 8 | "preview": "astro preview", 9 | "types": "astro sync && tsc" 10 | }, 11 | "dependencies": { 12 | "@astrojs/mdx": "^4.2.0", 13 | "@astrojs/starlight": "^0.32.2", 14 | "@astrojs/starlight-tailwind": "^3.0.0", 15 | "@graphql-ts/extend": "workspace:^", 16 | "@graphql-ts/schema": "workspace:^", 17 | "@shikijs/twoslash": "^3.2.1", 18 | "@tailwindcss/vite": "^4.0.14", 19 | "@types/hast": "^3.0.4", 20 | "@types/mdast": "^4.0.4", 21 | "astro": "^5.5.2", 22 | "esbuild": "^0.25.1", 23 | "expressive-code-twoslash": "^0.4.0", 24 | "graphql": "^16.3.0", 25 | "mdast-util-mdx-jsx": "^3.2.0", 26 | "remark-shiki-twoslash": "^3.1.3", 27 | "tailwindcss": "^4.0.14" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /site/src/Hero.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import InnerHero from '@astrojs/starlight/components/Hero.astro' 3 | 4 | --- 5 | 6 | {Astro.locals.starlightRoute.id !== '' && } 7 | -------------------------------------------------------------------------------- /site/src/content.config.mts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from "astro:content"; 2 | import { docsLoader } from "@astrojs/starlight/loaders"; 3 | import { docsSchema } from "@astrojs/starlight/schema"; 4 | 5 | export const collections = { 6 | docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), 7 | }; 8 | -------------------------------------------------------------------------------- /site/src/content/docs/examples/Example.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import './styles.css' 3 | 4 | type Props = { 5 | example: string 6 | } 7 | 8 | --- 9 | 10 | 14 | -------------------------------------------------------------------------------- /site/src/content/docs/examples/graphql-yoga.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: GraphQL Yoga 3 | tableOfContents: false 4 | --- 5 | 6 | import Example from "./Example.astro"; 7 | 8 | 9 | -------------------------------------------------------------------------------- /site/src/content/docs/examples/styles.css: -------------------------------------------------------------------------------- 1 | main .content-panel:nth-child(2) .sl-container { 2 | max-width: initial; 3 | } 4 | -------------------------------------------------------------------------------- /site/src/content/docs/extend.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Extending an existing schema 3 | --- 4 | 5 | import { TabItem, Tabs } from "@astrojs/starlight/components"; 6 | 7 | graphql-ts provides a small library to extend an existing GraphQL schema with new query/mutation fields and types. 8 | 9 | :::note 10 | 11 | If you're creating your schema entirely with `@graphql-ts/schema`, you **shouldn't use this package**. 12 | 13 | ::: 14 | 15 | ## Installation 16 | 17 | 18 | 19 | 20 | ```sh 21 | npm install @graphql-ts/extend 22 | ``` 23 | 24 | 25 | 26 | 27 | ```sh 28 | pnpm add @graphql-ts/extend 29 | ``` 30 | 31 | 32 | 33 | 34 | ```sh 35 | yarn add @graphql-ts/extend 36 | ``` 37 | 38 | 39 | 40 | 41 | ## Usage 42 | 43 | ```ts twoslash "base" 44 | // @noErrors 45 | // @filename: g.ts 46 | import { gWithContext } from "@graphql-ts/schema"; 47 | 48 | export type Context = {}; 49 | 50 | export const g = gWithContext(); 51 | export type g = gWithContext.infer; 52 | // @filename: existing-schema.ts 53 | import { GraphQLSchema, GraphQLString, GraphQLObjectType } from "graphql"; 54 | export const schema = new GraphQLSchema({ 55 | query: new GraphQLObjectType({ 56 | name: "Query", 57 | fields: { 58 | hello: { 59 | type: GraphQLString, 60 | resolve: () => "world", 61 | }, 62 | }, 63 | }), 64 | }); 65 | // @filename: index.ts 66 | // ---cut--- 67 | import { extend } from "@graphql-ts/extend"; 68 | import { schema } from "./existing-schema"; 69 | import { g } from "./g"; 70 | 71 | const newSchema = extend((base) => { 72 | const existingType = base. 73 | // ^| 74 | return { 75 | query: { 76 | hello: g.field({ 77 | type: g.String, 78 | resolve: () => "world", 79 | }), 80 | users: g.field({ 81 | // `base` allows easily retrieving types from the existing schema 82 | type: g.list(base.object("User")), 83 | resolve() { 84 | return []; 85 | }, 86 | }), 87 | }, 88 | }; 89 | })(schema); 90 | ``` 91 | 92 | ## Limitations 93 | 94 | `extend` only supports extending the query and mutation types. `extend` also doesn't support schemas that use the query or mutation types in any types besides as the root query/mutation types. 95 | 96 | Also, since there is no TypeScript type information in the existing schema, all existing types that can be used are typed generically e.g. for object types, the source type is `unknown` and etc. 97 | -------------------------------------------------------------------------------- /site/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: graphql-ts 3 | template: splash 4 | editUrl: false 5 | lastUpdated: false 6 | hero: {} 7 | --- 8 | 9 | import { CardGrid, Card, LinkButton } from "@astrojs/starlight/components"; 10 | 11 |
12 |
13 |
14 |

15 | graphql-ts 16 |

17 |
18 | Simple Type-Safe GraphQL Schemas in TypeScript 19 |
20 |
21 |
22 | 23 | Get Started 24 | 25 | 30 | View on GitHub 31 | 32 |
33 |
34 | 35 | {/* prettier-ignore */} 36 | ```ts twoslash 37 | // @noErrors 38 | import { gWithContext } from "@graphql-ts/schema"; 39 | import { GraphQLSchema } from "graphql"; 40 | type Context = { 41 | loadUser: (id: string) => Promise; 42 | } 43 | const g = gWithContext(); 44 | type g = gWithContext.infer; 45 | type UserSource = { id: string; name: string }; 46 | const User = g.object()({ 47 | name: "User", 48 | fields: () => ({ 49 | id: g.field({ type: g.nonNull(g.ID) }), 50 | name: g.field({ type: g.nonNull(g.String) }), 51 | }), 52 | }); 53 | // ---cut--- 54 | const Query = g.object()({ 55 | name: "Query", 56 | fields: { 57 | user: g.field({ 58 | type: User, 59 | args: { 60 | id: g.arg({ type: g.nonNull(g.ID) }), 61 | }, 62 | resolve(_, args, context) { 63 | return context.loadUser(args.); 64 | // ^| 65 | }, 66 | }), 67 | }, 68 | }); 69 | // ---cut-after--- 70 | const schema = new GraphQLSchema({ query: Query }); 71 | ``` 72 | 73 |
74 | 75 | 76 | 77 | `@graphql-ts/schema` provides type safety when defining your GraphQL schema 78 | while avoiding the need for code generation, decorators, or other 79 | complexity. 80 | 81 | 82 | Use `@graphql-ts/schema` with any existing GraphQL server that uses 83 | GraphQL.js, like GraphQL Yoga, Apollo Server, graphql-http, etc. 84 | 85 | 86 | -------------------------------------------------------------------------------- /site/src/content/docs/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | --- 4 | 5 | import { TabItem, Tabs } from "@astrojs/starlight/components"; 6 | 7 | :::tip 8 | 9 | `@graphql-ts/schema` is for **constructing** a GraphQL schema. If you're looking for a tool for type-safe **querying**, check out [gql.tada](https://gql-tada.0no.co). 10 | 11 | ::: 12 | 13 | To get started, you'll need to install `@graphql-ts/schema` and `graphql`. 14 | 15 | 16 | 17 | 18 | ```sh 19 | npm install graphql @graphql-ts/schema 20 | ``` 21 | 22 | 23 | 24 | 25 | ```sh 26 | pnpm add graphql @graphql-ts/schema 27 | ``` 28 | 29 | 30 | 31 | 32 | ```sh 33 | yarn add graphql @graphql-ts/schema 34 | ``` 35 | 36 | 37 | 38 | 39 | Using `@graphql-ts/schema` starts with the `g` object which is a set of functions that mirror the GraphQL.js API, but with added type-safety. 40 | 41 | To create `g`, you need to call `gWithContext` with a context type. The context type defines the type of the context that resolvers receive. You'll generally want to export this from a module so you can use it in your schema definition. You'll also want to export a type alias for `g` like `type g = gWithContext.infer`, [you'll use this later](/types/object#circular-types). 42 | 43 | ```ts twoslash 44 | // g.ts 45 | import { gWithContext } from "@graphql-ts/schema"; 46 | 47 | export type Context = {}; 48 | 49 | export const g = gWithContext(); 50 | export type g = gWithContext.infer; 51 | ``` 52 | 53 | Now you can use `g` to define your schema, you'll probably start with a query type, like this: 54 | 55 | ```ts twoslash 56 | // schema.ts 57 | // @filename: g.ts 58 | import { gWithContext } from "@graphql-ts/schema"; 59 | 60 | export type Context = {}; 61 | 62 | export const g = gWithContext(); 63 | export type g = gWithContext.infer; 64 | // @filename: schema.ts 65 | // ---cut--- 66 | import { g } from "./g"; 67 | import { GraphQLSchema } from "graphql"; 68 | 69 | const Query = g.object()({ 70 | name: "Query", 71 | fields: { 72 | hello: g.field({ 73 | type: g.String, 74 | resolve: () => "world", 75 | }), 76 | }, 77 | }); 78 | 79 | const schema = new GraphQLSchema({ 80 | query: Query, 81 | }); 82 | ``` 83 | 84 | If we print that schema in GraphQL SDL, it looks like this: 85 | 86 | ```graphql 87 | # schema.graphql 88 | type Query { 89 | hello: String 90 | } 91 | ``` 92 | 93 | You've now created a GraphQL schema using `@graphql-ts/schema`! 🎉 94 | 95 | This `schema` object can be used with any server that works with GraphQL.js schema, such as [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server), [Apollo Server](https://www.apollographql.com/docs/apollo-server), [graphql-http](https://github.com/graphql/graphql-http), etc. 96 | -------------------------------------------------------------------------------- /site/src/content/docs/types/enum.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Enum Types 3 | sidebar: 4 | label: Enum 5 | order: 3 6 | --- 7 | 8 | import ShowSchema from "../../../show-schema.astro"; 9 | 10 | GraphQL Enum Types are a way to define a type that can be one of a set of predefined values. 11 | 12 | 13 | 14 | ```ts twoslash 15 | import { gWithContext } from "@graphql-ts/schema"; 16 | const g = gWithContext(); 17 | // ---cut--- 18 | const TaskStatus = g.enum({ 19 | name: "TaskStatus", 20 | values: { 21 | TODO: { value: "todo" }, 22 | IN_PROGRESS: { value: "inProgress" }, 23 | DONE: { value: "done" }, 24 | }, 25 | }); 26 | // ---cut-after--- 27 | import { GraphQLSchema } from "graphql"; 28 | const schema = new GraphQLSchema({ types: [TaskStatus] }); 29 | ``` 30 | 31 | 32 | 33 | Note the key and value don't have to be the same, the key defines what the enum is for consumers of the GraphQL API. 34 | 35 | The `value` defines what the enum is for the schema implementation when it is received in/returned from resolvers. (this value can be any type, it is not constrained to a string, it could be a number, symbol, TypeScript enum value or any other value) 36 | 37 | 38 | 39 | ```ts twoslash 40 | import { gWithContext } from "@graphql-ts/schema"; 41 | const g = gWithContext(); 42 | 43 | const TaskStatus = g.enum({ 44 | name: "TaskStatus", 45 | values: { 46 | TODO: { value: "todo" }, 47 | IN_PROGRESS: { value: "inProgress" }, 48 | DONE: { value: "done" }, 49 | }, 50 | }); 51 | 52 | // ---cut--- 53 | const field = g.field({ 54 | type: TaskStatus, 55 | args: { status: g.arg({ type: g.nonNull(TaskStatus) }) }, 56 | resolve(source, args, context) { 57 | return args.status; 58 | // ^? 59 | }, 60 | }); 61 | // ---cut-after--- 62 | import { GraphQLSchema } from "graphql"; 63 | const schema = new GraphQLSchema({ 64 | query: g.object()({ name: "Query", fields: { field } }), 65 | }); 66 | ``` 67 | 68 | 69 | 70 | Of course in most cases, the internal and external values will likely be the same so `@graphql-ts/schema` provides a shorthand for defining enum values with `g.enumValues`: 71 | 72 | 73 | 74 | ```ts twoslash 75 | import { gWithContext } from "@graphql-ts/schema"; 76 | const g = gWithContext(); 77 | // ---cut--- 78 | const TaskStatus = g.enum({ 79 | name: "TaskStatus", 80 | values: g.enumValues(["TODO", "IN_PROGRESS", "DONE"]), 81 | }); 82 | 83 | const field = g.field({ 84 | type: TaskStatus, 85 | args: { status: g.arg({ type: g.nonNull(TaskStatus) }) }, 86 | resolve(source, args, context) { 87 | return args.status; 88 | // ^? 89 | }, 90 | }); 91 | // ---cut-after--- 92 | import { GraphQLSchema } from "graphql"; 93 | const schema = new GraphQLSchema({ 94 | query: g.object()({ name: "Query", fields: { field } }), 95 | }); 96 | ``` 97 | 98 | 99 | -------------------------------------------------------------------------------- /site/src/content/docs/types/input-object.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Input Object Types 3 | sidebar: 4 | order: 2 5 | label: Input Object 6 | --- 7 | 8 | import ShowSchema from "../../../show-schema.astro"; 9 | 10 | Input objects are used to represent the input arguments of a field. They are similar to object types, but they are used to represent inputs instead of output fields. Input object types use `g.arg` similar to how args are defined on output fields rather than using the `g.field` function like on output object types. 11 | 12 | 13 | 14 | ```ts twoslash {1-7} 15 | import { gWithContext } from "@graphql-ts/schema"; 16 | 17 | type Context = { 18 | createUser: (input: { name: string; email: string }) => UserSource; 19 | }; 20 | 21 | const g = gWithContext(); 22 | type g = gWithContext.infer; 23 | 24 | type UserSource = { 25 | id: string; 26 | name: string; 27 | email: string; 28 | }; 29 | 30 | const User = g.object()({ 31 | name: "User", 32 | fields: { 33 | id: g.field({ type: g.nonNull(g.ID) }), 34 | name: g.field({ type: g.nonNull(g.String) }), 35 | email: g.field({ type: g.nonNull(g.String) }), 36 | }, 37 | }); 38 | // ---cut--- 39 | const UserCreateInput = g.inputObject({ 40 | name: "UserCreateInput", 41 | fields: { 42 | name: g.arg({ type: g.nonNull(g.String) }), 43 | email: g.arg({ type: g.nonNull(g.String) }), 44 | }, 45 | }); 46 | 47 | const Mutation = g.object()({ 48 | name: "Mutation", 49 | fields: { 50 | createUser: g.field({ 51 | type: User, 52 | args: { 53 | input: g.arg({ type: g.nonNull(UserCreateInput) }), 54 | }, 55 | resolve: (source, args, context) => { 56 | return context.createUser(args.input); 57 | // ^? 58 | }, 59 | }), 60 | }, 61 | }); 62 | // ---cut-after--- 63 | import { GraphQLSchema } from "graphql"; 64 | const schema = new GraphQLSchema({ mutation: Mutation }); 65 | ``` 66 | 67 | 68 | 69 | ## Circular Types 70 | 71 | While circular input object types are less common than circular Object Types, they can still occur. Just like with circular object types, we need to start by making `fields` a function: 72 | 73 | 74 | 75 | ```ts twoslash {3,6} 76 | // @errors: 7022 7023 77 | import { gWithContext } from "@graphql-ts/schema"; 78 | const g = gWithContext(); 79 | type g = gWithContext.infer; 80 | // ---cut--- 81 | const UserCreateInput = g.inputObject({ 82 | name: "UserCreateInput", 83 | fields: () => ({ 84 | name: g.arg({ type: g.nonNull(g.String) }), 85 | email: g.arg({ type: g.nonNull(g.String) }), 86 | friends: g.arg({ type: g.list(g.nonNull(UserCreateInput)) }), 87 | }), 88 | }); 89 | // ---cut-after--- 90 | import { GraphQLSchema } from "graphql"; 91 | const schema = new GraphQLSchema({ types: [UserCreateInput] }); 92 | ``` 93 | 94 | 95 | 96 | To resolve TypeScript's errors about circularity though, we unfortunately can't use the `g` type like we did with object types. Instead, we need to use the `GInputObjectType` type and we also need to essentially duplicate the fields definition in the types 97 | 98 | 99 | 100 | ```ts twoslash {8-14} ins=": UserCreateInput" 101 | import { gWithContext } from "@graphql-ts/schema"; 102 | const g = gWithContext(); 103 | type g = gWithContext.infer; 104 | // ---cut--- 105 | import type { 106 | GArg, 107 | GNonNull, 108 | GList, 109 | GInputObjectType, 110 | } from "@graphql-ts/schema"; 111 | 112 | type UserCreateInput = GInputObjectType<{ 113 | // you can use the G* types from @graphql-ts/schema to define the fields 114 | name: GArg>; 115 | // or you can use the g type and g.* functions 116 | email: g>>>; 117 | friends: GArg>>; 118 | }>; 119 | 120 | const UserCreateInput: UserCreateInput = g.inputObject({ 121 | name: "UserCreateInput", 122 | fields: () => ({ 123 | name: g.arg({ type: g.nonNull(g.String) }), 124 | email: g.arg({ type: g.nonNull(g.String) }), 125 | friends: g.arg({ type: g.list(g.nonNull(UserCreateInput)) }), 126 | }), 127 | }); 128 | // ---cut-after--- 129 | import { GraphQLSchema } from "graphql"; 130 | const schema = new GraphQLSchema({ types: [UserCreateInput] }); 131 | ``` 132 | 133 | 134 | 135 | Since most fields in input objects won't be circular, you can also extract out the non-circular fields and use `typeof` instead of duplicating the fields: 136 | 137 | 138 | 139 | ```ts twoslash /typeof userCreateInputFields/ 140 | import { gWithContext } from "@graphql-ts/schema"; 141 | const g = gWithContext(); 142 | type g = gWithContext.infer; 143 | // ---cut--- 144 | import type { 145 | GArg, 146 | GNonNull, 147 | GList, 148 | GInputObjectType, 149 | } from "@graphql-ts/schema"; 150 | 151 | const userCreateInputFields = { 152 | name: g.arg({ type: g.nonNull(g.String) }), 153 | email: g.arg({ type: g.nonNull(g.String) }), 154 | }; 155 | 156 | type UserCreateInput = GInputObjectType< 157 | typeof userCreateInputFields & { 158 | friends: GArg>>; 159 | } 160 | >; 161 | 162 | const UserCreateInput: UserCreateInput = g.inputObject({ 163 | name: "UserCreateInput", 164 | fields: () => ({ 165 | ...userCreateInputFields, 166 | friends: g.arg({ type: g.list(g.nonNull(UserCreateInput)) }), 167 | }), 168 | }); 169 | // ---cut-after--- 170 | import { GraphQLSchema } from "graphql"; 171 | const schema = new GraphQLSchema({ types: [UserCreateInput] }); 172 | ``` 173 | 174 | 175 | 176 | ## Nullability & Default Values 177 | 178 | Input objects can also have default values for their arguments. Default values are used when the argument is not provided in the input object. Default values can be provided using the `defaultValue` property in the `g.arg` function. We're going to demonstrate by using `args` on a field rather than on an input object type but the same concept applies to input object types. 179 | 180 | To start with, since all types are nullable in GraphQL by default, let's look at what the type of a nullable type is: 181 | 182 | 183 | 184 | ```ts twoslash {4} 185 | import { gWithContext } from "@graphql-ts/schema"; 186 | const g = gWithContext(); 187 | type g = gWithContext.infer; 188 | // ---cut--- 189 | const field = g.field({ 190 | type: g.String, 191 | args: { 192 | name: g.arg({ type: g.String }), 193 | }, 194 | resolve(_, args) { 195 | // ^? 196 | return args.name; 197 | }, 198 | }); 199 | // ---cut-after--- 200 | import { GraphQLSchema } from "graphql"; 201 | const schema = new GraphQLSchema({ 202 | query: g.object()({ name: "Query", fields: { field } }), 203 | }); 204 | ``` 205 | 206 | 207 | 208 | For the `String` scalar, the type is `string | null | undefined`. 209 | 210 | This is because for a nullable input type, there are three possible options for a consumer of a GraphQL API: 211 | 212 | ```graphql 213 | query { 214 | withoutName: field # represented as `undefined` in resolvers 215 | withNull: field(name: null) 216 | withValue: field(name: "string") 217 | } 218 | ``` 219 | 220 | For a nullable input type, adding a `defaultValue` will remove the `undefined` option but note that it will not remove `null`. 221 | 222 | ```ts twoslash /defaultValue: "name"/ 223 | import { gWithContext } from "@graphql-ts/schema"; 224 | const g = gWithContext(); 225 | type g = gWithContext.infer; 226 | // ---cut--- 227 | g.field({ 228 | type: g.String, 229 | args: { 230 | name: g.arg({ type: g.String, defaultValue: "name" }), 231 | }, 232 | resolve(_, args) { 233 | // ^? 234 | return args.name; 235 | }, 236 | }); 237 | ``` 238 | 239 | For non-nullable input types, adding a `defaultValue` doesn't affect the type received by the `resolve` function, it just allows the consumer to not provide the argument in which case the default value will be used: 240 | 241 | ```ts twoslash "g.nonNull" 242 | import { gWithContext } from "@graphql-ts/schema"; 243 | const g = gWithContext(); 244 | type g = gWithContext.infer; 245 | // ---cut--- 246 | g.field({ 247 | type: g.String, 248 | args: { 249 | name: g.arg({ type: g.nonNull(g.String), defaultValue: "name" }), 250 | }, 251 | resolve(_, args) { 252 | // ^? 253 | return args.name; 254 | }, 255 | }); 256 | ``` 257 | 258 | ## One Of 259 | 260 | To express an input object where a consumer must provide exactly one of the keys, you can use `isOneOf`. Note all the fields must be nullable when defining the type but in your resolver, exactly one key will be provided and it will be non-null and no other keys will be provided. 261 | 262 | ```ts twoslash {3} 263 | import { gWithContext } from "@graphql-ts/schema"; 264 | const g = gWithContext(); 265 | type g = gWithContext.infer; 266 | 267 | type UserSource = { id: string; name: string; email: string }; 268 | const User = g.object()({ 269 | name: "User", 270 | fields: { 271 | id: g.field({ type: g.nonNull(g.ID) }), 272 | name: g.field({ type: g.nonNull(g.String) }), 273 | email: g.field({ type: g.nonNull(g.String) }), 274 | }, 275 | }); 276 | // ---cut--- 277 | const UserCreateInput = g.inputObject({ 278 | name: "UserSearchInput", 279 | isOneOf: true, 280 | fields: { 281 | id: g.arg({ type: g.ID }), 282 | email: g.arg({ type: g.String }), 283 | name: g.arg({ type: g.String }), 284 | }, 285 | }); 286 | 287 | const search = g.field({ 288 | type: User, 289 | args: { 290 | input: g.arg({ type: g.nonNull(UserCreateInput) }), 291 | }, 292 | resolve(_, args) { 293 | console.log(args.input); 294 | // ^? 295 | return null; 296 | }, 297 | }); 298 | // ---cut-after--- 299 | import { GraphQLSchema } from "graphql"; 300 | const schema = new GraphQLSchema({ 301 | query: g.object()({ 302 | name: "Query", 303 | fields: { search }, 304 | }), 305 | }); 306 | ``` 307 | -------------------------------------------------------------------------------- /site/src/content/docs/types/interface-union.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Interface & Union Types 3 | sidebar: 4 | label: Interface & Union 5 | --- 6 | 7 | import ShowSchema from "../../../show-schema.astro"; 8 | import { TabItem } from "@astrojs/starlight/components"; 9 | 10 | GraphQL union & interface types are both ways to allow a GraphQL field to return one of a set of [object types](./object). 11 | 12 | The difference between the two is that interface types declare a common set of fields that must be implemented by the object types that implement the interface, while union types declare a set of object types and those objects don't have to share any fields. 13 | 14 | ## Interface Types 15 | 16 | In `@graphql-ts/schema`, you can define interface types using the `g.interface` function and then use the `interfaces` option on object types to declare that the object type implements the interface: 17 | 18 | 19 | 20 | ```ts twoslash {1-7,12,26} 21 | import { gWithContext } from "@graphql-ts/schema"; 22 | const g = gWithContext<{ 23 | loadEntity: (id: string) => Promise; 24 | }>(); 25 | type g = gWithContext.infer; 26 | // ---cut--- 27 | const Entity = g.interface()({ 28 | name: "Entity", 29 | fields: { 30 | id: g.field({ type: g.nonNull(g.ID) }), 31 | name: g.field({ type: g.nonNull(g.String) }), 32 | }, 33 | }); 34 | 35 | type UserSource = { __typename: "User"; id: string; name: string }; 36 | const User = g.object()({ 37 | name: "User", 38 | interfaces: [Entity], 39 | fields: { 40 | id: g.field({ type: g.nonNull(g.ID) }), 41 | name: g.field({ type: g.nonNull(g.String) }), 42 | }, 43 | }); 44 | 45 | type OrganisationSource = { 46 | __typename: "Organisation"; 47 | id: string; 48 | name: string; 49 | }; 50 | const Organisation = g.object()({ 51 | name: "Organisation", 52 | interfaces: [Entity], 53 | fields: { 54 | id: g.field({ type: g.nonNull(g.ID) }), 55 | name: g.field({ type: g.nonNull(g.String) }), 56 | members: g.field({ type: g.list(User), resolve: () => [] }), 57 | }, 58 | }); 59 | // ---cut-after--- 60 | 61 | const Query = g.object()({ 62 | name: "Query", 63 | fields: { 64 | entity: g.field({ 65 | type: Entity, 66 | args: { id: g.arg({ type: g.nonNull(g.ID) }) }, 67 | resolve(_, args, context) { 68 | return context.loadEntity(args.id); 69 | }, 70 | }), 71 | }, 72 | }); 73 | import { GraphQLSchema } from "graphql"; 74 | const schema = new GraphQLSchema({ 75 | query: Query, 76 | types: [Entity, User, Organisation], 77 | }); 78 | ``` 79 | 80 | 81 | 82 | ```graphql 83 | query { 84 | entity(id: "123") { 85 | id 86 | name 87 | ... on Organisation { 88 | members { 89 | id 90 | } 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | 97 | 98 | 99 | 100 | ## Union Types 101 | 102 | Union types are defined using the `g.union` function: 103 | 104 | 105 | 106 | ```ts twoslash {19-22} 107 | import { gWithContext } from "@graphql-ts/schema"; 108 | const g = gWithContext<{ 109 | search: (query: string) => Array; 110 | }>(); 111 | type g = gWithContext.infer; 112 | // ---cut--- 113 | type UserSource = { __typename: "User"; id: string; name: string }; 114 | const User = g.object()({ 115 | name: "User", 116 | fields: { 117 | id: g.field({ type: g.nonNull(g.ID) }), 118 | name: g.field({ type: g.nonNull(g.String) }), 119 | }, 120 | }); 121 | 122 | type PostSource = { __typename: "Post"; id: string; title: string }; 123 | const Post = g.object()({ 124 | name: "Post", 125 | fields: { 126 | id: g.field({ type: g.nonNull(g.ID) }), 127 | title: g.field({ type: g.nonNull(g.String) }), 128 | }, 129 | }); 130 | 131 | const SearchResult = g.union({ 132 | name: "SearchResult", 133 | types: [User, Post], 134 | }); 135 | // ---cut-after--- 136 | const Query = g.object()({ 137 | name: "Query", 138 | fields: { 139 | search: g.field({ 140 | type: g.list(SearchResult), 141 | args: { 142 | query: g.arg({ type: g.nonNull(g.String) }), 143 | }, 144 | resolve(_, args, context) { 145 | return context.search(args.query); 146 | }, 147 | }), 148 | }, 149 | }); 150 | import { GraphQLSchema } from "graphql"; 151 | const schema = new GraphQLSchema({ query: Query }); 152 | ``` 153 | 154 | 155 | 156 | ```graphql 157 | query { 158 | search(query: "graphql") { 159 | __typename 160 | ... on User { 161 | id 162 | name 163 | } 164 | ... on Post { 165 | id 166 | title 167 | } 168 | } 169 | } 170 | ``` 171 | 172 | 173 | 174 | 175 | ## `resolveType` and `isTypeOf` 176 | 177 | In the examples above, we have included `__typename` fields on the source objects. By default GraphQL.js uses `__typename` to determine what concrete object type is being returned when returning a union or interface type. 178 | 179 | There are also two other options: 180 | 181 | - `resolveType` on a interface/union types, this accepts a source type and returns the object type name that it corresponds to 182 | - `isTypeOf` on object types, this accepts a source type and returns a boolean indicating if the object type is the correct type for the source type 183 | 184 | You can use either one, you don't need to use both. 185 | 186 | ```ts twoslash {7-9,16} /entityType/ 187 | import { gWithContext } from "@graphql-ts/schema"; 188 | const g = gWithContext(); 189 | type g = gWithContext.infer; 190 | // ---cut--- 191 | const Entity = g.interface<{ entityType: string }>()({ 192 | name: "Entity", 193 | fields: { 194 | id: g.field({ type: g.nonNull(g.ID) }), 195 | name: g.field({ type: g.nonNull(g.String) }), 196 | }, 197 | resolveType(source) { 198 | return source.entityType; 199 | }, 200 | }); 201 | 202 | type UserSource = { entityType: "User"; id: string; name: string }; 203 | 204 | const User = g.object()({ 205 | name: "User", 206 | interfaces: [Entity], 207 | fields: { 208 | id: g.field({ type: g.nonNull(g.ID) }), 209 | name: g.field({ type: g.nonNull(g.String) }), 210 | }, 211 | }); 212 | ``` 213 | 214 | ```ts twoslash {12,17-19} 215 | import { gWithContext } from "@graphql-ts/schema"; 216 | const g = gWithContext(); 217 | type g = gWithContext.infer; 218 | // ---cut--- 219 | const Entity = g.interface()({ 220 | name: "Entity", 221 | fields: { 222 | id: g.field({ type: g.nonNull(g.ID) }), 223 | name: g.field({ type: g.nonNull(g.String) }), 224 | }, 225 | }); 226 | 227 | type UserSource = { entityType: "User"; id: string; name: string }; 228 | const User = g.object()({ 229 | name: "User", 230 | interfaces: [Entity], 231 | fields: { 232 | id: g.field({ type: g.nonNull(g.ID) }), 233 | name: g.field({ type: g.nonNull(g.String) }), 234 | }, 235 | isTypeOf(source) { 236 | return source.entityType === "User"; 237 | }, 238 | }); 239 | ``` 240 | 241 | :::note 242 | 243 | `@graphql-ts/schema` isn't able to provide strong validation to ensure that you're providing a `__typename`/correctly implementing `resolveType`/`isTypeOf` functions so be careful to ensure you've implemented it correctly. 244 | 245 | ::: 246 | -------------------------------------------------------------------------------- /site/src/content/docs/types/list-non-null.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: List & Non-Null Types 3 | sidebar: 4 | label: List & Non-Null 5 | order: 4 6 | --- 7 | 8 | import ShowSchema from "../../../show-schema.astro"; 9 | 10 | ## List Types 11 | 12 | GraphQL list types represent a list of values of a specific type. In `@graphql-ts/schema`, you can create a list type by calling the `g.list` function with the type of the list as the argument. They can be used in both input and output types. 13 | 14 | 15 | 16 | ```ts twoslash /g.list/ 17 | import { gWithContext } from "@graphql-ts/schema"; 18 | 19 | const g = gWithContext<{ 20 | fetchUsers: (ids: string[]) => Promise; 21 | }>(); 22 | type g = gWithContext.infer; 23 | 24 | type UserSource = { 25 | id: string; 26 | name: string; 27 | }; 28 | 29 | const User = g.object()({ 30 | name: "User", 31 | fields: { 32 | id: g.field({ type: g.nonNull(g.ID) }), 33 | name: g.field({ type: g.nonNull(g.String) }), 34 | }, 35 | }); 36 | // ---cut--- 37 | const Query = g.object()({ 38 | name: "Query", 39 | fields: { 40 | users: g.field({ 41 | type: g.list(User), 42 | args: { 43 | ids: g.arg({ type: g.nonNull(g.list(g.nonNull(g.ID))) }), 44 | }, 45 | resolve(_, args, context) { 46 | return context.fetchUsers(args.ids); 47 | }, 48 | }), 49 | }, 50 | }); 51 | // ---cut-after--- 52 | import { GraphQLSchema } from "graphql"; 53 | const schema = new GraphQLSchema({ query: Query }); 54 | ``` 55 | 56 | 57 | 58 | ## Non-Null Types 59 | 60 | In GraphQL, by default every type can also be null (and in the case of input types, `undefined` as well). You can make a type non-null by wrapping it in the `g.nonNull` function. Non-null types can be used in both input and output types. 61 | 62 | 63 | 64 | ```ts twoslash /g.nonNull/ 65 | import { gWithContext } from "@graphql-ts/schema"; 66 | 67 | const g = gWithContext<{ 68 | fetchUser: (id: string) => Promise; 69 | }>(); 70 | 71 | type g = gWithContext.infer; 72 | // ---cut--- 73 | type UserSource = { id: string; name: string }; 74 | const User = g.object()({ 75 | name: "User", 76 | fields: { 77 | id: g.field({ type: g.nonNull(g.ID) }), 78 | name: g.field({ type: g.nonNull(g.String) }), 79 | }, 80 | }); 81 | 82 | const Query = g.object()({ 83 | name: "Query", 84 | fields: { 85 | user: g.field({ 86 | type: User, 87 | args: { 88 | id: g.arg({ type: g.nonNull(g.ID) }), 89 | }, 90 | resolve(_, args, context) { 91 | return context.fetchUser(args.id); 92 | }, 93 | }), 94 | }, 95 | }); 96 | // ---cut-after--- 97 | import { GraphQLSchema } from "graphql"; 98 | const schema = new GraphQLSchema({ query: Query }); 99 | ``` 100 | 101 | 102 | -------------------------------------------------------------------------------- /site/src/content/docs/types/object.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Object Types 3 | sidebar: 4 | label: Object 5 | order: 1 6 | --- 7 | 8 | import ShowSchema from "../../../show-schema.astro"; 9 | 10 | Object types are the most common type in GraphQL. They represent a type with a set of fields that you can query. In `@graphql-ts/schema`, you can define object types using the `g.object` function. 11 | 12 | To define an object type, you need to provide a source type that represents the shape of the object type on the server. Note this type doesn't have to align with the GraphQL fields. 13 | 14 | ```ts twoslash 15 | // @noErrors 16 | import { gWithContext } from "@graphql-ts/schema"; 17 | const g = gWithContext(); 18 | type g = gWithContext.infer; 19 | // ---cut--- 20 | type UserSource = { 21 | id: string; 22 | name: string; 23 | }; 24 | 25 | const User = g.object()({ 26 | name: "User", 27 | }); 28 | ``` 29 | 30 | :::note 31 | 32 | `g.object` is called with only the source type and no run-time arguments and then the return of that function is called with the actual runtime arguments. 33 | 34 | ::: 35 | 36 | Then we can define the fields of the object type. The `fields` object is a map of field names to field definitions. The field definitions are created using the `g.field` function. 37 | 38 | 39 | 40 | ```ts twoslash 41 | import { gWithContext } from "@graphql-ts/schema"; 42 | const g = gWithContext(); 43 | type g = gWithContext.infer; 44 | 45 | type UserSource = { 46 | id: string; 47 | name: string; 48 | }; 49 | // ---cut--- 50 | const User = g.object()({ 51 | name: "User", 52 | fields: { 53 | id: g.field({ type: g.nonNull(g.ID) }), 54 | name: g.field({ type: g.nonNull(g.String) }), 55 | }, 56 | }); 57 | // ---cut-after--- 58 | import { GraphQLSchema } from "graphql"; 59 | const schema = new GraphQLSchema({ types: [User] }); 60 | ``` 61 | 62 | 63 | 64 | Since the `UserSource` type in this case aligns with the GraphQL fields, we don't need to provide a `resolve` function for the fields. This isn't always the case though, if we add a field that doesn't exist on the `UserSource` type, we'll receive a TypeScript error: 65 | 66 | 67 | 68 | ```ts twoslash 69 | // @errors: 2322 2375 70 | import { gWithContext } from "@graphql-ts/schema"; 71 | const g = gWithContext(); 72 | type g = gWithContext.infer; 73 | 74 | type UserSource = { 75 | id: string; 76 | name: string; 77 | }; 78 | // ---cut--- 79 | const User = g.object()({ 80 | name: "User", 81 | fields: { 82 | id: g.field({ type: g.nonNull(g.ID) }), 83 | name: g.field({ type: g.nonNull(g.String) }), 84 | nameBackwards: g.field({ type: g.nonNull(g.String) }), 85 | }, 86 | }); 87 | // ---cut-after--- 88 | import { GraphQLSchema } from "graphql"; 89 | const schema = new GraphQLSchema({ types: [User] }); 90 | ``` 91 | 92 | 93 | 94 | This is telling us that we need to provide a `resolve` function for the `nameBackwards` field. The `resolve` function recieves the source type that we provided when creating the object type. This allows us to calculate the value of the field based on the source type. 95 | 96 | 97 | 98 | ```ts twoslash 99 | import { gWithContext } from "@graphql-ts/schema"; 100 | const g = gWithContext(); 101 | type g = gWithContext.infer; 102 | 103 | type UserSource = { 104 | id: string; 105 | name: string; 106 | }; 107 | // ---cut--- 108 | const User = g.object()({ 109 | name: "User", 110 | fields: { 111 | id: g.field({ type: g.nonNull(g.ID) }), 112 | name: g.field({ type: g.nonNull(g.String) }), 113 | nameBackwards: g.field({ 114 | type: g.nonNull(g.String), 115 | resolve(source) { 116 | return source.name.split("").reverse().join(""); 117 | }, 118 | }), 119 | }, 120 | }); 121 | // ---cut-after--- 122 | import { GraphQLSchema } from "graphql"; 123 | const schema = new GraphQLSchema({ types: [User] }); 124 | ``` 125 | 126 | 127 | 128 | ## Field Arguments 129 | 130 | Fields can also have arguments. Arguments are defined using the `args` object on the field definition. The `args` object is a map of argument names to argument definitions. The argument definitions are created using the `g.arg` function. `@graphql-ts/schema` will then infer the second parameter to `resolve` based on the `args` defintion. 131 | 132 | 133 | 134 | ```ts twoslash 135 | // @noErrors 136 | import { gWithContext } from "@graphql-ts/schema"; 137 | const g = gWithContext(); 138 | type g = gWithContext.infer; 139 | 140 | type UserSource = { 141 | id: string; 142 | name: string; 143 | }; 144 | // ---cut--- 145 | const User = g.object()({ 146 | name: "User", 147 | fields: { 148 | id: g.field({ type: g.nonNull(g.ID) }), 149 | name: g.field({ type: g.nonNull(g.String) }), 150 | nameWithLength: g.field({ 151 | type: g.nonNull(g.String), 152 | args: { 153 | length: g.arg({ type: g.nonNull(g.Int) }), 154 | }, 155 | resolve(source, args) { 156 | return source.name.slice(0, args.l); 157 | // ^| 158 | }, 159 | }), 160 | }, 161 | }); 162 | // ---cut-after--- 163 | import { GraphQLSchema } from "graphql"; 164 | const schema = new GraphQLSchema({ types: [User] }); 165 | ``` 166 | 167 | 168 | 169 | ## Context 170 | 171 | The `resolve` function is also provided a third argument, the `context`. This is useful for providing things like database connections, authentication information, etc. The `context` is defined by the `Context` type that is passed to `gWithContext`. 172 | 173 | 174 | 175 | {/* prettier-ignore */} 176 | ```ts twoslash "" /, (context)/ 177 | // @noErrors 178 | import { gWithContext } from "@graphql-ts/schema"; 179 | 180 | type Context = { 181 | loadUser: (id: string) => Promise; 182 | }; 183 | const g = gWithContext(); 184 | type g = gWithContext.infer; 185 | // ---cut-start--- 186 | type UserSource = { 187 | id: string; 188 | name: string; 189 | }; 190 | 191 | const User = g.object()({ 192 | name: "User", 193 | fields: { 194 | id: g.field({ type: g.nonNull(g.ID) }), 195 | name: g.field({ type: g.nonNull(g.String) }), 196 | }, 197 | }); 198 | // ---cut-end--- 199 | 200 | const Query = g.object()({ 201 | name: "Query", 202 | fields: () => ({ 203 | user: g.field({ 204 | type: User, 205 | args: { 206 | id: g.arg({ type: g.nonNull(g.ID) }), 207 | }, 208 | resolve(_, args, context) { 209 | return context.lo 210 | // ^| 211 | }, 212 | }), 213 | }), 214 | }); 215 | // ---cut-after--- 216 | import { GraphQLSchema } from "graphql"; 217 | const schema = new GraphQLSchema({ query: Query }); 218 | ``` 219 | 220 | 221 | 222 | ## Circular Types 223 | 224 | Since GraphQL, like the name suggests, is about querying a graph, it's common to have circular types in your schema where one type references another type that references the first type. To start with, we need to make `fields` a function that returns a function so we can lazily reference the `User` type: 225 | 226 | ```ts twoslash ins="() => (" 227 | // @errors: 7022 7023 228 | import { gWithContext } from "@graphql-ts/schema"; 229 | const g = gWithContext(); 230 | type g = gWithContext.infer; 231 | 232 | type UserSource = { 233 | id: string; 234 | name: string; 235 | }; 236 | // ---cut--- 237 | const User = g.object()({ 238 | name: "User", 239 | fields: () => ({ 240 | id: g.field({ type: g.nonNull(g.ID) }), 241 | name: g.field({ type: g.nonNull(g.String) }), 242 | friends: g.field({ 243 | type: g.list(User), 244 | resolve: () => [], 245 | }), 246 | }), 247 | }); 248 | ``` 249 | 250 | TypeScript doesn't understand this circularity though, we can fix this by using the `g` type and the `g.object` function to be explicit about the type of `User`: 251 | 252 | 253 | 254 | ```ts twoslash {3} ins=": g>" 255 | import { gWithContext } from "@graphql-ts/schema"; 256 | const g = gWithContext(); 257 | type g = gWithContext.infer; 258 | 259 | type UserSource = { id: string; name: string }; 260 | const User: g> = g.object()({ 261 | name: "User", 262 | fields: () => ({ 263 | id: g.field({ type: g.nonNull(g.ID) }), 264 | name: g.field({ type: g.nonNull(g.String) }), 265 | friends: g.field({ 266 | type: g.list(User), 267 | resolve: () => [], 268 | }), 269 | }), 270 | }); 271 | // ---cut-after--- 272 | import { GraphQLSchema } from "graphql"; 273 | const schema = new GraphQLSchema({ types: [User] }); 274 | ``` 275 | 276 | 277 | 278 | :::note[What's going on here?] 279 | 280 | The `g>` might seem a bit magic so let's break down what's going on here. 281 | 282 | The inner bit `typeof g.object` is using [Instantiation Expressions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#instantiation-expressions) which gives us a function type that already has the type parameters applied: 283 | 284 | ```ts twoslash 285 | import { gWithContext, GObjectType } from "@graphql-ts/schema"; 286 | type Context = {}; 287 | const g = gWithContext(); 288 | type g = gWithContext.infer; 289 | 290 | type SomeArgs = { 291 | thisIsntImportant: string; 292 | }; 293 | 294 | type UserSource = { 295 | id: string; 296 | name: string; 297 | }; 298 | // ---cut--- 299 | type UserSourceFn = typeof g.object; 300 | // these two types are ~equivalent 301 | type UserSourceFn2 = () => (args: SomeArgs) => GObjectType; 302 | ``` 303 | 304 | But then we have to wrap it in `g<...>` to get the type of the object type. 305 | 306 | ```ts twoslash 307 | import { gWithContext, GObjectType } from "@graphql-ts/schema"; 308 | type Context = {}; 309 | const g = gWithContext(); 310 | type g = gWithContext.infer; 311 | 312 | type UserSource = { 313 | id: string; 314 | name: string; 315 | }; 316 | // ---cut--- 317 | type UserType = g>; 318 | ``` 319 | 320 | If you've seen the `ReturnType` utility type, `g` is basically doing `ReturnType>` here. 321 | 322 | ```ts twoslash 323 | type ReturnType any> = T extends ( 324 | ...args: any 325 | ) => infer R 326 | ? R 327 | : any; 328 | ``` 329 | 330 | `g` is a bit more complex than that though because it supports using both `g.object` along with e.g. `g.enum` which doesn't have the double function call syntax. 331 | 332 | ```ts twoslash 333 | import { gWithContext, GObjectType } from "@graphql-ts/schema"; 334 | type Context = {}; 335 | const g = gWithContext(); 336 | 337 | type UserSource = { 338 | id: string; 339 | name: string; 340 | }; 341 | // ---cut--- 342 | type g = T extends () => (args: any) => infer R 343 | ? R 344 | : T extends (args: any) => infer R 345 | ? R 346 | : never; 347 | type UserType = g>; 348 | ``` 349 | 350 | You also might have noticed that we've been defining `g` as both a type and a value with `g.object`/etc. This makes accessing the type and functions easy so you only have to import a single thing. 351 | 352 | ::: 353 | 354 |
355 | Alternative Syntax 356 | 357 | The `g` type and `g.object` function is a shorthand for using `GObjectType`. You can also use `GObjectType` directly, though it does mean importing the extra type and instead of only providing the source type, you also need to provide the context type whereas with `g>`, the context type is inferred from the `gWithContext` call. 358 | 359 | 360 | 361 | ```ts twoslash ins=", GObjectType" ins=": GObjectType" 362 | import { gWithContext, GObjectType } from "@graphql-ts/schema"; 363 | type Context = {}; 364 | const g = gWithContext(); 365 | type g = gWithContext.infer; 366 | type UserSource = { 367 | id: string; 368 | name: string; 369 | }; 370 | 371 | const User: GObjectType = g.object()({ 372 | name: "User", 373 | fields: () => ({ 374 | id: g.field({ type: g.nonNull(g.ID) }), 375 | name: g.field({ type: g.nonNull(g.String) }), 376 | friends: g.field({ 377 | type: g.list(User), 378 | resolve: () => [], 379 | }), 380 | }), 381 | }); 382 | // ---cut-after--- 383 | import { GraphQLSchema } from "graphql"; 384 | const schema = new GraphQLSchema({ types: [User] }); 385 | ``` 386 | 387 | 388 | 389 |
390 | -------------------------------------------------------------------------------- /site/src/content/docs/types/scalar.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Scalar Types 3 | sidebar: 4 | label: Scalar 5 | --- 6 | 7 | import ShowSchema from "../../../show-schema.astro"; 8 | 9 | GraphQL has a set of built-in scalars that are used to represent primitive types, in `@graphql-ts/schema`, these are accessible at `g.Int`, `g.Float`, `g.String`, `g.Boolean`, and `g.ID`. 10 | 11 | ## Custom Scalars 12 | 13 | You can define custom scalars using the `g.scalar` function. In general since you will likely want to use your custom scalars all around your schema, a common pattern is to include them in the `g` object like this: 14 | 15 | 16 | 17 | ```ts twoslash "_g.scalar" 18 | // @declaration: true 19 | import { gWithContext } from "@graphql-ts/schema"; 20 | import { GraphQLError } from "graphql"; 21 | 22 | const _g = gWithContext(); 23 | 24 | export const g = { 25 | ..._g, 26 | BigInt: _g.scalar({ 27 | name: "BigInt", 28 | serialize(value) { 29 | if (typeof value === "bigint") { 30 | return value.toString(); 31 | } 32 | throw new GraphQLError( 33 | "BigInt cannot represent non-bigint value: " + value 34 | ); 35 | }, 36 | parseValue(value) { 37 | if (typeof value === "bigint") { 38 | return value; 39 | } 40 | if (typeof value === "string") { 41 | return BigInt(value); 42 | } 43 | throw new GraphQLError("BigInt must be a string"); 44 | }, 45 | parseLiteral(ast) { 46 | if (ast.kind === "StringValue") { 47 | return BigInt(ast.value); 48 | } 49 | throw new GraphQLError("BigInt must be a string"); 50 | }, 51 | }), 52 | }; 53 | export type g = gWithContext.infer; 54 | // ---cut-after--- 55 | import { GraphQLSchema } from "graphql"; 56 | const schema = new GraphQLSchema({ 57 | types: [g.BigInt], 58 | }); 59 | ``` 60 | 61 | 62 | 63 | :::tip 64 | 65 | If you receive a TypeScript error like `The inferred type of 'g' cannot be named without a reference to './node_modules/@graphql-ts/schema/src/types'. This is likely not portable. A type annotation is necessary.`, use the `GWithContext` type to be explicit about the type of `g`: 66 | 67 | ```ts twoslash "GWithContext" 68 | import { gWithContext, GWithContext } from "@graphql-ts/schema"; 69 | type Context = {}; 70 | const extra = {}; 71 | const g: GWithContext & typeof extra = { 72 | ...gWithContext(), 73 | ...extra, 74 | }; 75 | ``` 76 | 77 | ::: 78 | 79 | Unlike all other GraphQL types in `@graphql-ts/schema`, the TypeScript types for scalars are exactly the same in `@graphql-ts/schema` as GraphQL.js so you can use `GraphQLScalarType` from GraphQL.js interchangably with `g.scalar`/`GScalarType`, For example, the above code can be rewritten using `GraphQLScalarType`: 80 | 81 | 82 | 83 | ```ts twoslash 84 | import { gWithContext } from "@graphql-ts/schema"; 85 | import { GraphQLError, GraphQLScalarType } from "graphql"; 86 | 87 | export const g = { 88 | ...gWithContext(), 89 | BigInt: new GraphQLScalarType({ 90 | name: "BigInt", 91 | serialize(value) { 92 | if (typeof value === "bigint") { 93 | return value.toString(); 94 | } 95 | throw new GraphQLError( 96 | "BigInt cannot represent non-bigint value: " + value 97 | ); 98 | }, 99 | parseValue(value) { 100 | if (typeof value === "bigint") { 101 | return value; 102 | } 103 | if (typeof value === "string") { 104 | return BigInt(value); 105 | } 106 | throw new GraphQLError("BigInt must be a string"); 107 | }, 108 | parseLiteral(ast) { 109 | if (ast.kind === "StringValue") { 110 | return BigInt(ast.value); 111 | } 112 | throw new GraphQLError("BigInt must be a string"); 113 | }, 114 | }), 115 | }; 116 | export type g = gWithContext.infer; 117 | // ---cut-after--- 118 | import { GraphQLSchema } from "graphql"; 119 | const schema = new GraphQLSchema({ 120 | types: [g.BigInt], 121 | }); 122 | ``` 123 | 124 | 125 | -------------------------------------------------------------------------------- /site/src/index.css: -------------------------------------------------------------------------------- 1 | /* Dark mode colors. */ 2 | :root { 3 | --sl-color-accent-low: #36113e; 4 | --sl-color-accent: #a400c0; 5 | --sl-color-accent-high: #e3b6ed; 6 | --sl-color-white: #ffffff; 7 | --sl-color-gray-1: #f2e9fd; 8 | --sl-color-gray-2: #c7bdd5; 9 | --sl-color-gray-3: #9581ae; 10 | --sl-color-gray-4: #614e78; 11 | --sl-color-gray-5: #412e55; 12 | --sl-color-gray-6: #2f1c42; 13 | --sl-color-black: #1c1425; 14 | } 15 | /* Light mode colors. */ 16 | :root[data-theme="light"] { 17 | --sl-color-accent-low: #ebc9f3; 18 | --sl-color-accent: #a700c3; 19 | --sl-color-accent-high: #4e0e5b; 20 | --sl-color-white: #1c1425; 21 | --sl-color-gray-1: #2f1c42; 22 | --sl-color-gray-2: #412e55; 23 | --sl-color-gray-3: #614e78; 24 | --sl-color-gray-4: #9581ae; 25 | --sl-color-gray-5: #c7bdd5; 26 | --sl-color-gray-6: #f2e9fd; 27 | --sl-color-gray-7: #f8f4fe; 28 | --sl-color-black: #ffffff; 29 | } 30 | 31 | @import "tailwindcss"; 32 | 33 | @layer base { 34 | ul, 35 | ol { 36 | list-style-type: revert; 37 | list-style-position: inside; 38 | } 39 | :is(ul, ol) :is(ul, ol) { 40 | margin: revert; 41 | padding: revert; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /site/src/show-schema.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { TabItem, Tabs, Code } from '@astrojs/starlight/components' 3 | 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /site/tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | import starlightPlugin from "@astrojs/starlight-tailwind"; 2 | 3 | // Generated color palettes 4 | const accent = { 5 | 200: "#e3b6ed", 6 | 600: "#a700c3", 7 | 900: "#4e0e5b", 8 | 950: "#36113e", 9 | }; 10 | const gray = { 11 | 100: "#f8f4fe", 12 | 200: "#f2e9fd", 13 | 300: "#c7bdd5", 14 | 400: "#9581ae", 15 | 500: "#614e78", 16 | 700: "#412e55", 17 | 800: "#2f1c42", 18 | 900: "#1c1425", 19 | }; 20 | 21 | /** @type {import("tailwindcss").Config} */ 22 | export default { 23 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], 24 | theme: { 25 | extend: { 26 | colors: { accent, gray }, 27 | }, 28 | }, 29 | plugins: [starlightPlugin()], 30 | }; 31 | -------------------------------------------------------------------------------- /site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "checkJs": true, 5 | "verbatimModuleSyntax": false, 6 | "skipLibCheck": true 7 | }, 8 | "exclude": ["**/node_modules/**"] 9 | } 10 | -------------------------------------------------------------------------------- /test-project/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @graphql-ts/test-project 2 | 3 | ## 1.0.13 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`85c667e3a6affe4651dcfe31151e56d39c78411e`](https://github.com/Thinkmill/graphql-ts/commit/85c667e3a6affe4651dcfe31151e56d39c78411e)]: 8 | - @graphql-ts/schema@1.0.2 9 | - @graphql-ts/extend@2.0.0 10 | 11 | ## 1.0.12 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies [[`11ee5523d046a91c8e091580cc0954f9e2b108ae`](https://github.com/Thinkmill/graphql-ts/commit/11ee5523d046a91c8e091580cc0954f9e2b108ae)]: 16 | - @graphql-ts/schema@1.0.1 17 | - @graphql-ts/extend@2.0.0 18 | 19 | ## 1.0.11 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [[`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81), [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81), [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81), [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81), [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085), [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085), [`d7151bd2a6333327ac1a57e0c924bd4bfdbdf01f`](https://github.com/Thinkmill/graphql-ts/commit/d7151bd2a6333327ac1a57e0c924bd4bfdbdf01f), [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085), [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085), [`5d2341e2d4653f8370c05f0e07ba9a151bf6b085`](https://github.com/Thinkmill/graphql-ts/commit/5d2341e2d4653f8370c05f0e07ba9a151bf6b085), [`8169eb85cdfc22f1f9730fca9136bd44e057af81`](https://github.com/Thinkmill/graphql-ts/commit/8169eb85cdfc22f1f9730fca9136bd44e057af81)]: 24 | - @graphql-ts/schema@1.0.0 25 | - @graphql-ts/extend@2.0.0 26 | 27 | ## 1.0.10 28 | 29 | ### Patch Changes 30 | 31 | - Updated dependencies [[`692b08c5f7fdfb8e6aead74be2ea0841ba74dbad`](https://github.com/Thinkmill/graphql-ts/commit/692b08c5f7fdfb8e6aead74be2ea0841ba74dbad)]: 32 | - @graphql-ts/schema@0.6.4 33 | 34 | ## 1.0.9 35 | 36 | ### Patch Changes 37 | 38 | - Updated dependencies [[`91a28cb0b8eedf4a66dca624afc71de07d1c3a11`](https://github.com/Thinkmill/graphql-ts/commit/91a28cb0b8eedf4a66dca624afc71de07d1c3a11), [`7756410781a21ba77616c8fbf6b36e7ab211200f`](https://github.com/Thinkmill/graphql-ts/commit/7756410781a21ba77616c8fbf6b36e7ab211200f), [`7756410781a21ba77616c8fbf6b36e7ab211200f`](https://github.com/Thinkmill/graphql-ts/commit/7756410781a21ba77616c8fbf6b36e7ab211200f)]: 39 | - @graphql-ts/schema@0.6.3 40 | 41 | ## 1.0.8 42 | 43 | ### Patch Changes 44 | 45 | - Updated dependencies [[`2dc7f48a07f4e235261891dd0ff3bc4ca2ec9858`](https://github.com/Thinkmill/graphql-ts/commit/2dc7f48a07f4e235261891dd0ff3bc4ca2ec9858), [`974bdd8f2d1ca72b04dfde471fa4e119b2d1a7b1`](https://github.com/Thinkmill/graphql-ts/commit/974bdd8f2d1ca72b04dfde471fa4e119b2d1a7b1), [`65f914c83a6438e8b326047c0dc3d83d47ba110c`](https://github.com/Thinkmill/graphql-ts/commit/65f914c83a6438e8b326047c0dc3d83d47ba110c)]: 46 | - @graphql-ts/schema@0.6.2 47 | 48 | ## 1.0.7 49 | 50 | ### Patch Changes 51 | 52 | - Updated dependencies [[`d79115e1007e6e47b425293122818e7d40edf8b5`](https://github.com/Thinkmill/graphql-ts/commit/d79115e1007e6e47b425293122818e7d40edf8b5)]: 53 | - @graphql-ts/schema@0.6.1 54 | 55 | ## 1.0.6 56 | 57 | ### Patch Changes 58 | 59 | - Updated dependencies [[`012d84e`](https://github.com/Thinkmill/graphql-ts/commit/012d84e04bfe37c18aa0afdc541843586cf768bf)]: 60 | - @graphql-ts/schema@0.6.0 61 | 62 | ## 1.0.5 63 | 64 | ### Patch Changes 65 | 66 | - Updated dependencies [[`5d1c299`](https://github.com/Thinkmill/graphql-ts/commit/5d1c299ae50a8bafea8e409f9c2c1e5abedaa29a)]: 67 | - @graphql-ts/schema@0.5.0 68 | 69 | ## 1.0.4 70 | 71 | ### Patch Changes 72 | 73 | - Updated dependencies [[`6c85396`](https://github.com/Thinkmill/graphql-ts/commit/6c85396eee29d6eea75c43f54e50b90a3e63a266), [`910d1ed`](https://github.com/Thinkmill/graphql-ts/commit/910d1edc596f4a17b0a3dec3e3df8ebd94a5cb80)]: 74 | - @graphql-ts/schema@0.4.0 75 | 76 | ## 1.0.3 77 | 78 | ### Patch Changes 79 | 80 | - Updated dependencies [[`6e9a2fb`](https://github.com/Thinkmill/graphql-ts/commit/6e9a2fb1b5dd2965bc9e2783dfddd8a2bacf88f6)]: 81 | - @graphql-ts/schema@0.3.0 82 | 83 | ## 1.0.2 84 | 85 | ### Patch Changes 86 | 87 | - Updated dependencies [[`9f2e0fa`](https://github.com/Thinkmill/graphql-ts/commit/9f2e0fab2c7c483c3f4c13b285d6a33e75bb563c), [`9f2e0fa`](https://github.com/Thinkmill/graphql-ts/commit/9f2e0fab2c7c483c3f4c13b285d6a33e75bb563c), [`9f2e0fa`](https://github.com/Thinkmill/graphql-ts/commit/9f2e0fab2c7c483c3f4c13b285d6a33e75bb563c), [`9f2e0fa`](https://github.com/Thinkmill/graphql-ts/commit/9f2e0fab2c7c483c3f4c13b285d6a33e75bb563c), [`9f2e0fa`](https://github.com/Thinkmill/graphql-ts/commit/9f2e0fab2c7c483c3f4c13b285d6a33e75bb563c)]: 88 | - @graphql-ts/schema@0.2.0 89 | 90 | ## 1.0.1 91 | 92 | ### Patch Changes 93 | 94 | - Updated dependencies [[`2c9d25a`](https://github.com/Thinkmill/graphql-ts/commit/2c9d25ab7724a8a460b337a4a529accc0d3169ec)]: 95 | - @graphql-ts/schema@0.1.0 96 | -------------------------------------------------------------------------------- /test-project/example-2.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { gWithContext } from "@graphql-ts/schema"; 3 | import { GraphQLSchema, graphql } from "graphql"; 4 | import { deepEqual } from "node:assert"; 5 | 6 | type Context = { 7 | todos: Map; 8 | }; 9 | 10 | const g = gWithContext(); 11 | type g = gWithContext.infer; 12 | 13 | type TodoItem = { 14 | id: string; 15 | title: string; 16 | relatedTodos: string[]; 17 | }; 18 | 19 | const Todo: g> = g.object()({ 20 | name: "Todo", 21 | fields: () => ({ 22 | id: g.field({ type: g.nonNull(g.ID) }), 23 | title: g.field({ type: g.nonNull(g.String) }), 24 | relatedTodos: g.field({ 25 | type: g.list(Todo), 26 | resolve(source, _args, context) { 27 | return source.relatedTodos 28 | .map((id) => context.todos.get(id)) 29 | .filter((todo) => todo !== undefined); 30 | }, 31 | }), 32 | }), 33 | }); 34 | 35 | const Query = g.object()({ 36 | name: "Query", 37 | fields: { 38 | todos: g.field({ 39 | type: g.list(Todo), 40 | resolve(_source, _args, context) { 41 | return context.todos.values(); 42 | }, 43 | }), 44 | }, 45 | }); 46 | 47 | const Mutation = g.object()({ 48 | name: "Mutation", 49 | fields: { 50 | createTodo: g.field({ 51 | args: { 52 | title: g.arg({ type: g.nonNull(g.String) }), 53 | relatedTodos: g.arg({ 54 | type: g.nonNull(g.list(g.nonNull(g.ID))), 55 | defaultValue: [], 56 | }), 57 | }, 58 | type: Todo, 59 | resolve(_source, { title, relatedTodos }, context) { 60 | const todo = { title, relatedTodos, id: crypto.randomUUID() }; 61 | context.todos.set(todo.id, todo); 62 | return todo; 63 | }, 64 | }), 65 | }, 66 | }); 67 | 68 | const schema = new GraphQLSchema({ 69 | query: Query, 70 | mutation: Mutation, 71 | }); 72 | 73 | (async () => { 74 | const contextValue: Context = { todos: new Map() }; 75 | { 76 | const result = await graphql({ 77 | source: ` 78 | query { 79 | todos { 80 | title 81 | } 82 | } 83 | `, 84 | schema, 85 | contextValue, 86 | }); 87 | deepEqual(result, { data: { todos: [] } }); 88 | } 89 | 90 | { 91 | const result = await graphql({ 92 | source: ` 93 | mutation { 94 | createTodo(title: "Try graphql-ts") { 95 | title 96 | } 97 | } 98 | `, 99 | schema, 100 | contextValue, 101 | }); 102 | deepEqual(result, { 103 | data: { createTodo: { title: "Try graphql-ts" } }, 104 | }); 105 | } 106 | { 107 | const result = await graphql({ 108 | source: ` 109 | query { 110 | todos { 111 | title 112 | } 113 | } 114 | `, 115 | schema, 116 | contextValue, 117 | }); 118 | deepEqual(result, { 119 | data: { todos: [{ title: "Try graphql-ts" }] }, 120 | }); 121 | } 122 | })(); 123 | -------------------------------------------------------------------------------- /test-project/example.ts: -------------------------------------------------------------------------------- 1 | import { gWithContext } from "@graphql-ts/schema"; 2 | import { GraphQLSchema, graphql } from "graphql"; 3 | 4 | type Context = { 5 | loadPerson: (id: string) => Person | undefined; 6 | loadFriends: (id: string) => Person[]; 7 | }; 8 | const g = gWithContext(); 9 | type g = gWithContext.infer; 10 | 11 | type Person = { 12 | id: string; 13 | name: string; 14 | }; 15 | 16 | const Person: g> = g.object()({ 17 | name: "Person", 18 | fields: () => ({ 19 | id: g.field({ type: g.nonNull(g.ID) }), 20 | name: g.field({ type: g.nonNull(g.String) }), 21 | friends: g.field({ 22 | type: g.list(g.nonNull(Person)), 23 | resolve(source, _, context) { 24 | return context.loadFriends(source.id); 25 | }, 26 | }), 27 | }), 28 | }); 29 | 30 | const Query = g.object()({ 31 | name: "Query", 32 | fields: { 33 | person: g.field({ 34 | type: Person, 35 | args: { 36 | id: g.arg({ type: g.nonNull(g.ID) }), 37 | }, 38 | resolve(_, args, context) { 39 | return context.loadPerson(args.id); 40 | }, 41 | }), 42 | }, 43 | }); 44 | 45 | const schema = new GraphQLSchema({ 46 | query: Query, 47 | }); 48 | 49 | { 50 | const people = new Map([ 51 | ["1", { id: "1", name: "Alice" }], 52 | ["2", { id: "2", name: "Bob" }], 53 | ]); 54 | const friends = new Map([ 55 | ["1", ["2"]], 56 | ["2", ["1"]], 57 | ]); 58 | const contextValue: Context = { 59 | loadPerson: (id) => people.get(id), 60 | loadFriends: (id) => { 61 | return (friends.get(id) ?? []) 62 | .map((id) => people.get(id)) 63 | .filter((person) => person !== undefined) as Person[]; 64 | }, 65 | }; 66 | graphql({ 67 | source: ` 68 | query { 69 | person(id: "1") { 70 | id 71 | name 72 | friends { 73 | id 74 | name 75 | } 76 | } 77 | } 78 | `, 79 | schema, 80 | contextValue, 81 | }).then((result) => { 82 | console.log(result); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /test-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphql-ts/test-project", 3 | "version": "1.0.13", 4 | "private": true, 5 | "repository": "https://github.com/Thinkmill/graphql-ts/tree/main/test-project", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@graphql-ts/extend": "workspace:^", 9 | "@graphql-ts/schema": "workspace:^", 10 | "@types/node": "^22.13.10", 11 | "graphql": "^16.3.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test-project/runtime.test.ts: -------------------------------------------------------------------------------- 1 | import { gWithContext } from "@graphql-ts/schema"; 2 | import { GraphQLSchema, graphql } from "graphql"; 3 | 4 | const g = gWithContext(); 5 | type g = gWithContext.infer; 6 | 7 | test("a basic schema works", async () => { 8 | const Query = g.object()({ 9 | name: "Query", 10 | fields: { 11 | hello: g.field({ 12 | type: g.String, 13 | resolve() { 14 | return "something"; 15 | }, 16 | }), 17 | }, 18 | }); 19 | const graphQLSchema = new GraphQLSchema({ 20 | query: Query, 21 | }); 22 | const result = await graphql({ 23 | schema: graphQLSchema, 24 | source: "query { hello }", 25 | }); 26 | expect(result).toEqual({ data: { hello: "something" } }); 27 | }); 28 | -------------------------------------------------------------------------------- /test-project/simple-example.ts: -------------------------------------------------------------------------------- 1 | import { gWithContext } from "@graphql-ts/schema"; 2 | import { GraphQLSchema, graphql } from "graphql"; 3 | 4 | type Context = {}; 5 | 6 | const g = gWithContext(); 7 | type g = gWithContext.infer; 8 | 9 | const Query = g.object()({ 10 | name: "Query", 11 | fields: { 12 | hello: g.field({ 13 | type: g.String, 14 | resolve() { 15 | return "Hello!"; 16 | }, 17 | }), 18 | }, 19 | }); 20 | 21 | const schema = new GraphQLSchema({ 22 | query: Query, 23 | }); 24 | 25 | graphql({ 26 | source: ` 27 | query { 28 | hello 29 | } 30 | `, 31 | schema, 32 | }).then((result) => { 33 | console.log(result); 34 | }); 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "NodeNext", 5 | "noEmit": true, 6 | "isolatedModules": true, 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "jsx": "preserve", 11 | "skipLibCheck": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "declaration": true, 14 | "declarationMap": true 15 | }, 16 | "exclude": ["examples/**", "site/**", "**/node_modules/**"] 17 | } 18 | --------------------------------------------------------------------------------