houdini 0.17.4 → 0.17.6

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 (55) hide show
  1. package/.turbo/turbo-compile.log +2 -2
  2. package/.turbo/turbo-typedefs.log +2 -2
  3. package/CHANGELOG.md +18 -0
  4. package/build/cmd/init.d.ts +2 -2
  5. package/build/cmd-cjs/index.js +9344 -10447
  6. package/build/cmd-esm/index.js +9344 -10447
  7. package/build/codegen/transforms/paginate.d.ts +10 -11
  8. package/build/codegen-cjs/index.js +9143 -10227
  9. package/build/codegen-esm/index.js +9143 -10227
  10. package/build/lib/constants.d.ts +7 -0
  11. package/build/lib/fs.d.ts +2 -0
  12. package/build/lib-cjs/index.js +9265 -10347
  13. package/build/lib-esm/index.js +9264 -10347
  14. package/build/runtime/cache/subscription.d.ts +2 -1
  15. package/build/runtime/lib/network.d.ts +5 -2
  16. package/build/runtime/lib/networkUtils.d.ts +8 -0
  17. package/build/runtime-cjs/cache/cache.js +5 -3
  18. package/build/runtime-cjs/cache/subscription.d.ts +2 -1
  19. package/build/runtime-cjs/cache/subscription.js +6 -4
  20. package/build/runtime-cjs/cache/tests/subscriptions.test.js +101 -0
  21. package/build/runtime-cjs/lib/network.d.ts +5 -2
  22. package/build/runtime-cjs/lib/network.js +39 -3
  23. package/build/runtime-cjs/lib/networkUtils.d.ts +8 -0
  24. package/build/runtime-cjs/lib/networkUtils.js +85 -0
  25. package/build/runtime-esm/cache/cache.js +5 -3
  26. package/build/runtime-esm/cache/subscription.d.ts +2 -1
  27. package/build/runtime-esm/cache/subscription.js +6 -4
  28. package/build/runtime-esm/cache/tests/subscriptions.test.js +101 -0
  29. package/build/runtime-esm/lib/network.d.ts +5 -2
  30. package/build/runtime-esm/lib/network.js +39 -3
  31. package/build/runtime-esm/lib/networkUtils.d.ts +8 -0
  32. package/build/runtime-esm/lib/networkUtils.js +60 -0
  33. package/build/test-cjs/index.js +9144 -10228
  34. package/build/test-esm/index.js +9144 -10228
  35. package/build/vite-cjs/index.js +9283 -10367
  36. package/build/vite-esm/index.js +9283 -10367
  37. package/package.json +2 -2
  38. package/src/cmd/init.ts +169 -187
  39. package/src/codegen/generators/artifacts/artifacts.test.ts +99 -66
  40. package/src/codegen/generators/artifacts/pagination.test.ts +12 -8
  41. package/src/codegen/generators/artifacts/policy.test.ts +12 -8
  42. package/src/codegen/generators/definitions/schema.test.ts +12 -36
  43. package/src/codegen/generators/persistedQueries/persistedQuery.test.ts +2 -2
  44. package/src/codegen/generators/runtime/index.ts +2 -2
  45. package/src/codegen/transforms/fragmentVariables.test.ts +24 -16
  46. package/src/codegen/transforms/paginate.test.ts +9 -6
  47. package/src/codegen/transforms/paginate.ts +2 -2
  48. package/src/lib/config.ts +3 -2
  49. package/src/lib/constants.ts +10 -0
  50. package/src/lib/fs.ts +59 -12
  51. package/src/runtime/cache/cache.ts +3 -1
  52. package/src/runtime/cache/subscription.ts +6 -4
  53. package/src/runtime/cache/tests/subscriptions.test.ts +115 -0
  54. package/src/runtime/lib/network.ts +65 -2
  55. package/src/runtime/lib/networkUtils.ts +151 -0
