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.
Files changed (60) hide show
  1. package/README.md +22 -1145
  2. package/build/cmd/generators/artifacts/selection.js +7 -1
  3. package/build/cmd/generators/typescript/addReferencedInputTypes.js +2 -1
  4. package/build/cmd/generators/typescript/index.js +1 -1
  5. package/build/cmd.js +10 -3
  6. package/build/runtime/cache/cache.d.ts +12 -4
  7. package/build/runtime/cache/cache.js +185 -159
  8. package/build/runtime/cache/lists.js +1 -12
  9. package/build/runtime/mutation.js +1 -3
  10. package/build/runtime/network.d.ts +10 -2
  11. package/build/runtime/network.js +43 -57
  12. package/build/runtime/pagination.js +41 -36
  13. package/build/runtime/query.d.ts +5 -1
  14. package/build/runtime/query.js +29 -22
  15. package/build/runtime/types.d.ts +2 -0
  16. package/build/runtime-cjs/cache/cache.d.ts +12 -4
  17. package/build/runtime-cjs/cache/cache.js +185 -159
  18. package/build/runtime-cjs/cache/lists.js +1 -12
  19. package/build/runtime-cjs/mutation.js +1 -3
  20. package/build/runtime-cjs/network.d.ts +10 -2
  21. package/build/runtime-cjs/network.js +43 -57
  22. package/build/runtime-cjs/pagination.js +41 -36
  23. package/build/runtime-cjs/query.d.ts +5 -1
  24. package/build/runtime-cjs/query.js +29 -22
  25. package/build/runtime-cjs/types.d.ts +2 -0
  26. package/build/runtime-esm/cache/cache.d.ts +12 -4
  27. package/build/runtime-esm/cache/cache.js +115 -87
  28. package/build/runtime-esm/cache/list.d.ts +35 -0
  29. package/build/runtime-esm/cache/list.js +203 -0
  30. package/build/runtime-esm/cache/lists.js +1 -12
  31. package/build/runtime-esm/cache/record.d.ts +40 -0
  32. package/build/runtime-esm/cache/record.js +195 -0
  33. package/build/runtime-esm/mutation.js +1 -3
  34. package/build/runtime-esm/network.d.ts +10 -2
  35. package/build/runtime-esm/network.js +39 -35
  36. package/build/runtime-esm/pagination.js +20 -8
  37. package/build/runtime-esm/query.d.ts +5 -1
  38. package/build/runtime-esm/query.js +23 -14
  39. package/build/runtime-esm/types.d.ts +2 -0
  40. package/cmd/generators/artifacts/artifacts.test.ts +2 -0
  41. package/cmd/generators/artifacts/pagination.test.ts +5 -0
  42. package/cmd/generators/artifacts/selection.ts +8 -1
  43. package/cmd/generators/typescript/addReferencedInputTypes.ts +4 -1
  44. package/cmd/generators/typescript/index.ts +5 -3
  45. package/cmd/generators/typescript/typescript.test.ts +19 -19
  46. package/cmd/transforms/paginate.test.ts +3 -0
  47. package/package.json +3 -3
  48. package/runtime/cache/cache.ts +140 -103
  49. package/runtime/cache/lists.ts +1 -14
  50. package/runtime/cache/tests/availability.test.ts +84 -29
  51. package/runtime/cache/tests/gc.test.ts +15 -10
  52. package/runtime/cache/tests/list.test.ts +12 -12
  53. package/runtime/cache/tests/readwrite.test.ts +1024 -0
  54. package/runtime/cache/tests/scalars.test.ts +5 -5
  55. package/runtime/cache/tests/subscriptions.test.ts +235 -745
  56. package/runtime/mutation.ts +1 -3
  57. package/runtime/network.ts +50 -38
  58. package/runtime/pagination.ts +30 -5
  59. package/runtime/query.ts +38 -23
  60. 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
- ## ✨&nbsp;&nbsp;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
- ## πŸ“š&nbsp;&nbsp;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
- ## πŸ•Ή&nbsp;&nbsp;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
- ## ⚑&nbsp;&nbsp;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
- ## πŸ”§&nbsp;&nbsp;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" />&nbsp;&nbsp;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
- ## πŸ“„&nbsp;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
- ## πŸš€&nbsp;&nbsp;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 AllItems {
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
- ## 🧩&nbsp;&nbsp;Fragments
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
- ## πŸ“&nbsp;&nbsp;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
- ## 🧾&nbsp;&nbsp;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
- ## ♻️&nbsp;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
- ## βš–οΈ&nbsp;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
- ## πŸ”&nbsp;&nbsp;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
- ## 🚦&nbsp;&nbsp;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
+ ## ✨&nbsp;&nbsp;Features
1136
34
 
1137
- // there were errors, send the hash and the query to associate the two for
1138
- // future requests
1139
- return await sendFetch.call(this, { variables, hash, text })
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
- ### Fixed List of Persisted Queries
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
- If you don't want the flexibility of Automatic Persisted Queries, you will need
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
+ ## πŸ•Ή&nbsp;&nbsp;Example
1149
48
 
1150
- ```bash
1151
- npx houdini generate --persist-output ./path/to/persisted-queries.json
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
- Once this map has been created, you will have to make it available to your server.
52
+ ## πŸ“š&nbsp;&nbsp;Documentation
1157
53
 
1158
- Now, instead of sending the full operation text with every request, you can now simply
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
- ```typescript
1162
- /// src/environment.ts
56
+ ## πŸš€&nbsp;&nbsp;Getting Started
1163
57
 
1164
- export default new Environment(async function({ text, variables = {}, hash }){
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
- // parse the result as json
1177
- return await result.json()
1178
- })
1179
- ```
60
+ ## ✏️&nbsp;&nbsp;Contributing
1180
61
 
1181
- ## ⚠️&nbsp;&nbsp;Notes, Constraints, and Conventions
1182
- - The compiler must be run every time the contents of a `graphql` tagged string changes
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 πŸ™‚