Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: raw typed #1021

Merged
merged 10 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion DOCUMENTATION_NEXT.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# GraphQL Request Documentation
# Graffle Documentation

# Output

This section covers the ways you can control the return types of methods in a Graffle instance.

The standard GraphQL execution result type in the JavaScript ecosystem (from the `graphql` package) has roughly this type:

```ts
Expand Down Expand Up @@ -111,6 +113,106 @@ assertType<
>(await graffle.query.foo())
```

# Raw

Raw methods allow you to work directly with GraphQL queries and data types. They have [`...OrThrow`](#orthrow) variants like other methods. They force you to use the [envelope](#envelope), however your configuration for [error channels](#errors) is still honoured.

> Aside: These methods are approximately what `graphql-request` was before it turned into Graffle.

## DocumentNode Document

Use the `gql` template tag to get syntax highlighting (GraphQL editor extensions special case this template tag name) and construction of `TypedQueryDocumentNode`s from strings:

Example ([see full runnable example](./examples/raw.ts)):

```ts
import { gql } from 'graffle/utils'

const document = gql`
query MyThing {
stuff {
foo
bar
}
}
`

const result = await graffle.raw({ document })
```

### Type Safety

You can attain type safety by creating your document with type variables. In a typical real project this would be something that a tool like [GraphQL Code Generator automatically](https://the-guild.dev/graphql/codegen) does for you.

Example ([see full runnable example](./examples/raw-typed.ts)):

```ts
const document = gql<{ stuff: { foo: string; bar: number }, { filter: boolean } }>`
query MyThing ($filter:boolean) {
stuff (filter:$filter) {
foo
bar
}
}
`

const result = await graffle.raw({
document,
// Correctly typed variables now required.
variables: {
filter: true,
}
})
```

## String Document

You can skip creating document nodes if you need or wish by using `.rawString`.

> Aside: During interface design, using an overload to combine `.raw` and `.rawString` was at first used but soon abandoned because of the poor intellisense experience TypeScript overloads currently have in editors.

Example ([see full runnable example](./examples/rawString.ts)):

```ts
const document = `
query MyThing {
stuff (filter:$filter) {
foo
bar
}
}
`

const result = await graffle.rawString({ document })
```

### Type Safety

You can attain type safety by casting your document with `TypedDocumentString`. In a typical project your tooling (like GraphQL Code Generator) would do this for you.

Example ([see full runnable example](./examples/rawString-typed.ts)):

```ts
import { TypedDocumentString } from 'graffle/utils'

const document = `
query MyThing ($filter: boolean) {
stuff (filter: $filter) {
foo
bar
}
}
` as TypedDocumentString<{ stuff: { foo: string; bar: number }, { filter: boolean } }>

const result = await graffle.rawString({
document,
// Correctly typed variables now required.
variables: {
filter: true,
}
})
```

# Schema Errors

There is a GraphQL schema design pattern that advocates for encoding errors into your schema. It generally has two parts: One, objects that represent errors; Two, root fields that return unions of one success object and multiple error objects. The benefit of this approach is letting users know about error states and enabling clients to receive them in a type safe way. The general net positive is higher quality and easier to develop software.
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import configPrisma from 'eslint-config-prisma'
import tsEslint from 'typescript-eslint'

export default tsEslint.config({
ignores: ['**/build/**/*', 'eslint.config.js', 'vite.config.ts', '**/generated/**/*', '**/generated-clients/**/*'],
ignores: ['**/build/**/*', 'eslint.config.js', 'vite.config.ts', '**/generated/**/*', '**/$generated-clients/**/*'],
extends: configPrisma,
languageOptions: {
parserOptions: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ const ErrorObjectsTypeNameSelected = Object.values(ErrorObjectsTypeNameSelectedE
type ErrorObjectsTypeNameSelected = (typeof ErrorObjectsTypeNameSelected)[number]

export const isError = <$Value>(value: $Value): value is Include<$Value, ErrorObjectsTypeNameSelected> => {
return typeof value === 'object' && value !== null && '__typename' in value
return typeof value === `object` && value !== null && `__typename` in value
&& ErrorObjectsTypeNameSelected.some(_ => _.__typename === value.__typename)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Index } from './Index.js'
// -------

import { createSelect } from '../../../src/entrypoints/alpha/client.js'
export const Select = createSelect('default')
export const Select = createSelect(`default`)

// Buildtime
// ---------
Expand Down
3 changes: 3 additions & 0 deletions examples/$helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const publicGraphQLSchemaEndpoints = {
SocialStudies: `https://countries.trevorblades.com/graphql`,
}
12 changes: 12 additions & 0 deletions examples/arguments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable */
import { SocialStudies } from './$generated-clients/SocialStudies/__.js'

const socialStudies = SocialStudies.create()

const countries = await socialStudies.query.countries({
$: { filter: { name: { in: ['Canada', 'Germany', 'Japan'] } } },
name: true,
continent: { name: true },
})

console.log(countries)
6 changes: 4 additions & 2 deletions examples/config-fetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable */
import { SocialStudies } from './generated-clients/SocialStudies/__.js'
import { SocialStudies } from './$generated-clients/SocialStudies/__.js'

const socialStudies = SocialStudies.create()
.use({
Expand All @@ -17,6 +17,8 @@ const socialStudies = SocialStudies.create()

// todo $scalars does not work
// todo intelisense for $ doesn't work
const countries = await socialStudies.query.countries({ name: true })
const countries = await socialStudies.query.countries({
name: true,
})

console.log(countries)
2 changes: 1 addition & 1 deletion examples/config-http-headers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SocialStudies } from './generated-clients/SocialStudies/__.js'
import { SocialStudies } from './$generated-clients/SocialStudies/__.js'

const socialStudies = SocialStudies.create({
headers: {
Expand Down
2 changes: 1 addition & 1 deletion examples/legacy/typescript-typed-document-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { gql, GraphQLClient, request } from '../../src/entrypoints/main.js'

const client = new GraphQLClient(endpoint)

const query: TypedDocumentNode<{ greetings: string }, Record<any, never>> = parse(gql`
const query: TypedDocumentNode<{ greetings: string }> = parse(gql`
query greetings {
greetings
}
Expand Down
62 changes: 62 additions & 0 deletions examples/raw-typed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { TypedQueryDocumentNode } from 'graphql'
import { gql, Graffle } from '../src/entrypoints/alpha/main.js'
import { publicGraphQLSchemaEndpoints } from './$helpers.js'

const graffle = Graffle.create({
schema: publicGraphQLSchemaEndpoints.SocialStudies,
})

/*************************************** Variation 1 ***************************************
* -
* -
* -
* You can pass type variables to the `gql` template tag.
* -
*/

{
const document = gql<{ countries: { name: string; continent: { name: string } }[] }, { filter: string[] }>`
query countries ($filter: [String!]) {
countries (filter: { name: { in: $filter } }) {
name
continent {
name
}
}
}
`

const result = await graffle.raw({ document, variables: { filter: [`Canada`, `Germany`, `Japan`] } })

console.log(result.data?.countries)
}

/*************************************** Variation 2 ***************************************
* -
* -
* -
* You can also cast the type if you have a reference to a pre constructed type.
* -
*/

{
type Document = TypedQueryDocumentNode<
{ countries: { name: string; continent: { name: string } }[] },
{ filter: string[] }
>

const document: Document = gql`
query countries ($filter: [String!]) {
countries (filter: { name: { in: $filter } }) {
name
continent {
name
}
}
}
`

const result = await graffle.raw({ document, variables: { filter: [`Canada`, `Germany`, `Japan`] } })

console.log(result.data?.countries)
}
35 changes: 18 additions & 17 deletions examples/raw.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { gql, Graffle } from '../src/entrypoints/alpha/main.js'
import { publicGraphQLSchemaEndpoints } from './$helpers.js'

const graffle = Graffle.create({ schema: `https://countries.trevorblades.com/graphql` })
const graffle = Graffle.create({
schema: publicGraphQLSchemaEndpoints.SocialStudies,
})

// todo typed document node
// interface Data {
// countries: { name }[]
// }
// const { data } = await request<Data>(
const result = await graffle.raw({
document: gql`
query countries ($filter: [String!]) {
countries (filter: { name: { in: $filter } }) {
name
continent {
name
}
}
}
`,
variables: { filter: [`Canada`, `Germany`, `Japan`] },
})

const { data } = await graffle.rawOrThrow(
gql`
{
countries {
name
}
}
`,
)

console.log(data)
console.log(result.data)
37 changes: 37 additions & 0 deletions examples/rawString-typed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Graffle } from '../src/entrypoints/alpha/main.js'
// todo from '../src/entrypoints/alpha/utils.js'
import type { TypedDocumentString } from '../src/layers/0_functions/types.js'
import { publicGraphQLSchemaEndpoints } from './$helpers.js'

const graffle = Graffle.create({
schema: publicGraphQLSchemaEndpoints.SocialStudies,
})

/**
* @remarks Typically this type would come from your code generation tool.
*
* @see https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#documentmode
* @see https://github.com/jasonkuhrt/graphql-request/issues/997
*/
type Document = TypedDocumentString<
{ countries: { name: string; continent: { name: string } }[] },
{ filter: string[] }
>

const document: Document = /* gql */ `
query countries ($filter: [String!]) {
countries (filter: { name: { in: $filter } }) {
name
continent {
name
}
}
}
`

const result = await graffle.rawString({
document,
variables: { filter: [`Canada`, `Germany`, `Japan`] },
})

console.log(result.data?.countries)
20 changes: 20 additions & 0 deletions examples/rawString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Graffle } from '../src/entrypoints/alpha/main.js'
import { publicGraphQLSchemaEndpoints } from './$helpers.js'

const graffle = Graffle.create({
schema: publicGraphQLSchemaEndpoints.SocialStudies,
})

const document = /* gql */ `
{
countries {
name
}
}
`

const result = await graffle.rawString({
document,
})

console.log(result.data)
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@
"@types/express": "^4.17.21",
"@types/json-bigint": "^1.0.4",
"@types/node": "^22.1.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1",
"doctoc": "^2.2.1",
"dripip": "^0.10.0",
"es-toolkit": "^1.13.1",
Expand All @@ -131,14 +131,14 @@
"graphql-scalars": "^1.23.0",
"graphql-tag": "^2.12.6",
"graphql-upload-minimal": "^1.6.1",
"graphql-yoga": "^5.6.2",
"graphql-yoga": "^5.6.3",
"jsdom": "^24.1.1",
"json-bigint": "^1.0.0",
"publint": "^0.2.9",
"tsx": "^4.16.5",
"type-fest": "^4.23.0",
"typescript": "^5.5.4",
"typescript-eslint": "^8.0.0",
"typescript-eslint": "^8.0.1",
"vitest": "^2.0.5"
}
}
Loading