@@ -156,16 +156,17 @@ export class InMemorySubscriptions {
156
156
  selection,
157
157
  variables,
158
158
  subscribers,
159
+ parentType,
159
160
  }: {
160
161
  parent: string
161
162
  selection: SubscriptionSelection
162
163
  variables: {}
163
164
  subscribers: SubscriptionSpec[]
165
+ parentType: string
164
166
  }) {
165
167
  // look at every field in the selection and add the subscribers
166
168
  for (const fieldSelection of Object.values(selection)) {
167
- const { keyRaw, fields } = fieldSelection
168
-
169
+ const { type: linkedType, keyRaw, fields } = fieldSelection
169
170
  const key = evaluateKey(keyRaw, variables)
170
171
 
171
172
  // add the subscriber to the
@@ -175,7 +176,7 @@ export class InMemorySubscriptions {
175
176
  key,
176
177
  selection: fieldSelection,
177
178
  spec,
178
- parentType: 'asdf',
179
+ parentType,
179
180
  variables,
180
181
  })
181
182
  }
@@ -183,6 +184,7 @@ export class InMemorySubscriptions {
183
184
  // if there are fields under this
184
185
  if (fields) {
185
186
  const { value: link } = this.cache._internal_unstable.storage.get(parent, key)
187
+
186
188
  // figure out who else needs subscribers
187
189
  const children = !Array.isArray(link)
188
190
  ? ([link] as string[])
@@ -192,13 +194,13 @@ export class InMemorySubscriptions {
192
194
  if (!linkedRecord) {
193
195
  continue
194
196
  }
195
-
196
197
  // insert the subscriber
197
198
  this.addMany({
198
199
  parent: linkedRecord,
199
200
  selection: fields,
200
201
  variables,
201
202
  subscribers,
203
+ parentType: linkedType,
202
204
  })
203
205
  }
204
206
  }
@@ -1,6 +1,7 @@
1
1
  import { test, expect, vi } from 'vitest'
2
2
 
3
3
  import { testConfigFile } from '../../../test'
4
+ import { RefetchUpdateMode, SubscriptionSelection } from '../../lib'
4
5
  import { Cache } from '../cache'
5
6
 
6
7
  const config = testConfigFile()
@@ -1637,6 +1638,120 @@ test('clearing a display layer updates subscribers', function () {
1637
1638
  })
1638
1639
  })
1639
1640
 
1641
+ test('ensure parent type is properly passed for nested lists', function () {
1642
+ // instantiate a cache
1643
+ const cache = new Cache(config)
1644
+
1645
+ const selection: SubscriptionSelection = {
1646
+ cities: {
1647
+ type: 'City',
1648
+ keyRaw: 'cities',
1649
+ list: {
1650
+ name: 'City_List',
1651
+ connection: false,
1652
+ type: 'City',
1653
+ },
1654
+ update: RefetchUpdateMode.append,
1655
+ fields: {
1656
+ id: {
1657
+ type: 'ID',
1658
+ keyRaw: 'id',
1659
+ },
1660
+ name: {
1661
+ type: 'String',
1662
+ keyRaw: 'name',
1663
+ },
1664
+ libraries: {
1665
+ type: 'Library',
1666
+ keyRaw: 'libraries',
1667
+ update: RefetchUpdateMode.append,
1668
+ list: {
1669
+ name: 'Library_List',
1670
+ connection: false,
1671
+ type: 'Library',
1672
+ },
1673
+ fields: {
1674
+ id: {
1675
+ type: 'ID',
1676
+ keyRaw: 'id',
1677
+ },
1678
+ name: {
1679
+ type: 'String',
1680
+ keyRaw: 'name',
1681
+ },
1682
+ books: {
1683
+ type: 'Book',
1684
+ keyRaw: 'books',
1685
+ list: {
1686
+ name: 'Book_List',
1687
+ connection: false,
1688
+ type: 'Book',
1689
+ },
1690
+ fields: {
1691
+ id: {
1692
+ type: 'ID',
1693
+ keyRaw: 'id',
1694
+ },
1695
+ title: {
1696
+ type: 'String',
1697
+ keyRaw: 'title',
1698
+ },
1699
+ },
1700
+ },
1701
+ },
1702
+ },
1703
+ },
1704
+ },
1705
+ }
1706
+
1707
+ // a function to spy on that will play the role of set
1708
+ const set = vi.fn()
1709
+
1710
+ // subscribe to the fields
1711
+ cache.subscribe({
1712
+ rootType: 'Query',
1713
+ selection,
1714
+ set,
1715
+ })
1716
+
1717
+ // add a city to the list by hand since using the list util adds type information
1718
+
1719
+ cache.write({
1720
+ selection,
1721
+ data: {
1722
+ cities: [
1723
+ {
1724
+ id: '1',
1725
+ name: 'Alexandria',
1726
+ libraries: [
1727
+ {
1728
+ id: '1',
1729
+ name: 'The Library of Alexandria',
1730
+ books: [],
1731
+ },
1732
+ {
1733
+ id: '2',
1734
+ name: 'Bibliotheca Alexandrina',
1735
+ books: [],
1736
+ },
1737
+ ],
1738
+ },
1739
+ {
1740
+ id: '2',
1741
+ name: 'Aalborg',
1742
+ libraries: [],
1743
+ },
1744
+ ],
1745
+ },
1746
+ })
1747
+
1748
+ // since there are multiple lists inside of City_List, we need to
1749
+ // specify the parentID of the city in order to add a library to City:3
1750
+ expect(() => cache.list('Library_List', '2')).not.toThrow()
1751
+ // same with Books_List for Library:2
1752
+ expect(() => cache.list('Book_List', '2')).not.toThrow()
1753
+ })
1754
+
1640
1755
  test.todo('can write to and resolve layers')
