houdini 0.13.0 β 0.13.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1145
- package/build/cmd/generators/artifacts/selection.js +7 -1
- package/build/cmd/generators/typescript/addReferencedInputTypes.js +2 -1
- package/build/cmd/generators/typescript/index.js +1 -1
- package/build/cmd.js +10 -3
- package/build/runtime/cache/cache.d.ts +12 -4
- package/build/runtime/cache/cache.js +185 -159
- package/build/runtime/cache/lists.js +1 -12
- package/build/runtime/mutation.js +1 -3
- package/build/runtime/network.d.ts +10 -2
- package/build/runtime/network.js +43 -57
- package/build/runtime/pagination.js +41 -36
- package/build/runtime/query.d.ts +5 -1
- package/build/runtime/query.js +29 -22
- package/build/runtime/types.d.ts +2 -0
- package/build/runtime-cjs/cache/cache.d.ts +12 -4
- package/build/runtime-cjs/cache/cache.js +185 -159
- package/build/runtime-cjs/cache/lists.js +1 -12
- package/build/runtime-cjs/mutation.js +1 -3
- package/build/runtime-cjs/network.d.ts +10 -2
- package/build/runtime-cjs/network.js +43 -57
- package/build/runtime-cjs/pagination.js +41 -36
- package/build/runtime-cjs/query.d.ts +5 -1
- package/build/runtime-cjs/query.js +29 -22
- package/build/runtime-cjs/types.d.ts +2 -0
- package/build/runtime-esm/cache/cache.d.ts +12 -4
- package/build/runtime-esm/cache/cache.js +115 -87
- package/build/runtime-esm/cache/list.d.ts +35 -0
- package/build/runtime-esm/cache/list.js +203 -0
- package/build/runtime-esm/cache/lists.js +1 -12
- package/build/runtime-esm/cache/record.d.ts +40 -0
- package/build/runtime-esm/cache/record.js +195 -0
- package/build/runtime-esm/mutation.js +1 -3
- package/build/runtime-esm/network.d.ts +10 -2
- package/build/runtime-esm/network.js +39 -35
- package/build/runtime-esm/pagination.js +20 -8
- package/build/runtime-esm/query.d.ts +5 -1
- package/build/runtime-esm/query.js +23 -14
- package/build/runtime-esm/types.d.ts +2 -0
- package/cmd/generators/artifacts/artifacts.test.ts +2 -0
- package/cmd/generators/artifacts/pagination.test.ts +5 -0
- package/cmd/generators/artifacts/selection.ts +8 -1
- package/cmd/generators/typescript/addReferencedInputTypes.ts +4 -1
- package/cmd/generators/typescript/index.ts +5 -3
- package/cmd/generators/typescript/typescript.test.ts +19 -19
- package/cmd/transforms/paginate.test.ts +3 -0
- package/package.json +3 -3
- package/runtime/cache/cache.ts +140 -103
- package/runtime/cache/lists.ts +1 -14
- package/runtime/cache/tests/availability.test.ts +84 -29
- package/runtime/cache/tests/gc.test.ts +15 -10
- package/runtime/cache/tests/list.test.ts +12 -12
- package/runtime/cache/tests/readwrite.test.ts +1024 -0
- package/runtime/cache/tests/scalars.test.ts +5 -5
- package/runtime/cache/tests/subscriptions.test.ts +235 -745
- package/runtime/mutation.ts +1 -3
- package/runtime/network.ts +50 -38
- package/runtime/pagination.ts +30 -5
- package/runtime/query.ts +38 -23
- package/runtime/types.ts +2 -0
package/README.md
CHANGED
|
@@ -11,523 +11,18 @@
|
|
|
11
11
|
<br />
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
-
If you are interested in helping out, the [contributing guide](./CONTRIBUTING.md) should provide some guidance. If you need something more
|
|
15
|
-
specific, feel free to reach out to @AlecAivazis on the Svelte discord. There's lots to do regardless of how deep you want to dive π
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
## β¨ Features
|
|
19
|
-
|
|
20
|
-
- Composable and colocated data requirements for your components
|
|
21
|
-
- Normalized cache with declarative updates
|
|
22
|
-
- Generated types
|
|
23
|
-
- Subscriptions
|
|
24
|
-
- Support for SvelteKit and Sapper
|
|
25
|
-
- Pagination (cursors **and** offsets)
|
|
26
|
-
|
|
27
|
-
At its core, houdini seeks to enable a high quality developer experience
|
|
28
|
-
without compromising bundle size. Like Svelte, houdini shifts what is
|
|
29
|
-
traditionally handled by a bloated runtime into a compile step that allows
|
|
30
|
-
for the generation of an incredibly lean GraphQL abstraction for your application.
|
|
31
|
-
|
|
32
|
-
## π Table of Contents
|
|
33
|
-
|
|
34
|
-
1. [Example](#example)
|
|
35
|
-
1. [Installation](#installation)
|
|
36
|
-
1. [Configuring Your Application](#configuring-your-application)
|
|
37
|
-
1. [SvelteKit](#sveltekit)
|
|
38
|
-
1. [Sapper](#sapper)
|
|
39
|
-
1. [Svelte](#svelte)
|
|
40
|
-
1. [Config File](#config-file)
|
|
41
|
-
1. [Running the Compiler](#running-the-compiler)
|
|
42
|
-
1. [Fetching Data](#fetching-data)
|
|
43
|
-
1. [Query variables and page data](#query-variables-and-page-data)
|
|
44
|
-
1. [Loading State](#loading-state)
|
|
45
|
-
1. [Hooks](#load-hooks)
|
|
46
|
-
1. [Refetching Data](#refetching-data)
|
|
47
|
-
1. [Cache policy](#cache-policy)
|
|
48
|
-
1. [Data Retention](#data-retention)
|
|
49
|
-
1. [Changing default cache policy](#changing-default-cache-policy)
|
|
50
|
-
1. [What about load?](#what-about-load)
|
|
51
|
-
1. [Fragments](#fragments)
|
|
52
|
-
1. [Fragment Arguments](#fragment-arguments)
|
|
53
|
-
1. [Mutations](#mutations)
|
|
54
|
-
1. [Updating fields](#updating-fields)
|
|
55
|
-
1. [Lists](#lists)
|
|
56
|
-
1. [Insert](#inserting-a-record)
|
|
57
|
-
1. [Remove](#removing-a-record)
|
|
58
|
-
1. [Delete](#deleting-a-record)
|
|
59
|
-
1. [Conditionals](#conditionals)
|
|
60
|
-
1. [Subscriptions](#subscriptions)
|
|
61
|
-
1. [Configuring the WebSocket client](#configuring-the-websocket-client)
|
|
62
|
-
1. [Using graphql-ws](#using-graphql-ws)
|
|
63
|
-
1. [Using subscriptions-transport-ws](#using-subscriptions-transport-ws)
|
|
64
|
-
1. [Pagination](#%EF%B8%8Fpagination)
|
|
65
|
-
1. [Paginated Fragments](#paginated-fragments)
|
|
66
|
-
1. [Mutation Operations](#mutation-operations)
|
|
67
|
-
1. [Custom Scalars](#%EF%B8%8Fcustom-scalars)
|
|
68
|
-
1. [Authentication](#authentication)
|
|
69
|
-
1. [Persisted Queries](#persisted-queries)
|
|
70
|
-
1. [Notes, Constraints, and Conventions](#%EF%B8%8Fnotes-constraints-and-conventions)
|
|
71
|
-
|
|
72
|
-
## πΉ Example
|
|
73
|
-
|
|
74
|
-
A demo can be found in the <a href='./example'>example directory</a>.
|
|
75
|
-
|
|
76
|
-
Please note that the examples in that directory and this readme showcase the typescript definitions
|
|
77
|
-
generated by the compiler. While it is highly recommended, Typescript is NOT required in order to use houdini.
|
|
78
|
-
|
|
79
|
-
## β‘ Installation
|
|
80
|
-
|
|
81
|
-
houdini is available on npm.
|
|
82
|
-
|
|
83
|
-
```sh
|
|
84
|
-
yarn add -D houdini houdini-preprocess
|
|
85
|
-
# or
|
|
86
|
-
npm install --save-dev houdini houdini-preprocess
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## π§ Configuring Your Application
|
|
90
|
-
|
|
91
|
-
Adding houdini to an existing project can easily be done with the provided command-line tool.
|
|
92
|
-
If you don't already have an existing app, visit [this link](https://kit.svelte.dev/docs)
|
|
93
|
-
for help setting one up. Once you have a project and want to add houdini, execute the following command which will create a few necessary files, as well as pull down a json
|
|
94
|
-
representation of your API's schema.
|
|
95
|
-
|
|
96
|
-
```sh
|
|
97
|
-
npx houdini init
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
> This will send a request to your API to download your schema definition. If you need
|
|
101
|
-
> headers to authenticate this request, you can pass them in with the `--pull-header`
|
|
102
|
-
> flag (abbreviated `-ph`). For example,
|
|
103
|
-
> `npx houdini init -ph Authorization="Bearer MyToken"`.
|
|
104
|
-
> You will also need to provide the same flag to `generate` when using the
|
|
105
|
-
> `--pull-schema` flag.
|
|
106
|
-
|
|
107
|
-
Finally, follow the steps appropriate for your framework.
|
|
108
|
-
|
|
109
|
-
### SvelteKit
|
|
110
|
-
|
|
111
|
-
We need to define an alias so that your codebase can import the generated runtime. Add the following
|
|
112
|
-
values to `svelte.config.js`:
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
import houdini from 'houdini-preprocess'
|
|
116
|
-
|
|
117
|
-
{
|
|
118
|
-
preprocess: [houdini()],
|
|
119
|
-
|
|
120
|
-
kit: {
|
|
121
|
-
vite: {
|
|
122
|
-
resolve: {
|
|
123
|
-
alias: {
|
|
124
|
-
$houdini: path.resolve('.', '$houdini')
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
server: {
|
|
128
|
-
fs: {
|
|
129
|
-
allow: ['.'],
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
And finally, we need to configure our application to use the generated network layer. To do
|
|
138
|
-
this, add the following block of code to `src/routes/__layout.svelte`:
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
<script context="module">
|
|
142
|
-
import env from '../environment';
|
|
143
|
-
import { setEnvironment } from '$houdini';
|
|
144
|
-
|
|
145
|
-
setEnvironment(env);
|
|
146
|
-
</script>
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
You might need to generate your runtime in order to fix typescript errors.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
**Note**: If you are building your application with
|
|
153
|
-
[`adapter-static`](https://github.com/sveltejs/kit/tree/master/packages/adapter-static) (or any other adapter that turns
|
|
154
|
-
your application into a static site), you will need to set the `static` value in your config file to `true`.
|
|
155
|
-
|
|
156
|
-
### Sapper
|
|
157
|
-
|
|
158
|
-
You'll need to add the preprocessor to both your client and your server configuration:
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
import houdini from 'houdini-preprocess'
|
|
162
|
-
|
|
163
|
-
// add to both server and client configurations
|
|
164
|
-
{
|
|
165
|
-
plugins: [
|
|
166
|
-
svelte({
|
|
167
|
-
preprocess: [houdini()],
|
|
168
|
-
}),
|
|
169
|
-
]
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
With that in place, the only thing left to configure your Sapper application is to connect your client and server to the generate network layer:
|
|
174
|
-
|
|
175
|
-
```typescript
|
|
176
|
-
// in both src/client.js and src/server.js
|
|
177
|
-
|
|
178
|
-
import { setEnvironment } from '$houdini'
|
|
179
|
-
import env from './environment'
|
|
180
|
-
|
|
181
|
-
setEnvironment(env)
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Svelte
|
|
185
|
-
|
|
186
|
-
If you are working on an application that isn't using SvelteKit or Sapper, you have to configure the
|
|
187
|
-
compiler and preprocessor to generate the correct logic by setting the `framework` field in your
|
|
188
|
-
config file to `"svelte"`.
|
|
189
|
-
|
|
190
|
-
Please keep in mind that returning the response from a query, you should not rely on `this.redirect` to handle the
|
|
191
|
-
redirect as it will update your browsers `location` attribute, causing a hard transition to that url. Instead, you should
|
|
192
|
-
use `this.error` to return an error and handle the redirect in a way that's appropriate for your application.
|
|
193
|
-
|
|
194
|
-
## <img src="./.github/assets/cylon.gif" height="28px" /> Running the Compiler
|
|
195
|
-
|
|
196
|
-
The compiler is responsible for a number of things, ranging from generating the actual runtime
|
|
197
|
-
to creating types for your documents. Running the compiler can be done with npx or via a script
|
|
198
|
-
in `package.json` and needs to be run every time a GraphQL document in your source code changes:
|
|
199
|
-
|
|
200
|
-
```sh
|
|
201
|
-
npx houdini generate
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
The generated runtime can be accessed by importing `$houdini` anywhere in your application.
|
|
205
|
-
|
|
206
|
-
If you have updated your schema on the server, you can pull down the most recent schema before generating your runtime by using `--pull-schema` or `-p`:
|
|
207
|
-
```sh
|
|
208
|
-
npx houdini generate --pull-schema
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
I know this sounds like a huge burden but we have plans for removing this step from the developer experience, it's just not ready yet.
|
|
212
|
-
|
|
213
|
-
## π Config File
|
|
214
|
-
|
|
215
|
-
All configuration for your houdini application is defined in a single file that is imported by both the runtime and the
|
|
216
|
-
command-line tool. Because of this, you must make sure that any imports and logic are resolvable in both environments.
|
|
217
|
-
This means that if you rely on `process.env` or other node-specifics you will have to use a
|
|
218
|
-
[plugin](https://www.npmjs.com/package/vite-plugin-replace) to replace the expression with something that can run in the browser.
|
|
219
|
-
|
|
220
|
-
## π Fetching Data
|
|
221
|
-
|
|
222
|
-
Grabbing data from your API is done with the `query` function:
|
|
223
|
-
|
|
224
|
-
```svelte
|
|
225
|
-
<script lang="ts">
|
|
226
|
-
import { query, graphql, AllItems } from '$houdini'
|
|
227
|
-
|
|
228
|
-
// load the items
|
|
229
|
-
const { data } = query<AllItems>(graphql`
|
|
230
|
-
query AllItems {
|
|
231
|
-
items {
|
|
232
|
-
id
|
|
233
|
-
text
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
`)
|
|
237
|
-
</script>
|
|
238
|
-
|
|
239
|
-
{#each $data.items as item}
|
|
240
|
-
<div>{item.text}</div>
|
|
241
|
-
{/each}
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
### Query variables and page data
|
|
245
|
-
|
|
246
|
-
At the moment, query variables are declared as a function in the module context of your component.
|
|
247
|
-
This function must be named after your query and in a sapper application, it takes the same arguments
|
|
248
|
-
that are passed to the `preload` function described in the [Sapper](https://sapper.svelte.dev/docs#Pages)
|
|
249
|
-
documentation. In a SvelteKit project, this function takes the same arguments passed to the `load` function
|
|
250
|
-
described in the [SvelteKit](https://kit.svelte.dev/docs#Loading) docs. Regardless of the framework, you can return
|
|
251
|
-
the value from `this.error` and `this.redirect` in order to change the behavior of the response. Here is a
|
|
252
|
-
modified example from the [demo](./example):
|
|
253
|
-
|
|
254
|
-
```svelte
|
|
255
|
-
// src/routes/[filter].svelte
|
|
256
|
-
|
|
257
|
-
<script lang="ts">
|
|
258
|
-
import { query, graphql, AllItems } from '$houdini'
|
|
259
|
-
|
|
260
|
-
// load the items
|
|
261
|
-
const { data } = query<AllItems>(graphql`
|
|
262
|
-
query AllItems($completed: Boolean) {
|
|
263
|
-
items(completed: $completed) {
|
|
264
|
-
id
|
|
265
|
-
text
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
`)
|
|
269
|
-
</script>
|
|
270
|
-
|
|
271
|
-
<script context="module" lang="ts">
|
|
272
|
-
// This is the function for the AllItems query.
|
|
273
|
-
// Query variable functions must be named <QueryName>Variables.
|
|
274
|
-
export function AllItemsVariables(page): AllItems$input {
|
|
275
|
-
// make sure we recognize the value
|
|
276
|
-
if (!['active', 'completed'].includes(page.params.filter)) {
|
|
277
|
-
return this.error(400, "filter must be one of 'active' or 'completed'")
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
completed: page.params.filter === 'completed',
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
</script>
|
|
285
|
-
|
|
286
|
-
{#each $data.items as item}
|
|
287
|
-
<div>{item.text}</div>
|
|
288
|
-
{/each}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
### Loading State
|
|
292
|
-
|
|
293
|
-
The methods used for tracking the loading state of your queries changes depending
|
|
294
|
-
on the context of your component. For queries that live in routes (ie, in
|
|
295
|
-
`/src/routes/...`), the actual query happens in a `load` function as described
|
|
296
|
-
in [What about load?](#what-about-load). Because of this, the best way to track
|
|
297
|
-
if your query is loading is to use the
|
|
298
|
-
[navigating store](https://kit.svelte.dev/docs#modules-$app-stores) exported from `$app/stores`:
|
|
299
14
|
|
|
300
15
|
```svelte
|
|
301
|
-
// src/routes/index.svelte
|
|
302
|
-
|
|
303
|
-
<script>
|
|
304
|
-
import { query } from '$houdini'
|
|
305
|
-
import { navigating } from '$app/stores'
|
|
306
|
-
|
|
307
|
-
const { data } = query(...)
|
|
308
|
-
</script>
|
|
309
|
-
|
|
310
|
-
{#if $navigating}
|
|
311
|
-
loading...
|
|
312
|
-
{:else}
|
|
313
|
-
data is loaded!
|
|
314
|
-
{/if}
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
However, since queries inside of non-route components (ie, ones that are not defined in `/src/routes/...`)
|
|
318
|
-
do not get hoisted to a `load` function, the recommended practice to is use the store returned from
|
|
319
|
-
the result of query:
|
|
320
|
-
|
|
321
|
-
```svelte
|
|
322
|
-
// src/components/MyComponent.svelte
|
|
323
|
-
|
|
324
|
-
<script>
|
|
325
|
-
import { query } from '$houdini'
|
|
326
|
-
|
|
327
|
-
const { data, loading } = query(...)
|
|
328
|
-
</script>
|
|
329
|
-
|
|
330
|
-
{#if $loading}
|
|
331
|
-
loading...
|
|
332
|
-
{:else}
|
|
333
|
-
data is loaded!
|
|
334
|
-
{/if}
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### Load Hooks
|
|
338
|
-
|
|
339
|
-
Sometimes you will need to add additional logic to a component's query. For example, you might want to
|
|
340
|
-
check if the current session is valid before a query is sent to the server. In order to support this,
|
|
341
|
-
houdini will look for hook functions defined in the module context which can be used to perform
|
|
342
|
-
any logic you need.
|
|
343
|
-
|
|
344
|
-
#### `beforeLoad`
|
|
345
|
-
|
|
346
|
-
Called before Houdini executes load queries against the server. You can expect the same
|
|
347
|
-
arguments as SvelteKit's [`load`](https://kit.svelte.dev/docs#loading) hook.
|
|
348
|
-
|
|
349
|
-
If you return a value from this function, it will be passed as props to your component.
|
|
350
|
-
|
|
351
|
-
```svelte
|
|
352
|
-
<script context="module">
|
|
353
|
-
// It has access to the same arguments and this.error this.redirect as the variable functions
|
|
354
|
-
export function beforeLoad({page, session}){
|
|
355
|
-
if(!session.authenticated){
|
|
356
|
-
return this.redirect(302, '/login')
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return {
|
|
360
|
-
message: "There are this many items"
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
</script>
|
|
364
|
-
|
|
365
16
|
<script>
|
|
366
17
|
import { query, graphql } from '$houdini'
|
|
367
18
|
|
|
368
|
-
export let message
|
|
369
|
-
|
|
370
|
-
// load the items
|
|
371
19
|
const { data } = query(graphql`
|
|
372
|
-
query
|
|
20
|
+
query AllTodoItems {
|
|
373
21
|
items {
|
|
374
|
-
id
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
`)
|
|
378
|
-
</script>
|
|
379
|
-
|
|
380
|
-
{message}: {$data.items.length}
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
#### `afterLoad`
|
|
384
|
-
|
|
385
|
-
Called after Houdini executes load queries against the server. You can expect the same
|
|
386
|
-
arguments as SvelteKit's [`load`](https://kit.svelte.dev/docs#loading) hook, plus an additional
|
|
387
|
-
`data` property referencing query result data.
|
|
388
|
-
|
|
389
|
-
If you return a value from this function, it will be passed as props to your component.
|
|
390
|
-
|
|
391
|
-
```svelte
|
|
392
|
-
<script context="module">
|
|
393
|
-
export function MyProfileVariables({ page: { params: { id } } }) {
|
|
394
|
-
return { id }
|
|
395
|
-
}
|
|
396
|
-
export function afterLoad({ data }){
|
|
397
|
-
if(!data.MyProfile){
|
|
398
|
-
return this.error(404)
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
</script>
|
|
402
|
-
|
|
403
|
-
<script>
|
|
404
|
-
import { query, graphql } from '$houdini'
|
|
405
|
-
|
|
406
|
-
// load the items
|
|
407
|
-
const { data } = query(graphql`
|
|
408
|
-
query MyProfile {
|
|
409
|
-
profile(id) {
|
|
410
|
-
name
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
`)
|
|
414
|
-
</script>
|
|
415
|
-
|
|
416
|
-
Hello I'm {$data.profile.name}
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
### Refetching Data
|
|
420
|
-
|
|
421
|
-
Refetching data is done with the `refetch` function provided from the result of a query:
|
|
422
|
-
|
|
423
|
-
```svelte
|
|
424
|
-
|
|
425
|
-
<script lang="ts">
|
|
426
|
-
import { query, graphql, AllItems } from '$houdini'
|
|
427
|
-
|
|
428
|
-
// load the items
|
|
429
|
-
const { refetch } = query<AllItems>(graphql`
|
|
430
|
-
query AllItems($completed: Boolean) {
|
|
431
|
-
items(completed: $completed) {
|
|
432
|
-
id
|
|
433
22
|
text
|
|
434
23
|
}
|
|
435
24
|
}
|
|
436
25
|
`)
|
|
437
|
-
|
|
438
|
-
let completed = true
|
|
439
|
-
|
|
440
|
-
$: refetch({ completed })
|
|
441
|
-
</script>
|
|
442
|
-
|
|
443
|
-
<input type=checkbox bind:checked={completed}>
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
### Cache policy
|
|
447
|
-
|
|
448
|
-
By default, houdini will only try to load queries against its local cache when you indicate it is safe to do so.
|
|
449
|
-
This can be done with the `@cache` directive:
|
|
450
|
-
|
|
451
|
-
```graphql
|
|
452
|
-
query AllItems @cache(policy: CacheOrNetwork) {
|
|
453
|
-
items {
|
|
454
|
-
id
|
|
455
|
-
text
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
There are 3 different policies that can be specified:
|
|
461
|
-
|
|
462
|
-
- **CacheOrNetwork** will first check if a query can be resolved from the cache. If it can, it will return the cached value and only send a network request if data was missing.
|
|
463
|
-
- **CacheAndNetwork** will use cached data if it exists and always send a network request after the component has mounted to retrieve the latest data from the server
|
|
464
|
-
- **NetworkOnly** will never check if the data exists in the cache and always send a network request
|
|
465
|
-
|
|
466
|
-
#### Data Retention
|
|
467
|
-
|
|
468
|
-
Houdini will retain a query's data for a configurable number of queries (default 10).
|
|
469
|
-
For a concrete example, consider an example app that has 3 routes. If you load one of the
|
|
470
|
-
routes and then click between the other two 5 times, the first route's data will still be
|
|
471
|
-
resolvable (and the counter will reset if you visit it).
|
|
472
|
-
If you then toggle between the other routes 10 times and then try to load the first
|
|
473
|
-
route, a network request will be sent. This number is configurable with the
|
|
474
|
-
`cacheBufferSize` value in your config file:
|
|
475
|
-
|
|
476
|
-
```js
|
|
477
|
-
// houdini.config.js
|
|
478
|
-
|
|
479
|
-
export default {
|
|
480
|
-
// ...
|
|
481
|
-
cacheBufferSize: 5,
|
|
482
|
-
}
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
#### Changing default cache policy
|
|
486
|
-
|
|
487
|
-
As previously mentioned, the default cache policy is `CacheOrNetwork`. This can be changed
|
|
488
|
-
by setting the `defaultCachePolicy` config value:
|
|
489
|
-
|
|
490
|
-
```js
|
|
491
|
-
// houdini.config.js
|
|
492
|
-
|
|
493
|
-
import { CachePolicy } from '$houdini'
|
|
494
|
-
|
|
495
|
-
export default {
|
|
496
|
-
// ...
|
|
497
|
-
|
|
498
|
-
// note: if you are upgrading from a previous version of houdini, you might
|
|
499
|
-
// have to generate your runtime for this type to be defined.
|
|
500
|
-
defaultCachePolicy: CachePolicy.NetworkOnly,
|
|
501
|
-
}
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
### What about `load`?
|
|
505
|
-
|
|
506
|
-
Don't worry - that's where the preprocessor comes in. One of its responsibilities is moving the actual
|
|
507
|
-
fetch into a `load`. You can think of the block at the top of this section as equivalent to:
|
|
508
|
-
|
|
509
|
-
```svelte
|
|
510
|
-
<script context="module">
|
|
511
|
-
export async function load() {
|
|
512
|
-
return {
|
|
513
|
-
_data: await this.fetch({
|
|
514
|
-
text: `
|
|
515
|
-
query AllItems {
|
|
516
|
-
items {
|
|
517
|
-
id
|
|
518
|
-
text
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
`
|
|
522
|
-
}),
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
</script>
|
|
526
|
-
|
|
527
|
-
<script>
|
|
528
|
-
export let _data
|
|
529
|
-
|
|
530
|
-
const data = readable(_data, ...)
|
|
531
26
|
</script>
|
|
532
27
|
|
|
533
28
|
{#each $data.items as item}
|
|
@@ -535,652 +30,34 @@ fetch into a `load`. You can think of the block at the top of this section as eq
|
|
|
535
30
|
{/each}
|
|
536
31
|
```
|
|
537
32
|
|
|
538
|
-
##
|
|
539
|
-
|
|
540
|
-
Your components will want to make assumptions about which attributes are
|
|
541
|
-
available in your queries. To support this, Houdini uses GraphQL fragments embedded
|
|
542
|
-
within your component. Take, for example, a `UserAvatar` component that requires
|
|
543
|
-
the `profilePicture` field of a `User`:
|
|
544
|
-
|
|
545
|
-
```svelte
|
|
546
|
-
// components/UserAvatar.svelte
|
|
547
|
-
|
|
548
|
-
<script lang="ts">
|
|
549
|
-
import { fragment, graphql, UserAvatar } from '$houdini'
|
|
550
|
-
|
|
551
|
-
// the reference will get passed as a prop
|
|
552
|
-
export let user: UserAvatar
|
|
553
|
-
|
|
554
|
-
const data = fragment(graphql`
|
|
555
|
-
fragment UserAvatar on User {
|
|
556
|
-
profilePicture
|
|
557
|
-
}
|
|
558
|
-
`, user)
|
|
559
|
-
</script>
|
|
560
|
-
|
|
561
|
-
<img src={$data.profilePicture} />
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
This component can be rendered anywhere we want to query for a user, with a guarantee
|
|
565
|
-
that all necessary data has been asked for:
|
|
566
|
-
|
|
567
|
-
```svelte
|
|
568
|
-
// src/routes/users.svelte
|
|
569
|
-
|
|
570
|
-
<script>
|
|
571
|
-
import { query, graphql, AllUsers } from '$houdini'
|
|
572
|
-
import { UserAvatar } from 'components'
|
|
573
|
-
|
|
574
|
-
const { data } = query<AllUsers>(graphql`
|
|
575
|
-
query AllUsers {
|
|
576
|
-
users {
|
|
577
|
-
id
|
|
578
|
-
...UserAvatar
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
`)
|
|
582
|
-
</script>
|
|
583
|
-
|
|
584
|
-
{#each $data.users as user}
|
|
585
|
-
<UserAvatar user={user} />
|
|
586
|
-
{/each}
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
It's worth mentioning explicitly that a component can rely on multiple fragments
|
|
590
|
-
at the same time so long as the fragment names are unique and prop names are different.
|
|
591
|
-
|
|
592
|
-
### Fragment Arguments
|
|
593
|
-
|
|
594
|
-
In some situations it's necessary to configure the documents inside of a fragment. For example,
|
|
595
|
-
you might want to extend the `UserAvatar` component to allow for different sized profile pictures.
|
|
596
|
-
To support this, houdini provides two directives `@arguments` and `@with` which declare arguments
|
|
597
|
-
for a fragment and provide values, respectively.
|
|
598
|
-
|
|
599
|
-
Default values can be provided to fragment arguments with the `default` key:
|
|
600
|
-
|
|
601
|
-
```graphql
|
|
602
|
-
fragment UserAvatar on User @arguments(width: {type:"Int", default: 50}) {
|
|
603
|
-
profilePicture(width: $width)
|
|
604
|
-
}
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
In order to mark an argument as required, pass the type with a `!` at the end.
|
|
608
|
-
If no value is provided, an error will be thrown when generating your runtime.
|
|
609
|
-
|
|
610
|
-
```graphql
|
|
611
|
-
fragment UserAvatar on User @arguments(width: {type:"Int!"}) {
|
|
612
|
-
profilePicture(width: $width)
|
|
613
|
-
}
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
Providing values for fragments is done with the `@with` decorator:
|
|
617
|
-
|
|
618
|
-
```graphql
|
|
619
|
-
query AllUsers {
|
|
620
|
-
users {
|
|
621
|
-
...UserAvatar @with(width: 100)
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
```
|
|
625
|
-
> Keep in mind, if you are using fragment variables inside of a field flagged for
|
|
626
|
-
> list operations, you'll have to pass a value for the variable when performing the operation
|
|
627
|
-
|
|
628
|
-
## π Mutations
|
|
629
|
-
|
|
630
|
-
Mutations are defined in your component like the rest of the documents but
|
|
631
|
-
instead of triggering a network request when called, you get a function
|
|
632
|
-
which can be invoked to execute the mutation. Here's another modified example from
|
|
633
|
-
[the demo](./example):
|
|
634
|
-
|
|
635
|
-
```svelte
|
|
636
|
-
<script lang="ts">
|
|
637
|
-
import { mutation, graphql, UncheckItem } from '$houdini'
|
|
638
|
-
|
|
639
|
-
let itemID: string
|
|
640
|
-
|
|
641
|
-
const uncheckItem = mutation<UncheckItem>(graphql`
|
|
642
|
-
mutation UncheckItem($id: ID!) {
|
|
643
|
-
uncheckItem(item: $id) {
|
|
644
|
-
item {
|
|
645
|
-
id
|
|
646
|
-
completed
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
`)
|
|
651
|
-
</script>
|
|
652
|
-
|
|
653
|
-
<button on:click={() => uncheckItem({ id: itemID })}>
|
|
654
|
-
Uncheck Item
|
|
655
|
-
</button>
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
Note: mutations usually do best when combined with at least one fragment grabbing
|
|
659
|
-
the information needed for the mutation (for an example of this pattern, see below.)
|
|
660
|
-
|
|
661
|
-
### Updating fields
|
|
662
|
-
|
|
663
|
-
When a mutation is responsible for updating fields of entities, houdini
|
|
664
|
-
should take care of the details for you as long as you request the updated data alongside the
|
|
665
|
-
record's id. Take for example, an `TodoItemRow` component:
|
|
666
|
-
|
|
667
|
-
```svelte
|
|
668
|
-
<script lang="ts">
|
|
669
|
-
import { fragment, mutation, graphql, TodoItemRow } from '$houdini'
|
|
670
|
-
|
|
671
|
-
export let item: TodoItemRow
|
|
672
|
-
|
|
673
|
-
// the resulting store will stay up to date whenever `checkItem`
|
|
674
|
-
// is triggered
|
|
675
|
-
const data = fragment(
|
|
676
|
-
graphql`
|
|
677
|
-
fragment TodoItemRow on TodoItem {
|
|
678
|
-
id
|
|
679
|
-
text
|
|
680
|
-
completed
|
|
681
|
-
}
|
|
682
|
-
`,
|
|
683
|
-
item
|
|
684
|
-
)
|
|
685
|
-
|
|
686
|
-
const checkItem = mutation<CompleteItem>(graphql`
|
|
687
|
-
mutation CompleteItem($id: ID!) {
|
|
688
|
-
checkItem(item: $id) {
|
|
689
|
-
item {
|
|
690
|
-
id
|
|
691
|
-
completed
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
`)
|
|
696
|
-
</script>
|
|
697
|
-
|
|
698
|
-
<li class:completed={$data.completed}>
|
|
699
|
-
<input
|
|
700
|
-
name={$data.text}
|
|
701
|
-
class="toggle"
|
|
702
|
-
type="checkbox"
|
|
703
|
-
checked={$data.completed}
|
|
704
|
-
on:click={handleClick}
|
|
705
|
-
/>
|
|
706
|
-
<label for={$data.text}>{$data.text}</label>
|
|
707
|
-
<button class="destroy" on:click={() => deleteItem({ id: $data.id })} />
|
|
708
|
-
</li>
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
### Lists
|
|
712
|
-
|
|
713
|
-
Adding and removing records from a list is done by mixing together a few different generated fragments
|
|
714
|
-
and directives. In order to tell the compiler which lists are targets for these operations, you have to
|
|
715
|
-
mark them with the `@list` directive and provide a unique name:
|
|
716
|
-
|
|
717
|
-
```graphql
|
|
718
|
-
query AllItems {
|
|
719
|
-
items @list(name: "All_Items") {
|
|
720
|
-
id
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
It's recommended to name these lists with a different casing convention than the rest of your
|
|
726
|
-
application to distinguish the generated fragments from those in your codebase.
|
|
727
|
-
|
|
728
|
-
#### Inserting a record
|
|
729
|
-
|
|
730
|
-
With this field tagged, any mutation that returns an `Item` can be used to insert items in this list:
|
|
731
|
-
|
|
732
|
-
```graphql
|
|
733
|
-
mutation NewItem($input: AddItemInput!) {
|
|
734
|
-
addItem(input: $input) {
|
|
735
|
-
...All_Items_insert
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
```
|
|
739
|
-
|
|
740
|
-
#### Removing a record
|
|
741
|
-
|
|
742
|
-
Any mutation that returns an `Item` can also be used to remove an item from the list:
|
|
743
|
-
|
|
744
|
-
```graphql
|
|
745
|
-
mutation RemoveItem($input: RemoveItemInput!) {
|
|
746
|
-
removeItem(input: $input) {
|
|
747
|
-
...All_Items_remove
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
```
|
|
751
|
-
|
|
752
|
-
#### Deleting a record
|
|
753
|
-
|
|
754
|
-
Sometimes it can be tedious to remove a record from every single list that mentions it.
|
|
755
|
-
For these situations, Houdini provides a directive that can be used to mark a field in
|
|
756
|
-
the mutation response holding the ID of a record to delete from all lists.
|
|
757
|
-
|
|
758
|
-
```graphql
|
|
759
|
-
mutation DeleteItem($id: ID!) {
|
|
760
|
-
deleteItem(id: $id) {
|
|
761
|
-
itemID @Item_delete
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
#### Conditionals
|
|
767
|
-
|
|
768
|
-
Sometimes you only want to add or remove a record from a list when an argument has a particular value.
|
|
769
|
-
For example, in a todo list you might only want to add the result to the list if there is no filter being
|
|
770
|
-
applied. To support this, houdini provides the `@when` and `@when_not` directives:
|
|
771
|
-
|
|
772
|
-
```graphql
|
|
773
|
-
mutation NewItem($input: AddItemInput!) {
|
|
774
|
-
addItem(input: $input) {
|
|
775
|
-
...All_Items_insert @when_not(completed: true)
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
```
|
|
779
|
-
|
|
780
|
-
## π§Ύ Subscriptions
|
|
781
|
-
|
|
782
|
-
Subscriptions in houdini are handled with the `subscription` function exported by your runtime. This function
|
|
783
|
-
takes a tagged document, and returns a store with the most recent value returned by the server. Keep in mind
|
|
784
|
-
that houdini will keep the cache (and any subscribing components) up to date as new data is encountered.
|
|
785
|
-
|
|
786
|
-
It's worth mentioning that you can use the same fragments described in the [mutation section](#mutations)
|
|
787
|
-
in order to update houdini's cache with the response from a subscription.
|
|
788
|
-
|
|
789
|
-
Here is an example of a simple subscription from the example application included in this repo:
|
|
790
|
-
|
|
791
|
-
```svelte
|
|
792
|
-
<script lang="ts">
|
|
793
|
-
import {
|
|
794
|
-
fragment,
|
|
795
|
-
mutation,
|
|
796
|
-
graphql,
|
|
797
|
-
subscription,
|
|
798
|
-
ItemEntry_item,
|
|
799
|
-
} from '$houdini'
|
|
800
|
-
|
|
801
|
-
// the reference we're passed from our parents
|
|
802
|
-
export let item: ItemEntry_item
|
|
803
|
-
|
|
804
|
-
// get the information we need about the item
|
|
805
|
-
const data = fragment(/* ... */)
|
|
806
|
-
|
|
807
|
-
// since we're just using subscriptions to stay up to date, we don't care about the return value
|
|
808
|
-
subscription(
|
|
809
|
-
graphql`
|
|
810
|
-
subscription ItemUpdate($id: ID!) {
|
|
811
|
-
itemUpdate(id: $id) {
|
|
812
|
-
item {
|
|
813
|
-
id
|
|
814
|
-
completed
|
|
815
|
-
text
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
`,
|
|
820
|
-
{
|
|
821
|
-
id: $data.id,
|
|
822
|
-
}
|
|
823
|
-
)
|
|
824
|
-
</script>
|
|
825
|
-
|
|
826
|
-
<li class:completed={$data.completed}>
|
|
827
|
-
<div class="view">
|
|
828
|
-
<input
|
|
829
|
-
name={$data.text}
|
|
830
|
-
class="toggle"
|
|
831
|
-
type="checkbox"
|
|
832
|
-
checked={$data.completed}
|
|
833
|
-
on:click={handleClick}
|
|
834
|
-
/>
|
|
835
|
-
<label for={$data.text}>{$data.text}</label>
|
|
836
|
-
<button class="destroy" on:click={() => deleteItem({ id: $data.id })} />
|
|
837
|
-
</div>
|
|
838
|
-
</li>
|
|
839
|
-
```
|
|
840
|
-
|
|
841
|
-
### Configuring the WebSocket client
|
|
842
|
-
|
|
843
|
-
Houdini can work with any websocket client as long as you can provide an object that satisfies
|
|
844
|
-
the `SubscriptionHandler` interface as the second argument to the Environment's constructor. Keep in mind
|
|
845
|
-
that WebSocket connections only exist between the browser and your API, therefor you must remember to
|
|
846
|
-
pass `null` when configuring your environment on the rendering server.
|
|
847
|
-
|
|
848
|
-
#### Using `graphql-ws`
|
|
849
|
-
|
|
850
|
-
If your API supports the [`graphql-ws`](https://github.com/enisdenjo/graphql-ws) protocol, you can create a
|
|
851
|
-
client and pass it directly:
|
|
852
|
-
|
|
853
|
-
```typescript
|
|
854
|
-
// environment.ts
|
|
855
|
-
|
|
856
|
-
import { createClient } from 'graphql-ws'
|
|
857
|
-
import { browser } from '$app/env'
|
|
858
|
-
|
|
859
|
-
// in sapper, this would be something like `(process as any).browser`
|
|
860
|
-
let socketClient = browser
|
|
861
|
-
? new createClient({
|
|
862
|
-
url: 'ws://api.url',
|
|
863
|
-
})
|
|
864
|
-
: null
|
|
865
|
-
|
|
866
|
-
export default new Environment(fetchQuery, socketClient)
|
|
867
|
-
```
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
#### Using `subscriptions-transport-ws`
|
|
871
|
-
|
|
872
|
-
If you are using the deprecated `subscriptions-transport-ws` library and associated protocol,
|
|
873
|
-
you will have to slightly modify the above block:
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
```typescript
|
|
877
|
-
// environment.ts
|
|
878
|
-
|
|
879
|
-
import { SubscriptionClient } from 'subscriptions-transport-ws'
|
|
880
|
-
import { browser } from '$app/env'
|
|
881
|
-
|
|
882
|
-
let socketClient: SubscriptionHandler | null = null
|
|
883
|
-
if (browser) {
|
|
884
|
-
// instantiate the transport client
|
|
885
|
-
const client = new SubscriptionClient('ws://api.url', {
|
|
886
|
-
reconnect: true,
|
|
887
|
-
})
|
|
888
|
-
|
|
889
|
-
// wrap the client in something houdini can use
|
|
890
|
-
socketClient = {
|
|
891
|
-
subscribe(payload, handlers) {
|
|
892
|
-
// send the request
|
|
893
|
-
const { unsubscribe } = client.request(payload).subscribe(handlers)
|
|
894
|
-
|
|
895
|
-
// return the function to unsubscribe
|
|
896
|
-
return unsubscribe
|
|
897
|
-
},
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
export default new Environment(fetchQuery, socketClient)
|
|
902
|
-
```
|
|
903
|
-
|
|
904
|
-
## β»οΈ Pagination
|
|
905
|
-
|
|
906
|
-
It's often the case that you want to avoid querying an entire list from your API in order
|
|
907
|
-
to minimize the amount of data transfers over the network. To support this, GraphQL APIs will
|
|
908
|
-
"paginate" a field, allowing users to query a slice of the list. The strategy used to access
|
|
909
|
-
slices of a list fall into two categories. Offset-based pagination relies `offset` and `limit`
|
|
910
|
-
arguments and mimics the mechanisms provided by most database engines. Cursor-based pagination
|
|
911
|
-
is a bi-directional strategy that relies on `first`/`after` or `last`/`before` arguments and
|
|
912
|
-
is designed to handle modern pagination features such a infinite scrolling.
|
|
913
|
-
|
|
914
|
-
Regardless of the strategy used, houdini follows a simple pattern: wrap your document in a
|
|
915
|
-
"paginated" function (ie, `paginatedQuery` or `paginatedFragment`), mark the field with
|
|
916
|
-
`@paginate`, and provide the "page size" via the `first`, `last` or `limit` arguments to the field.
|
|
917
|
-
`paginatedQuery` and `paginatedFragment` behave identically: they return a `data` field containing
|
|
918
|
-
a svelte store with your full dataset, functions you can call to load the next or previous
|
|
919
|
-
page, as well as a readable store with a boolean loading state. For example, a field
|
|
920
|
-
supporting offset-based pagination would look something like:
|
|
921
|
-
|
|
922
|
-
```javascript
|
|
923
|
-
const { data, loadNextPage, loading } = paginatedQuery(graphql`
|
|
924
|
-
query UserList {
|
|
925
|
-
friends(limit: 10) @paginate {
|
|
926
|
-
id
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
`)
|
|
930
|
-
```
|
|
931
|
-
|
|
932
|
-
and a field that supports cursor-based pagination starting at the end of the list would look something like:
|
|
933
|
-
|
|
934
|
-
```javascript
|
|
935
|
-
const { data, loadPreviousPage } = paginatedQuery(graphql`
|
|
936
|
-
query UserList {
|
|
937
|
-
friends(last: 10) @paginate {
|
|
938
|
-
edges {
|
|
939
|
-
node {
|
|
940
|
-
id
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
`)
|
|
946
|
-
```
|
|
947
|
-
|
|
948
|
-
If you are paginating a field with a cursor-based strategy (forward or backwards), the current page
|
|
949
|
-
info can be looked up with the `pageInfo` store returned from the paginated function:
|
|
950
|
-
|
|
951
|
-
```svelte
|
|
952
|
-
<script>
|
|
953
|
-
const { data, loadNextPage, pageInfo } = paginatedQuery(graphql`
|
|
954
|
-
query UserList {
|
|
955
|
-
friends(first: 10) @paginate {
|
|
956
|
-
edges {
|
|
957
|
-
node {
|
|
958
|
-
id
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
`)
|
|
964
|
-
</script>
|
|
965
|
-
|
|
966
|
-
{#if $pageInfo.hasNextPage}
|
|
967
|
-
<button onClick={() => loadNextPage()}> load more </button>
|
|
968
|
-
{/if}
|
|
969
|
-
```
|
|
970
|
-
|
|
971
|
-
### Paginated Fragments
|
|
972
|
-
|
|
973
|
-
`paginatedFragment` functions very similarly to `paginatedQuery` with a few caveats.
|
|
974
|
-
Consider the following:
|
|
975
|
-
|
|
976
|
-
```javascript
|
|
977
|
-
const { loadNextPage, data, pageInfo } = paginatedFragment(graphql`
|
|
978
|
-
fragment UserWithFriends on User {
|
|
979
|
-
friends(first: 10) @paginate {
|
|
980
|
-
edges {
|
|
981
|
-
node {
|
|
982
|
-
id
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
`)
|
|
988
|
-
```
|
|
989
|
-
|
|
990
|
-
In order to look up the next page for the user's friend. We need a way to query the specific user
|
|
991
|
-
that this fragment has been spread into. In order to pull this off, houdini relies on the generic `Node`
|
|
992
|
-
interface and corresponding query:
|
|
993
|
-
|
|
994
|
-
```graphql
|
|
995
|
-
interface Node {
|
|
996
|
-
id: ID!
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
type Query {
|
|
1000
|
-
node(id: ID!): Node
|
|
1001
|
-
}
|
|
1002
|
-
```
|
|
1003
|
-
|
|
1004
|
-
In short, this means that any paginated fragment must be of a type that implements the Node interface
|
|
1005
|
-
(so it can be looked up in the api). You can read more information about the `Node` interface in
|
|
1006
|
-
[this section](https://graphql.org/learn/global-object-identification/) of the graphql community website.
|
|
1007
|
-
This is only a requirement for paginated fragments. If your application only uses paginated queries,
|
|
1008
|
-
you do not need to implement the Node interface and resolver.
|
|
1009
|
-
|
|
1010
|
-
### Mutation Operations
|
|
1011
|
-
|
|
1012
|
-
A paginated field can be marked as a potential target for a mutation operation by passing
|
|
1013
|
-
a `name` argument to the `@paginate` directive:
|
|
1014
|
-
|
|
1015
|
-
```javascript
|
|
1016
|
-
const { loadNextPage, data, pageInfo } = paginatedFragment(graphql`
|
|
1017
|
-
fragment UserWithFriends on User {
|
|
1018
|
-
friends(first: 10) @paginate(name: "User_Friends") {
|
|
1019
|
-
edges {
|
|
1020
|
-
node {
|
|
1021
|
-
id
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
`)
|
|
1027
|
-
```
|
|
1028
|
-
|
|
1029
|
-
## βοΈ Custom Scalars
|
|
1030
|
-
|
|
1031
|
-
Configuring your runtime to handle custom scalars is done under the `scalars` key in your config:
|
|
1032
|
-
|
|
1033
|
-
```javascript
|
|
1034
|
-
// houdini.config.js
|
|
1035
|
-
|
|
1036
|
-
export default {
|
|
1037
|
-
// ...
|
|
1038
|
-
|
|
1039
|
-
scalars: {
|
|
1040
|
-
// the name of the scalar we are configuring
|
|
1041
|
-
DateTime: {
|
|
1042
|
-
// the corresponding typescript type
|
|
1043
|
-
type: 'Date',
|
|
1044
|
-
// turn the api's response into that type
|
|
1045
|
-
unmarshal(val) {
|
|
1046
|
-
return new Date(val)
|
|
1047
|
-
},
|
|
1048
|
-
// turn the value into something the API can use
|
|
1049
|
-
marshal(date) {
|
|
1050
|
-
return date.getTime()
|
|
1051
|
-
},
|
|
1052
|
-
},
|
|
1053
|
-
},
|
|
1054
|
-
}
|
|
1055
|
-
```
|
|
1056
|
-
|
|
1057
|
-
## π Authentication
|
|
1058
|
-
|
|
1059
|
-
houdini defers to SvelteKit's sessions for authentication. Assuming that the session has been populated
|
|
1060
|
-
somehow, you can access it through the second argument in the environment definition:
|
|
1061
|
-
|
|
1062
|
-
```typescript
|
|
1063
|
-
//src/environment.ts
|
|
1064
|
-
|
|
1065
|
-
import { Environment } from '$houdini'
|
|
1066
|
-
|
|
1067
|
-
// this function can take a second argument that will contain the session
|
|
1068
|
-
// data during a request or mutation
|
|
1069
|
-
export default new Environment(async function ({ text, variables = {} }, session) {
|
|
1070
|
-
const result = await this.fetch('http://localhost:4000', {
|
|
1071
|
-
method: 'POST',
|
|
1072
|
-
headers: {
|
|
1073
|
-
'Content-Type': 'application/json',
|
|
1074
|
-
'Authorization': session.token ? `Bearer ${session.token}` : null,
|
|
1075
|
-
},
|
|
1076
|
-
body: JSON.stringify({
|
|
1077
|
-
query: text,
|
|
1078
|
-
variables,
|
|
1079
|
-
}),
|
|
1080
|
-
})
|
|
1081
|
-
|
|
1082
|
-
// parse the result as json
|
|
1083
|
-
return await result.json()
|
|
1084
|
-
})
|
|
1085
|
-
```
|
|
1086
|
-
|
|
1087
|
-
## π¦ Persisted Queries
|
|
1088
|
-
|
|
1089
|
-
Sometimes you want to confine an API to only fire a set of pre-defined queries. This
|
|
1090
|
-
can be useful to not only reduce the amount of information transferred over the write
|
|
1091
|
-
but also act as a list of approved queries, providing additional security. Regardless of
|
|
1092
|
-
your motivation, the approach involves associating a known string with a particular query
|
|
1093
|
-
and sending that string to the server instead of the full query body. To support this,
|
|
1094
|
-
houdini passes a queries hash to the fetch function for you to use.
|
|
1095
|
-
|
|
1096
|
-
### Automatic Persisted Queries
|
|
1097
|
-
|
|
1098
|
-
An approach to Persisted Queries, popularized by Apollo, is known as
|
|
1099
|
-
[Automatic Persisted Queries](https://www.apollographql.com/docs/apollo-server/performance/apq/).
|
|
1100
|
-
This involves first sending a queries hash and if its unrecognized, sending the full
|
|
1101
|
-
query string. This might look something like:
|
|
1102
|
-
|
|
1103
|
-
```typescript
|
|
1104
|
-
/// src/environment.ts
|
|
1105
|
-
|
|
1106
|
-
// This sends the actual fetch request to the server
|
|
1107
|
-
async function sendFetch({ text, variables, hash }) {
|
|
1108
|
-
const result = await this.fetch('localhost:4000/graphql', {
|
|
1109
|
-
method: 'POST',
|
|
1110
|
-
headers: {
|
|
1111
|
-
'Content-Type': 'application/json',
|
|
1112
|
-
},
|
|
1113
|
-
body: JSON.stringify({
|
|
1114
|
-
query: text ? text : undefined,
|
|
1115
|
-
variables,
|
|
1116
|
-
extensions: {
|
|
1117
|
-
persistedQuery: {
|
|
1118
|
-
version: 1,
|
|
1119
|
-
sha256Hash: hash,
|
|
1120
|
-
},
|
|
1121
|
-
},
|
|
1122
|
-
}),
|
|
1123
|
-
})
|
|
1124
|
-
|
|
1125
|
-
return result.json();
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
export default new Environment(async function({ text, variables = {}, hash }){
|
|
1129
|
-
// first send the request without the text, only the hash
|
|
1130
|
-
const response = await sendFetch.call(this, { variables, hash, text: null })
|
|
1131
|
-
|
|
1132
|
-
// if there were no errors, we're good to go
|
|
1133
|
-
if (!response.errors) {
|
|
1134
|
-
return response
|
|
1135
|
-
}
|
|
33
|
+
## β¨ Features
|
|
1136
34
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
35
|
+
- Composable and colocated data requirements for your components
|
|
36
|
+
- Normalized cache with declarative updates
|
|
37
|
+
- Generated types
|
|
38
|
+
- Subscriptions
|
|
39
|
+
- Support for SvelteKit and Sapper
|
|
40
|
+
- Pagination (cursors **and** offsets)
|
|
1142
41
|
|
|
1143
|
-
|
|
42
|
+
At its core, houdini seeks to enable a high quality developer experience
|
|
43
|
+
without compromising bundle size. Like Svelte, houdini shifts what is
|
|
44
|
+
traditionally handled by a bloated runtime into a compile step that allows
|
|
45
|
+
for the generation of an incredibly lean GraphQL abstraction for your application.
|
|
1144
46
|
|
|
1145
|
-
|
|
1146
|
-
a fixed association of hash to query for every document that your client will send.
|
|
1147
|
-
To support this, you can pass the `--persist-output` flag to the `generate` command
|
|
1148
|
-
and provide a path to save the map:
|
|
47
|
+
## πΉ Example
|
|
1149
48
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
# or
|
|
1153
|
-
npx houdini generate -po ./path/to/persisted-queries.json
|
|
1154
|
-
```
|
|
49
|
+
For a detailed example, you can check out the todo list in the [example directory](./example) or the [final version](https://github.com/HoudiniGraphql/intro/tree/final) of the
|
|
50
|
+
PokΓ©dex application from the [Getting Started guide](https://www.houdinigraphql.com/intro/welcome).
|
|
1155
51
|
|
|
1156
|
-
|
|
52
|
+
## π Documentation
|
|
1157
53
|
|
|
1158
|
-
|
|
1159
|
-
pass the hash under whatever field name you prefer:
|
|
54
|
+
For documentation, please visit the [api reference](https://www.houdinigraphql.com/api/welcome) on the website.
|
|
1160
55
|
|
|
1161
|
-
|
|
1162
|
-
/// src/environment.ts
|
|
56
|
+
## π Getting Started
|
|
1163
57
|
|
|
1164
|
-
|
|
1165
|
-
const result = await this.fetch('http://localhost:4000', {
|
|
1166
|
-
method: 'POST',
|
|
1167
|
-
headers: {
|
|
1168
|
-
'Content-Type': 'application/json',
|
|
1169
|
-
},
|
|
1170
|
-
body: JSON.stringify({
|
|
1171
|
-
doc_id: hash,
|
|
1172
|
-
variables,
|
|
1173
|
-
}),
|
|
1174
|
-
})
|
|
58
|
+
For an in-depth guide to getting started with Houdini, check out the [guide on the our website](https://www.houdinigraphql.com/intro/welcome).
|
|
1175
59
|
|
|
1176
|
-
|
|
1177
|
-
return await result.json()
|
|
1178
|
-
})
|
|
1179
|
-
```
|
|
60
|
+
## βοΈ Contributing
|
|
1180
61
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
- Every GraphQL Document must have a name that is unique
|
|
1184
|
-
- Variable functions must be named after their query
|
|
1185
|
-
- Documents with a query must have only one operation in them
|
|
1186
|
-
- Documents without an operation must have only one fragment in them
|
|
62
|
+
If you are interested in helping out, the [contributing guide](https://www.houdinigraphql.com/guides/contributing) should provide some guidance. If you need something more
|
|
63
|
+
specific, feel free to reach out to @AlecAivazis on the Svelte discord. There's lots to help with regardless of how deep you want to dive or how much time you can spend π
|