Skip to content

Commit

Permalink
Add ways to customize error behavior (#494)
Browse files Browse the repository at this point in the history
* document getting started with vanilla svelte and vite

* add config.quietQueryErrors

* add onError hook

* add test for onError

* schema plugin shouldn't crash with failures

* remove relative path hardcode

* document +server support

* changesets

* update snapshot

* tidy up PR template

* add regex group

* document quietQueryErrors
  • Loading branch information
AlecAivazis authored Aug 28, 2022
1 parent 74f62d0 commit 5573cfa
Show file tree
Hide file tree
Showing 24 changed files with 537 additions and 112 deletions.
5 changes: 5 additions & 0 deletions .changeset/tricky-mangos-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'houdini': patch
---

add onError hook
5 changes: 5 additions & 0 deletions .changeset/twenty-crabs-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'houdini': patch
---

add `quietQueryError` config value to supress all query errors
19 changes: 9 additions & 10 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
## Will fix
- #
Fixes #TICKET

### To help everyone out, please make sure your PR does the following:

- [] Update the first line to point to the ticket that this PR fixes
- [] Add a message that clearly describes the fix
- [] If applicable, add a test that would fail without this fix
- [] Make sure the unit and integration tests pass locally with `pnpm run tests` and `cd integration && pnpm run tests`
- [] Includes a changeset if your fix affects the user with `pnpm changeset`

## Some highlights
- [ ] New tests are passing
- [ ]
- [ ]
- [ ] New settings are available
- [ ]
- [ ]
- [ ] The doc has been updated
1 change: 1 addition & 0 deletions integration/src/lib/utils/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const routes = {
Plugin_query_component: '/plugin/query/component',
Plugin_query_beforeLoad: '/plugin/query/beforeLoad',
Plugin_query_afterLoad: '/plugin/query/afterLoad',
Plugin_query_onError: '/plugin/query/onError',
Plugin_query_layout: '/plugin/query/layout',

Plugin_mutation_mutation: '/plugin/mutation/mutation',
Expand Down
8 changes: 8 additions & 0 deletions integration/src/routes/plugin/query/onError/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import type { PageData } from './$houdini';
export let data: PageData;
</script>

<div id="result">
{data.fancyMessage}
</div>
16 changes: 16 additions & 0 deletions integration/src/routes/plugin/query/onError/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { graphql } from '$houdini';
import type { OnErrorEvent } from './$houdini';

export const houdini_load = graphql`
query PreprocessorOnErrorTestQuery {
user(id: "1000", snapshot: "preprocess-on-error-test-simple") {
name
}
}
`;

export const onError = ({ error, input }: OnErrorEvent) => {
return {
fancyMessage: 'hello'
};
};
11 changes: 11 additions & 0 deletions integration/src/routes/plugin/query/onError/spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { test } from '@playwright/test';
import { routes } from '../../../../lib/utils/routes.js';
import { expectToBe, goto } from '../../../../lib/utils/testsHelper.js';

test.describe('query preprocessor', () => {
test('onError hook', async ({ page }) => {
await goto(page, routes.Plugin_query_onError);

await expectToBe(page, 'hello');
});
});
1 change: 1 addition & 0 deletions site/src/routes/api/config.svx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ It can contain the following values:
- `schemaPollHeaders` (optional): An object specifying the headers to use when pulling your schema. Keys of the object are header names and its values can be either a string pointing to the environment variable to use as the header value or a function that takes the current `process.env` and returns the the value to use.
- `schemaPollInterval` (optional, default: `2000`): Configures the schema polling behavior for the kit plugin. If its value is greater than `0`, the plugin will poll the set number of milliseconds. If set to `0`, the plugin will only pull the schema when you first run `dev`. If you set to `null`, the plugin will never look for schema changes. You can see use the [pull-schema command] to get updates.
- `globalStorePrefix` (optional, default: `GQL_`): The default prefix of your global stores. This lets your editor provide autocompletion with just a few characters.
- `quietQueryErrors` (optional, default: `false`): With this enabled, errors in your query will not be thrown as exceptions. You will have to handle error state in your route components or by hand in your load (or the onError hook)

## Custom Scalars

Expand Down
26 changes: 24 additions & 2 deletions site/src/routes/api/routes.svx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ A query store holds an object with the following fields that accessed like `$sto

- `data` contains the result of the query. It's value will update as mutations, subscriptions, and other queries provide more recent information.
- `loading` contains the loading state (`true` or `false`) for a query found outside of a route component (ie, not defined in `src/routes`)
- `errors` contains any error values that occur for a query found outside of a route component (ie, not defined in `src/routes`)
- `errors` contains any error values that occur for a query found outside of a route component (ie, not defined in `src/routes`). If you want to use this for managing your errors, you should enable the [quietQueryError](/api/config) configuration option.
- `partial` contains a boolean that indicates if the result has a partial match

### Methods
Expand Down Expand Up @@ -239,6 +239,28 @@ export function afterLoad({ data }) {
}
```

#### `onError`

If defined, the load function will invoked this function instead of throwing an error when an error is received.
It receives three inputs: the load event, the inputs for each query, and the error that was encountered. Just like
the other hooks, `onError` can return an object that provides props to the route.

```javascript
// src/routes/myRoute/+page.js

export const houdini_load = graphql`
query MyProfile {
profile {
name
}
}
`

export function onError({ error }) {
throw this.redirect(307, '/login')
}
```

### Disabling Inline Query Loading

Sometimes it's useful to have a lazy query that only fires when you call `fetch` (for
Expand Down Expand Up @@ -298,7 +320,7 @@ export async function load(event) {
}
```

<DeepDive title="Loading multiple stores simulatenously">
<DeepDive title="Loading multiple stores simultaneously">

Be careful when loading multiple stores at once.

Expand Down
13 changes: 12 additions & 1 deletion site/src/routes/guides/faq.svx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ Yep! You can use queries or any document anywhere you can use a svelte store. Ju
Yes! You'll just have to rely on the store apis for your documents and write your route's loads manually.
For more information on using your document's stores check out [Working with GraphQL](/guides/working-with-graphql) guide.

### Are `+server` files supported?

Unforunately, `+server` have additional constraints that make them a unique challenge to support (not impossible, just tricky). This is also an area of SvelteKit that is
rapidly changing and so we are opting to not officially support `+server` files until things stabilize a bit more. Sorry for the inconvinience!

### What's the best way to build a Full-Stack application with SvelteKit?

Simple answer, we recommend [KitQL](https://www.kitql.dev/). It gives you everything you need when building a full-stack application with GraphQL (including [Houdini](https://www.houdinigraphql.com/) of course 😉). For more information about our collaboration, head over to this [blog post](https://www.the-guild.dev/blog/houdini-and-kitql).

### How does the plugin generate loads?

Curious how this works under the hook? Consider this query:
Consider this query:

```svelte
<!-- src/routes/myRoute/+page.svelte -->
Expand Down Expand Up @@ -83,6 +88,12 @@ If you haven't seen Houdini's document stores before, please check out the [Work
You should use the `metadata` parameter in the document store to pass arbitrary information into your client's network function. For more information,
please visit [the query store docs](/api/query/store#passing-metadata).

### What is this `Generated an empty chunk...` warning from vite?

If you have a route directory that does not contain a `+page.js` file and do not have an inline query in your `+page.svelte`, the
plugin will generate an empty `+page.js` file that is never used. This confuses vite hence the warning. Don't worry - this empty
file is never imported if you don't have an inline query so there's no performance implication.

### My IDE is complaining that the internal directives and fragments don't exist.

Every plugin and editor is different so we can't give you an exact answer but Houdini will write a file inside of the `$houdini` directory that contains all of the custom definitions that it relies on. Be default this file is located at `$houdini/graphql/schema.graphql` and `$houdini/graphql/documents.gql`. You can configure this value in your [config file](/api/config) under the `definitionsPath` value.
Expand Down
12 changes: 12 additions & 0 deletions site/src/routes/guides/migrating-to-016.svx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,18 @@ the store itself and do not need the `$`:
</button>
```

`afterLoad` hooks also now take the whole load event as a single `event` parameter:

```diff
- export async function afterLoad({ params, data }) {
+ export async function afterLoad({ event, data }) {
return {
- message: params.message
+ message: event.params.message
}
}
```

### 2.b Inline Fragments

The order for the arguments to inline fragments has been inverted.
Expand Down
26 changes: 20 additions & 6 deletions site/src/routes/guides/setting-up-your-project.svx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,26 @@ this, add the following block of code to `src/routes/+layout.svelte` and any nam

### Svelte

While your exact situation might change, somehow you should import houdini's preprocessor and pass it to your
svelte config
If you are building a vanilla svelte project, you will have to configure the compiler and preprocessor to generate the correct logic by setting the `framework`
field in your config file to `"svelte"`.

If you are using vite, you should use the `houdini/vite` plugin even if
you aren't using kit. Update your `vite.config.js` file to look like this:

```javascript
// vite.config.js

import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import houdini from 'houdini/vite'

export default defineConfig({
plugins: [houdini(), svelte()]
})
```

If you aren't using vite, it's a lot harder to give an exact recommendation but somehow
you should import houdini's preprocessor and pass it to your svelte config

```javascript
// svelte.config.js
Expand All @@ -105,10 +123,6 @@ export default {
}
```

You also have to configure the compiler and preprocessor to generate the correct logic by setting the `framework`
field in your config file to `"svelte"`. If you are using vite, you should use the `houdini/vite` plugin even if
you aren't using kit.

Please keep in mind that returning the response from a query, you should not rely on `this.redirect` to handle the
redirect as it will update your browsers `location` attribute, causing a hard transition to that url. Instead, you should
use `this.error` to return an error and handle the redirect in a way that's appropriate for your application.
Expand Down
18 changes: 15 additions & 3 deletions src/cmd/generators/kit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default async function svelteKitGenerator(config: Config, docs: Collected

const afterLoad = scriptExports.includes('afterLoad')
const beforeLoad = scriptExports.includes('beforeLoad')
const onError = scriptExports.includes('onError')

// we need to create a typescript file that has a definition of the variable and hook functions
const typeDefs = `import type * as Kit from '@sveltejs/kit';
Expand Down Expand Up @@ -106,7 +107,7 @@ type AfterLoadData = {
.join(', \n')}
}
type AfterLoadInput = {
type LoadInput = {
${queries
.filter((query) => query.variableDefinitions?.length)
.map((query) => {
Expand All @@ -122,7 +123,7 @@ type AfterLoadInput = {
export type AfterLoadEvent = {
event: PageLoadEvent
data: AfterLoadData
input: AfterLoadInput
input: LoadInput
}
`
: ''
Expand All @@ -138,7 +139,16 @@ type BeforeLoadReturn = ReturnType<typeof import('./+page').beforeLoad>;
`
: ''
}
${
onError
? `
export type OnErrorEvent = { event: LoadEvent, input: LoadInput, error: Error | Error[] }
type OnErrorReturn = ReturnType<typeof import('./+page').onError>;
`
: ''
}
export type PageData = {
${queries
Expand All @@ -148,7 +158,9 @@ export type PageData = {
return [name, name + config.storeSuffix].join(': ')
})
.join(', \n')}
} ${afterLoad ? '& AfterLoadReturn ' : ''} ${beforeLoad ? '& BeforeLoadReturn ' : ''}
} ${afterLoad ? '& AfterLoadReturn ' : ''} ${beforeLoad ? '& BeforeLoadReturn ' : ''} ${
onError ? '& OnErrorReturn ' : ''
}
`

Expand Down
72 changes: 70 additions & 2 deletions src/cmd/generators/kit/kit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,14 @@ test('generates types for after load', async function () {
MyPageLoad2Query: MyPageLoad2Query$result
};
type AfterLoadInput = {
type LoadInput = {
MyPageLoad1Query: MyPageLoad1Query$input
};
export type AfterLoadEvent = {
event: PageLoadEvent,
data: AfterLoadData,
input: AfterLoadInput
input: LoadInput
};
export type PageData = {
Expand All @@ -164,3 +164,71 @@ test('generates types for after load', async function () {
} & AfterLoadReturn;
`)
})

test('generates types for onError', async function () {
// create the mock filesystem
await fs.mock({
[config.routesDir]: {
myProfile: {
'+page.js': `
import { graphql } from '$houdini'
const store1 = graphql\`query MyPageLoad1Query($id: ID!) {
viewer(id: $id) {
id
}
}\`
const store2 = graphql\`query MyPageLoad2Query {
viewer {
id
}
}\`
export const houdini_load = [ store1, store2 ]
export function onError() {
return {
hello: 'world'
}
}
`,
},
},
})

// execute the generator
await runPipeline(config, [])

// load the contents of the file
const queryContents = await fs.readFile(
path.join(config.typeRouteDir, 'myProfile', '$houdini.d.ts')
)
expect(queryContents).toBeTruthy()

// verify contents
expect((await parseJS(queryContents!))?.script).toMatchInlineSnapshot(`
import type * as Kit from "@sveltejs/kit";
import type { VariableFunction, AfterLoadFunction, BeforeLoadFunction } from "../../../../runtime/lib/types";
import type { PageLoadEvent, PageData as KitPageData } from "./$types";
import { MyPageLoad1Query$result, MyPageLoad1Query$input } from "../../../../artifacts/MyPageLoad1Query";
import { MyPageLoad1QueryStore } from "../../../../stores/MyPageLoad1Query";
import { MyPageLoad2Query$result, MyPageLoad2Query$input } from "../../../../artifacts/MyPageLoad2Query";
import { MyPageLoad2QueryStore } from "../../../../stores/MyPageLoad2Query";
type Params = PageLoadEvent["params"];
export type MyPageLoad1QueryVariables = VariableFunction<Params, MyPageLoad1Query$input>;
export type OnErrorEvent = {
event: LoadEvent,
input: LoadInput,
error: Error | Error[]
};
type OnErrorReturn = ReturnType<typeof import("./+page").onError>;
export type PageData = {
MyPageLoad1Query: MyPageLoad1QueryStore,
MyPageLoad2Query: MyPageLoad2QueryStore
} & OnErrorReturn;
`)
})
Loading

0 comments on commit 5573cfa

Please sign in to comment.