1641
1756
 
1642
1757
  test.todo("resolving a layer with the same value as the most recent doesn't notify subscribers")
@@ -2,6 +2,7 @@
2
2
  import cache from '../cache'
3
3
  import type { ConfigFile } from './config'
4
4
  import * as log from './log'
5
+ import { extractFiles } from './networkUtils'
5
6
  import {
6
7
  CachePolicy,
7
8
  DataSource,
@@ -22,6 +23,56 @@ export class HoudiniClient {
22
23
  this.socket = subscriptionHandler
23
24
  }
24
25
 
26
+ handleMultipart(
27
+ params: FetchParams,
28
+ args: Parameters<FetchContext['fetch']>
29
+ ): Parameters<FetchContext['fetch']> | undefined {
30
+ // process any files that could be included
31
+ const { clone, files } = extractFiles({
32
+ query: params.text,
33
+ variables: params.variables,
34
+ })
35
+
36
+ // if there are files in the request
37
+ if (files.size) {
38
+ const [url, req] = args
39
+ let headers: Record<string, string> = {}
40
+
41
+ // filters `content-type: application/json` if received by client.ts
42
+ if (req?.headers) {
43
+ const filtered = Object.entries(req?.headers).filter(([key, value]) => {
44
+ return !(
45
+ key.toLowerCase() == 'content-type' &&
46
+ value.toLowerCase() == 'application/json'
47
+ )
48
+ })
49
+ headers = Object.fromEntries(filtered)
50
+ }
51
+
52
+ // See the GraphQL multipart request spec:
53
+ // https://github.com/jaydenseric/graphql-multipart-request-spec
54
+ const form = new FormData()
55
+ const operationJSON = JSON.stringify(clone)
56
+
57
+ form.set('operations', operationJSON)
58
+
59
+ const map: Record<string, Array<string>> = {}
60
+
61
+ let i = 0
62
+ files.forEach((paths) => {
63
+ map[++i] = paths
64
+ })
65
+ form.set('map', JSON.stringify(map))
66
+
67
+ i = 0
68
+ files.forEach((paths, file) => {
69
+ form.set(`${++i}`, file as Blob, (file as File).name)
70
+ })
71
+
72
+ return [url, { ...req, headers, body: form as any }]
73
+ }
74
+ }
75
+
25
76
  async sendRequest<_Data>(
26
77
  ctx: FetchContext,
27
78
  params: FetchParams
@@ -33,7 +84,11 @@ export class HoudiniClient {
33
84
  // wrap the user's fetch function so we can identify SSR by checking
34
85
  // the response.url
35
86
  fetch: async (...args: Parameters<FetchContext['fetch']>) => {
36
- const response = await ctx.fetch(...args)
87
+ // figure out if we need to do something special for multipart uploads
88
+ const newArgs = this.handleMultipart(params, args)
89
+
90
+ // use the new args if they exist, otherwise the old ones are good
91
+ const response = await ctx.fetch(...(newArgs || args))
37
92
  if (response.url) {
38
93
  url = response.url
39
94
  }
@@ -118,6 +173,7 @@ export async function executeQuery<_Data extends GraphQLObject, _Input extends {
118
173
  artifact,
119
174
  variables,
120
175
  session,
176
+ setFetching,
121
177
  cached,
122
178
  fetch,
123
179
  metadata,
@@ -126,6 +182,7 @@ export async function executeQuery<_Data extends GraphQLObject, _Input extends {
126
182
  artifact: QueryArtifact | MutationArtifact
127
183
  variables: _Input
128
184
  session: any
185
+ setFetching: (fetching: boolean) => void
129
186
  cached: boolean
130
187
  config: ConfigFile
131
188
  fetch?: typeof globalThis.fetch
@@ -139,6 +196,7 @@ export async function executeQuery<_Data extends GraphQLObject, _Input extends {
139
196
  session,
140
197
  },
141
198
  artifact,
199
+ setFetching,
142
200
  variables,
143
201
  cached,
144
202
  })
@@ -156,16 +214,18 @@ export async function executeQuery<_Data extends GraphQLObject, _Input extends {
156
214
 
157
215
  export async function fetchQuery<_Data extends GraphQLObject, _Input extends {}>({
158
216
  client,
217
+ context,
159
218
  artifact,
160
219
  variables,
220
+ setFetching,
161
221
  cached = true,
162
222
  policy,
163
- context,
164
223
  }: {
165
224
  client: HoudiniClient
166
225
  context: FetchContext
167
226
  artifact: QueryArtifact | MutationArtifact
168
227
  variables: _Input
228
+ setFetching: (fetching: boolean) => void
169
229
  cached?: boolean
170
230
  policy?: CachePolicy
171
231
  }): Promise<FetchQueryResult<_Data>> {
@@ -225,6 +285,9 @@ export async function fetchQuery<_Data extends GraphQLObject, _Input extends {}>
225
285
  cache._internal_unstable.collectGarbage()
226
286
  }, 0)
227
287
 
288
+ // tell everyone that we are fetching if the function is defined
289
+ setFetching(true)
290
+
228
291
  // the request must be resolved against the network
229
292
  const result = await client.sendRequest<_Data>(context, {
230
293
  text: artifact.raw,
@@ -0,0 +1,151 @@
1
+ /// This file contains a modified version, made by AlecAivazis, of the functions found here: https://github.com/jaydenseric/extract-files/blob/master/extractFiles.mjs
2
+ /// The associated license is at the end of the file (per the project's license agreement)
3
+
4
+ export function isExtractableFile(value: any): value is ExtractableFile {
5
+ return (
6
+ (typeof File !== 'undefined' && value instanceof File) ||
7
+ (typeof Blob !== 'undefined' && value instanceof Blob)
8
+ )
9
+ }
10
+
11
+ type ExtractableFile = File | Blob
12
+
13
+ /** @typedef {import("./isExtractableFile.mjs").default} isExtractableFile */
14
+
15
+ export function extractFiles(value: any) {
16
+ if (!arguments.length) throw new TypeError('Argument 1 `value` is required.')
17
+
18
+ /**
19
+ * Deeply clonable value.
20
+ * @typedef {Array<unknown> | FileList | Record<PropertyKey, unknown>} Cloneable
21
+ */
22
+
23
+ /**
24
+ * Clone of a {@link Cloneable deeply cloneable value}.
25
+ * @typedef {Exclude<Cloneable, FileList>} Clone
26
+ */
27
+
28
+ /**
29
+ * Map of values recursed within the input value and their clones, for reusing
30
+ * clones of values that are referenced multiple times within the input value.
31
+ * @type {Map<Cloneable, Clone>}
32
+ */
33
+ const clones = new Map()
34
+
35
+ /**
36
+ * Extracted files and their object paths within the input value.
37
+ * @type {Extraction<Extractable>["files"]}
38
+ */
39
+ const files = new Map()
40
+
41
+ /**
42
+ * Recursively clones the value, extracting files.
43
+ */
44
+ function recurse(value: any, path: string | string[], recursed: Set<any>) {
45
+ if (isExtractableFile(value)) {
46
+ const filePaths = files.get(value)
47
+
48
+ filePaths ? filePaths.push(path) : files.set(value, [path])
49
+
50
+ return null
51
+ }
52
+
53
+ const valueIsList =
54
+ Array.isArray(value) || (typeof FileList !== 'undefined' && value instanceof FileList)
55
+ const valueIsPlainObject = isPlainObject(value)
56
+
57
+ if (valueIsList || valueIsPlainObject) {
58
+ let clone = clones.get(value)
59
+
60
+ const uncloned = !clone
61
+
62
+ if (uncloned) {
63
+ clone = valueIsList
64
+ ? []
65
+ : // Replicate if the plain object is an `Object` instance.
66
+ value instanceof /** @type {any} */ Object
67
+ ? {}
68
+ : Object.create(null)
69
+
70
+ clones.set(value, /** @type {Clone} */ clone)
71
+ }
72
+
73
+ if (!recursed.has(value)) {
74
+ const pathPrefix = path ? `${path}.` : ''
75
+ const recursedDeeper = new Set(recursed).add(value)
76
+
77
+ if (valueIsList) {
78
+ let index = 0
79
+
80
+ // @ts-ignore
81
+ for (const item of value) {
82
+ const itemClone = recurse(item, pathPrefix + index++, recursedDeeper)
83
+
84
+ if (uncloned) /** @type {Array<unknown>} */ clone.push(itemClone)
85
+ }
86
+ } else
87
+ for (const key in value) {
88
+ const propertyClone = recurse(value[key], pathPrefix + key, recursedDeeper)
89
+
90
+ if (uncloned)
91
+ /** @type {Record<PropertyKey, unknown>} */ clone[key] = propertyClone
92
+ }
93
+ }
94
+
95
+ return clone
96
+ }
97
+
98
+ return value
99
+ }
100
+
101
+ return {
102
+ clone: recurse(value, '', new Set()),
103
+ files,
104
+ }
105
+ }
106
+
107
+ /**
108
+ * An extraction result.
109
+ * @template [Extractable=unknown] Extractable file type.
110
+ * @typedef {object} Extraction
111
+ * @prop {unknown} clone Clone of the original value with extracted files
112
+ * recursively replaced with `null`.
113
+ * @prop {Map<Extractable, Array<ObjectPath>>} files Extracted files and their
114
+ * object paths within the original value.
115
+ */
116
+
117
+ /**
118
+ * String notation for the path to a node in an object tree.
119
+ * @typedef {string} ObjectPath
120
+ * @see [`object-path` on npm](https://npm.im/object-path).
121
+ * @example
122
+ * An object path for object property `a`, array index `0`, object property `b`:
123
+ *
124
+ * ```
125
+ * a.0.b
126
+ * ```
127
+ */
128
+
129
+ function isPlainObject(value: any) {
130
+ if (typeof value !== 'object' || value === null) {
131
+ return false
132
+ }
133
+
134
+ const prototype = Object.getPrototypeOf(value)
135
+ return (
136
+ (prototype === null ||
137
+ prototype === Object.prototype ||
138
+ Object.getPrototypeOf(prototype) === null) &&
139
+ !(Symbol.toStringTag in value) &&
140
+ !(Symbol.iterator in value)
141
+ )
142
+ }
143
+
144
+ // MIT License
145
+ // Copyright Jayden Seric
146
+
147
+ // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
148
+
149
+ // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
150
+
151
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.