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

Documentation updates #1612

Merged
merged 4 commits into from
Dec 19, 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
27 changes: 24 additions & 3 deletions example/collections.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createGraphQLError } from 'graphql-yoga';

const pizzas = [
{ id: 1, dough: 'pan', toppings: ['cheese'] },
{ id: 2, dough: 'classic', toppings: ['ham'] },
Expand Down Expand Up @@ -43,7 +45,17 @@ export const UsersCollection = {
get(id: string | number) {
const uid = typeof id === 'string' ? parseInt(id, 10) : id;

return users.find((u) => u.id === uid);
const user = users.find((u) => u.id === uid);
if (!user) {
return createGraphQLError('User not found', {
extensions: {
http: {
status: 404,
},
},
});
}
return user;
},
all() {
return users;
Expand All @@ -53,8 +65,17 @@ export const UsersCollection = {
export const BooksCollection = {
get(id: string | number) {
const bid = typeof id === 'string' ? parseInt(id, 10) : id;

return books.find((u) => u.id === bid);
const book = books.find((u) => u.id === bid);
if (!book) {
return createGraphQLError('Book not found', {
extensions: {
http: {
status: 404,
},
},
});
}
return book;
},
all() {
return books;
Expand Down
20 changes: 10 additions & 10 deletions tests/repro.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,16 @@ test('error extensions', async () => {
resolvers: {
Query: {
me: () => {
throw new GraphQLError("account not found", {
throw new GraphQLError('account not found', {
extensions: {
code: "ACCOUNT_NOT_FOUND",
http: { status: 404 }
}
code: 'ACCOUNT_NOT_FOUND',
http: { status: 404 },
},
});
},
},
}
})
},
}),
});

for (let i = 0; i < 10; i++) {
Expand All @@ -159,9 +159,9 @@ test('error extensions', async () => {
extensions: {
code: 'ACCOUNT_NOT_FOUND',
},
path: ['me']
}
]
path: ['me'],
},
],
});
}
})
});
190 changes: 187 additions & 3 deletions website/src/pages/docs/api/error-handler.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,197 @@ api.use(
'/api',
useSofa({
schema,
errorHandler(errs) {
logErrors(errors);
return new Response(formatError(errs[0]), {
// `errors` is the array containing the `Error` objects
errorHandler(errors) {
for (const error of errors) {
console.error(`Error: ${error.message}`);
}
return new Response(errs[0].message, {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
},
})
);
```

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's nice
can you also on the top section show an example of the default response response that you'll get if you don't add anything and there is an error?

and also for each example - can you also show the actual full response that would be sent?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about now?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!
I would also add the full json itself, easier to read

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added more details including the expected response fully.

By default, it always returns a response with `200` if the request is valid.
If the request is invalid, it returns a response with `400` status code and the error message.

```ts
const res = await fetch('http://localhost:4000/api/createUser', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 1, // Invalid name
}),
});

console.log(res.status); // 400
const data = await res.json();
console.log(data); // {"errors":[{"message":"Expected type String, found 1."}]}
```

## HTTP Error Extensions

Just like GraphQL Yoga's [error handling](https://the-guild.dev/graphql/yoga-server/docs/features/error-masking#modifying-http-status-codes-and-headers) , SOFA respects the status code and headers provided in the error extensions.

```ts filename="GraphQL Error with http extensions." {6-11}
throw new GraphQLError(
`User with id '${args.byId}' not found.`,
// error extensions
{
extensions: {
http: {
status: 400,
headers: {
'x-custom-header': 'some-value',
},
},
},
}
);
```

In this case, you returns a response with `400` status code and `x-custom-header` in the response headers.

Let's say you have a simple GraphQL API like below;

```ts
import { createServer } from 'node:http';
import { useSofa } from 'sofa-api';
import { makeExecutableSchema } from '@graphql-tools/schema';

createServer(
useSofa({
basePath: '/api',
schema: makeExecutableSchema({
typeDefs: /* GraphQL */ `
type Query {
posts: [Post!]!
}
type Post {
id: ID!
title: String!
secret: String!
}
`,
resolvers: {
Query: {
posts() {
return getPosts();
},
},
Post: {
async secret(_, __, { request }) {
const authHeader = request.headers.get('Authorization');
if (!authHeader) {
throw new GraphQLError('Unauthorized', {
extensions: {
http: {
status: 401,
headers: {
'WWW-Authenticate': 'Bearer',
},
},
},
});
}
const [type, token] = authHeader.split(' ');
if (type !== 'Bearer') {
throw new GraphQLError('Invalid token type', {
extensions: {
http: {
status: 401,
headers: {
'WWW-Authenticate': 'Bearer',
},
},
},
});
}
if (token !== 'secret') {
throw new GraphQLError('Invalid token', {
extensions: {
http: {
status: 401,
headers: {
'WWW-Authenticate': 'Bearer',
},
},
},
});
}
return 'Secret value';
},
},
},
}),
})
).listen(4000);
```

In this case if you make a request to `/api/posts` without a valid `Authorization` header,
you will get a response with `401` status code and `WWW-Authenticate` in the response headers.
But the response body will contain the data and errors.

```ts
const res = await fetch('http://localhost:4000/api/posts');
console.log(res.status); // 401
console.log(res.headers.get('WWW-Authenticate')); // Bearer
const data = await res.json();
expect(data).toEqual({
data: {
posts: [
{
id: '1',
title: 'Post 1',
secret: null,
},
{
id: '2',
title: 'Post 2',
secret: null,
},
],
},
errors: [
{
message: 'Unauthorized',
path: ['posts', 'secret'],
},
],
});
```

In this case only errored fields will be `null` in the response body.

However if you make a request to `/api/me` with `x-user-id` header, you will get a response with `200` status code and `x-custom-header` in the response headers.

```ts
const res = await fetch('http://localhost:4000/api/posts', {
headers: {
Authorization: 'Bearer secret',
},
});
console.log(res.status); // 200
const data = await res.json();
expect(data).toEqual({
data: {
posts: [
{
id: '1',
title: 'Post 1',
secret: 'Secret value',
},
{
id: '2',
title: 'Post 2',
secret: 'Secret value',
},
],
},
});
```
64 changes: 48 additions & 16 deletions website/src/pages/docs/recipes/open-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,55 @@ Thanks to GraphQL's Type System Sofa is able to generate OpenAPI (Swagger) defin
```ts
import { useSofa } from 'sofa-api';

app.use(
'/api',
useSofa({
schema,
basePath: '/',
openAPI: {
info: {
title: 'Example API',
version: '3.0.0',
}
endpoint: '/openapi.json',
},
swaggerUI: {
path: '/docs',
useSofa({
schema,
basePath: '/api',
openAPI: {
info: {
title: 'Example API',
version: '3.0.0',
}
})
);
endpoint: '/openapi.json',
},
swaggerUI: {
path: '/docs',
}
})
```

> You can find swagger definitions in `/api/openapi.json` route.

## Extending OpenAPI

If you want to extend your OpenAPI document with [security schemes etc](https://swagger.io/docs/specification/v3_0/authentication/).
You can use `openAPI` option like below;

```ts
import { useSofa } from 'sofa-api';

useSofa({
schema,
basePath: '/api',
openAPI: {
info: {
title: 'Example API',
version: '3.0.0',
}
endpoint: '/openapi.json',
servers: [{ url: 'https://my-production.com', description: 'Production' }],
components: {
securitySchemes: {
bearerAuthorization: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
name: 'Authorization',
},
},
},
},
swaggerUI: {
path: '/docs',
}
})
```
Loading