houdini 0.17.4 → 0.17.5
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/.turbo/turbo-compile.log +2 -2
- package/.turbo/turbo-typedefs.log +2 -2
- package/CHANGELOG.md +6 -0
- package/build/cmd-cjs/index.js +9087 -10224
- package/build/cmd-esm/index.js +9087 -10224
- package/build/codegen/transforms/paginate.d.ts +10 -11
- package/build/codegen-cjs/index.js +9092 -10229
- package/build/codegen-esm/index.js +9092 -10229
- package/build/lib-cjs/index.js +9221 -10358
- package/build/lib-esm/index.js +9221 -10358
- package/build/runtime/lib/network.d.ts +1 -0
- package/build/runtime/lib/networkUtils.d.ts +8 -0
- package/build/runtime-cjs/lib/network.d.ts +1 -0
- package/build/runtime-cjs/lib/network.js +33 -1
- package/build/runtime-cjs/lib/networkUtils.d.ts +8 -0
- package/build/runtime-cjs/lib/networkUtils.js +85 -0
- package/build/runtime-esm/lib/network.d.ts +1 -0
- package/build/runtime-esm/lib/network.js +33 -1
- package/build/runtime-esm/lib/networkUtils.d.ts +8 -0
- package/build/runtime-esm/lib/networkUtils.js +60 -0
- package/build/test-cjs/index.js +9091 -10228
- package/build/test-esm/index.js +9091 -10228
- package/build/vite-cjs/index.js +9149 -10286
- package/build/vite-esm/index.js +9149 -10286
- package/package.json +2 -2
- package/src/codegen/generators/artifacts/artifacts.test.ts +99 -66
- package/src/codegen/generators/artifacts/pagination.test.ts +12 -8
- package/src/codegen/generators/artifacts/policy.test.ts +12 -8
- package/src/codegen/generators/definitions/schema.test.ts +12 -36
- package/src/codegen/generators/persistedQueries/persistedQuery.test.ts +2 -2
- package/src/codegen/transforms/fragmentVariables.test.ts +24 -16
- package/src/codegen/transforms/paginate.test.ts +9 -6
- package/src/codegen/transforms/paginate.ts +2 -2
- package/src/runtime/lib/network.ts +58 -1
- package/src/runtime/lib/networkUtils.ts +151 -0
|
@@ -29,7 +29,7 @@ test('cache policy is persisted in artifact', async function () {
|
|
|
29
29
|
export default {
|
|
30
30
|
name: "CachedFriends",
|
|
31
31
|
kind: "HoudiniQuery",
|
|
32
|
-
hash: "
|
|
32
|
+
hash: "ea9bab33b9e934c92f813b96c5a86f88fa81fbd06a27045efc95c4506b01ece4",
|
|
33
33
|
|
|
34
34
|
raw: \`query CachedFriends {
|
|
35
35
|
user {
|
|
@@ -38,7 +38,8 @@ test('cache policy is persisted in artifact', async function () {
|
|
|
38
38
|
}
|
|
39
39
|
id
|
|
40
40
|
}
|
|
41
|
-
}
|
|
41
|
+
}
|
|
42
|
+
\`,
|
|
42
43
|
|
|
43
44
|
rootType: "Query",
|
|
44
45
|
|
|
@@ -103,7 +104,7 @@ test('can change default cache policy', async function () {
|
|
|
103
104
|
export default {
|
|
104
105
|
name: "CachedFriends",
|
|
105
106
|
kind: "HoudiniQuery",
|
|
106
|
-
hash: "
|
|
107
|
+
hash: "ea9bab33b9e934c92f813b96c5a86f88fa81fbd06a27045efc95c4506b01ece4",
|
|
107
108
|
|
|
108
109
|
raw: \`query CachedFriends {
|
|
109
110
|
user {
|
|
@@ -112,7 +113,8 @@ test('can change default cache policy', async function () {
|
|
|
112
113
|
}
|
|
113
114
|
id
|
|
114
115
|
}
|
|
115
|
-
}
|
|
116
|
+
}
|
|
117
|
+
\`,
|
|
116
118
|
|
|
117
119
|
rootType: "Query",
|
|
118
120
|
|
|
@@ -172,7 +174,7 @@ test('partial opt-in is persisted', async function () {
|
|
|
172
174
|
export default {
|
|
173
175
|
name: "CachedFriends",
|
|
174
176
|
kind: "HoudiniQuery",
|
|
175
|
-
hash: "
|
|
177
|
+
hash: "ea9bab33b9e934c92f813b96c5a86f88fa81fbd06a27045efc95c4506b01ece4",
|
|
176
178
|
|
|
177
179
|
raw: \`query CachedFriends {
|
|
178
180
|
user {
|
|
@@ -181,7 +183,8 @@ test('partial opt-in is persisted', async function () {
|
|
|
181
183
|
}
|
|
182
184
|
id
|
|
183
185
|
}
|
|
184
|
-
}
|
|
186
|
+
}
|
|
187
|
+
\`,
|
|
185
188
|
|
|
186
189
|
rootType: "Query",
|
|
187
190
|
|
|
@@ -246,7 +249,7 @@ test('can set default partial opt-in', async function () {
|
|
|
246
249
|
export default {
|
|
247
250
|
name: "CachedFriends",
|
|
248
251
|
kind: "HoudiniQuery",
|
|
249
|
-
hash: "
|
|
252
|
+
hash: "ea9bab33b9e934c92f813b96c5a86f88fa81fbd06a27045efc95c4506b01ece4",
|
|
250
253
|
|
|
251
254
|
raw: \`query CachedFriends {
|
|
252
255
|
user {
|
|
@@ -255,7 +258,8 @@ test('can set default partial opt-in', async function () {
|
|
|
255
258
|
}
|
|
256
259
|
id
|
|
257
260
|
}
|
|
258
|
-
}
|
|
261
|
+
}
|
|
262
|
+
\`,
|
|
259
263
|
|
|
260
264
|
rootType: "Query",
|
|
261
265
|
|
|
@@ -39,14 +39,10 @@ test('adds internal documents to schema', async function () {
|
|
|
39
39
|
"""
|
|
40
40
|
directive @paginate(name: String) on FIELD
|
|
41
41
|
|
|
42
|
-
"""
|
|
43
|
-
@prepend is used to tell the runtime to add the result to the end of the list
|
|
44
|
-
"""
|
|
42
|
+
"""@prepend is used to tell the runtime to add the result to the end of the list"""
|
|
45
43
|
directive @prepend(parentID: ID) on FRAGMENT_SPREAD
|
|
46
44
|
|
|
47
|
-
"""
|
|
48
|
-
@append is used to tell the runtime to add the result to the start of the list
|
|
49
|
-
"""
|
|
45
|
+
"""@append is used to tell the runtime to add the result to the start of the list"""
|
|
50
46
|
directive @append(parentID: ID) on FRAGMENT_SPREAD
|
|
51
47
|
|
|
52
48
|
"""
|
|
@@ -55,14 +51,10 @@ test('adds internal documents to schema', async function () {
|
|
|
55
51
|
"""
|
|
56
52
|
directive @parentID(value: ID!) on FRAGMENT_SPREAD
|
|
57
53
|
|
|
58
|
-
"""
|
|
59
|
-
@when is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)
|
|
60
|
-
"""
|
|
54
|
+
"""@when is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)"""
|
|
61
55
|
directive @when on FRAGMENT_SPREAD
|
|
62
56
|
|
|
63
|
-
"""
|
|
64
|
-
@when_not is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)
|
|
65
|
-
"""
|
|
57
|
+
"""@when_not is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)"""
|
|
66
58
|
directive @when_not on FRAGMENT_SPREAD
|
|
67
59
|
|
|
68
60
|
"""@arguments is used to define the arguments of a fragment"""
|
|
@@ -114,14 +106,10 @@ test('list operations are included', async function () {
|
|
|
114
106
|
"""
|
|
115
107
|
directive @paginate(name: String) on FIELD
|
|
116
108
|
|
|
117
|
-
"""
|
|
118
|
-
@prepend is used to tell the runtime to add the result to the end of the list
|
|
119
|
-
"""
|
|
109
|
+
"""@prepend is used to tell the runtime to add the result to the end of the list"""
|
|
120
110
|
directive @prepend(parentID: ID) on FRAGMENT_SPREAD
|
|
121
111
|
|
|
122
|
-
"""
|
|
123
|
-
@append is used to tell the runtime to add the result to the start of the list
|
|
124
|
-
"""
|
|
112
|
+
"""@append is used to tell the runtime to add the result to the start of the list"""
|
|
125
113
|
directive @append(parentID: ID) on FRAGMENT_SPREAD
|
|
126
114
|
|
|
127
115
|
"""
|
|
@@ -130,14 +118,10 @@ test('list operations are included', async function () {
|
|
|
130
118
|
"""
|
|
131
119
|
directive @parentID(value: ID!) on FRAGMENT_SPREAD
|
|
132
120
|
|
|
133
|
-
"""
|
|
134
|
-
@when is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)
|
|
135
|
-
"""
|
|
121
|
+
"""@when is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)"""
|
|
136
122
|
directive @when on FRAGMENT_SPREAD
|
|
137
123
|
|
|
138
|
-
"""
|
|
139
|
-
@when_not is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)
|
|
140
|
-
"""
|
|
124
|
+
"""@when_not is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)"""
|
|
141
125
|
directive @when_not on FRAGMENT_SPREAD
|
|
142
126
|
|
|
143
127
|
"""@arguments is used to define the arguments of a fragment"""
|
|
@@ -208,14 +192,10 @@ test("writing twice doesn't duplicate definitions", async function () {
|
|
|
208
192
|
"""
|
|
209
193
|
directive @paginate(name: String) on FIELD
|
|
210
194
|
|
|
211
|
-
"""
|
|
212
|
-
@prepend is used to tell the runtime to add the result to the end of the list
|
|
213
|
-
"""
|
|
195
|
+
"""@prepend is used to tell the runtime to add the result to the end of the list"""
|
|
214
196
|
directive @prepend(parentID: ID) on FRAGMENT_SPREAD
|
|
215
197
|
|
|
216
|
-
"""
|
|
217
|
-
@append is used to tell the runtime to add the result to the start of the list
|
|
218
|
-
"""
|
|
198
|
+
"""@append is used to tell the runtime to add the result to the start of the list"""
|
|
219
199
|
directive @append(parentID: ID) on FRAGMENT_SPREAD
|
|
220
200
|
|
|
221
201
|
"""
|
|
@@ -224,14 +204,10 @@ test("writing twice doesn't duplicate definitions", async function () {
|
|
|
224
204
|
"""
|
|
225
205
|
directive @parentID(value: ID!) on FRAGMENT_SPREAD
|
|
226
206
|
|
|
227
|
-
"""
|
|
228
|
-
@when is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)
|
|
229
|
-
"""
|
|
207
|
+
"""@when is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)"""
|
|
230
208
|
directive @when on FRAGMENT_SPREAD
|
|
231
209
|
|
|
232
|
-
"""
|
|
233
|
-
@when_not is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)
|
|
234
|
-
"""
|
|
210
|
+
"""@when_not is used to provide a conditional or in situations where it doesn't make sense (eg when removing or deleting a node.)"""
|
|
235
211
|
directive @when_not on FRAGMENT_SPREAD
|
|
236
212
|
|
|
237
213
|
"""@arguments is used to define the arguments of a fragment"""
|
|
@@ -19,8 +19,8 @@ test('generates an artifact for every document', async function () {
|
|
|
19
19
|
|
|
20
20
|
expect(JSON.parse((await fs.readFile(config.persistedQueryPath))!)).toMatchInlineSnapshot(`
|
|
21
21
|
{
|
|
22
|
-
"
|
|
23
|
-
"
|
|
22
|
+
"361432f464ed44eed788f3ea66c4dabc46437b88edbe7daccca87045fd31447f": "query TestQuery1 {\\n version\\n}\\n",
|
|
23
|
+
"17f12389123502b3d5d81202d0af249bdf0ec95cea480c9c12501ef627abd463": "query TestQuery2 {\\n user {\\n ...TestFragment\\n id\\n }\\n}\\n\\nfragment TestFragment on User {\\n firstName\\n}\\n"
|
|
24
24
|
}
|
|
25
25
|
`)
|
|
26
26
|
})
|
|
@@ -43,7 +43,7 @@ test('pass argument values to generated fragments', async function () {
|
|
|
43
43
|
export default {
|
|
44
44
|
name: "AllUsers",
|
|
45
45
|
kind: "HoudiniQuery",
|
|
46
|
-
hash: "
|
|
46
|
+
hash: "c346b9eaafaa74d18a267a74706e193e8080b9533d994d6e8489d7e5b534ee41",
|
|
47
47
|
|
|
48
48
|
raw: \`query AllUsers {
|
|
49
49
|
...QueryFragment_10b3uv
|
|
@@ -53,7 +53,8 @@ test('pass argument values to generated fragments', async function () {
|
|
|
53
53
|
users(stringValue: "Hello") {
|
|
54
54
|
id
|
|
55
55
|
}
|
|
56
|
-
}
|
|
56
|
+
}
|
|
57
|
+
\`,
|
|
57
58
|
|
|
58
59
|
rootType: "Query",
|
|
59
60
|
|
|
@@ -115,7 +116,7 @@ test("nullable arguments with no values don't show up in the query", async funct
|
|
|
115
116
|
export default {
|
|
116
117
|
name: "AllUsers",
|
|
117
118
|
kind: "HoudiniQuery",
|
|
118
|
-
hash: "
|
|
119
|
+
hash: "19b6a6cc9d06ab798cbf4b0a9530e07a3473b78e7d964cc9d6557d8240ed9012",
|
|
119
120
|
|
|
120
121
|
raw: \`query AllUsers {
|
|
121
122
|
...QueryFragment
|
|
@@ -125,7 +126,8 @@ test("nullable arguments with no values don't show up in the query", async funct
|
|
|
125
126
|
users {
|
|
126
127
|
id
|
|
127
128
|
}
|
|
128
|
-
}
|
|
129
|
+
}
|
|
130
|
+
\`,
|
|
129
131
|
|
|
130
132
|
rootType: "Query",
|
|
131
133
|
|
|
@@ -187,7 +189,7 @@ test("fragment arguments with default values don't rename the fragment", async f
|
|
|
187
189
|
export default {
|
|
188
190
|
name: "AllUsers",
|
|
189
191
|
kind: "HoudiniQuery",
|
|
190
|
-
hash: "
|
|
192
|
+
hash: "3835ee68277547d738cc8fd5051fe98799b5bd470516146906fa0f134a2b3891",
|
|
191
193
|
|
|
192
194
|
raw: \`query AllUsers {
|
|
193
195
|
...QueryFragment
|
|
@@ -197,7 +199,8 @@ test("fragment arguments with default values don't rename the fragment", async f
|
|
|
197
199
|
users(stringValue: "Hello") {
|
|
198
200
|
id
|
|
199
201
|
}
|
|
200
|
-
}
|
|
202
|
+
}
|
|
203
|
+
\`,
|
|
201
204
|
|
|
202
205
|
rootType: "Query",
|
|
203
206
|
|
|
@@ -267,7 +270,7 @@ test('thread query variables to inner fragments', async function () {
|
|
|
267
270
|
export default {
|
|
268
271
|
name: "AllUsers",
|
|
269
272
|
kind: "HoudiniQuery",
|
|
270
|
-
hash: "
|
|
273
|
+
hash: "8fa4273ab75455c901e7de893f72a28af4c001afbf204ceca2fd7ab30b7ff372",
|
|
271
274
|
|
|
272
275
|
raw: \`query AllUsers($name: String!) {
|
|
273
276
|
...QueryFragment_VDHGm
|
|
@@ -281,7 +284,8 @@ test('thread query variables to inner fragments', async function () {
|
|
|
281
284
|
users(stringValue: $name) {
|
|
282
285
|
id
|
|
283
286
|
}
|
|
284
|
-
}
|
|
287
|
+
}
|
|
288
|
+
\`,
|
|
285
289
|
|
|
286
290
|
rootType: "Query",
|
|
287
291
|
|
|
@@ -359,7 +363,7 @@ test('inner fragment with intermediate default value', async function () {
|
|
|
359
363
|
export default {
|
|
360
364
|
name: "AllUsers",
|
|
361
365
|
kind: "HoudiniQuery",
|
|
362
|
-
hash: "
|
|
366
|
+
hash: "d5753a3cae56b8133c72527cdccdd0c001effb48104b98806ac62dd9afeeb259",
|
|
363
367
|
|
|
364
368
|
raw: \`query AllUsers {
|
|
365
369
|
...QueryFragment
|
|
@@ -373,7 +377,8 @@ test('inner fragment with intermediate default value', async function () {
|
|
|
373
377
|
users(stringValue: "Hello", intValue: 2) {
|
|
374
378
|
id
|
|
375
379
|
}
|
|
376
|
-
}
|
|
380
|
+
}
|
|
381
|
+
\`,
|
|
377
382
|
|
|
378
383
|
rootType: "Query",
|
|
379
384
|
|
|
@@ -443,7 +448,7 @@ test("default values don't overwrite unless explicitly passed", async function (
|
|
|
443
448
|
export default {
|
|
444
449
|
name: "AllUsers",
|
|
445
450
|
kind: "HoudiniQuery",
|
|
446
|
-
hash: "
|
|
451
|
+
hash: "b155b401cdbdfe0f63dd47575fbcfb2aa90678e7530b93476c4efe559405cf4f",
|
|
447
452
|
|
|
448
453
|
raw: \`query AllUsers {
|
|
449
454
|
...QueryFragment
|
|
@@ -457,7 +462,8 @@ test("default values don't overwrite unless explicitly passed", async function (
|
|
|
457
462
|
users(stringValue: "Goodbye", intValue: 10) {
|
|
458
463
|
id
|
|
459
464
|
}
|
|
460
|
-
}
|
|
465
|
+
}
|
|
466
|
+
\`,
|
|
461
467
|
|
|
462
468
|
rootType: "Query",
|
|
463
469
|
|
|
@@ -519,7 +525,7 @@ test('default arguments', async function () {
|
|
|
519
525
|
export default {
|
|
520
526
|
name: "AllUsers",
|
|
521
527
|
kind: "HoudiniQuery",
|
|
522
|
-
hash: "
|
|
528
|
+
hash: "5c4a8d1fe2e117286ecdfbd273bf1beb2f71a0a3fd9ea6bc84fe97c394c1a836",
|
|
523
529
|
|
|
524
530
|
raw: \`query AllUsers {
|
|
525
531
|
...QueryFragment
|
|
@@ -529,7 +535,8 @@ test('default arguments', async function () {
|
|
|
529
535
|
users(boolValue: true, stringValue: "Hello") {
|
|
530
536
|
id
|
|
531
537
|
}
|
|
532
|
-
}
|
|
538
|
+
}
|
|
539
|
+
\`,
|
|
533
540
|
|
|
534
541
|
rootType: "Query",
|
|
535
542
|
|
|
@@ -591,7 +598,7 @@ test('multiple with directives - no overlap', async function () {
|
|
|
591
598
|
export default {
|
|
592
599
|
name: "AllUsers",
|
|
593
600
|
kind: "HoudiniQuery",
|
|
594
|
-
hash: "
|
|
601
|
+
hash: "7327e6f7f6c8339feebb640b995c3e25efe1b25de29b1f43cb55c2a0566f713f",
|
|
595
602
|
|
|
596
603
|
raw: \`query AllUsers {
|
|
597
604
|
...QueryFragment_2prn0K
|
|
@@ -601,7 +608,8 @@ test('multiple with directives - no overlap', async function () {
|
|
|
601
608
|
users(boolValue: false, stringValue: "Goodbye") {
|
|
602
609
|
id
|
|
603
610
|
}
|
|
604
|
-
}
|
|
611
|
+
}
|
|
612
|
+
\`,
|
|
605
613
|
|
|
606
614
|
rootType: "Query",
|
|
607
615
|
|
|
@@ -450,7 +450,7 @@ test('embeds node pagination query as a separate document', async function () {
|
|
|
450
450
|
export default {
|
|
451
451
|
name: "UserFriends_Pagination_Query",
|
|
452
452
|
kind: "HoudiniQuery",
|
|
453
|
-
hash: "
|
|
453
|
+
hash: "bb5131f921805b85c17e7b882f4ad66a9dad452d0f66534a1c8b8f9942adec48",
|
|
454
454
|
|
|
455
455
|
refetch: {
|
|
456
456
|
update: "append",
|
|
@@ -491,7 +491,8 @@ test('embeds node pagination query as a separate document', async function () {
|
|
|
491
491
|
endCursor
|
|
492
492
|
}
|
|
493
493
|
}
|
|
494
|
-
}
|
|
494
|
+
}
|
|
495
|
+
\`,
|
|
495
496
|
|
|
496
497
|
rootType: "Query",
|
|
497
498
|
|
|
@@ -640,7 +641,7 @@ test('embeds custom pagination query as a separate document', async function ()
|
|
|
640
641
|
export default {
|
|
641
642
|
name: "UserGhost_Pagination_Query",
|
|
642
643
|
kind: "HoudiniQuery",
|
|
643
|
-
hash: "
|
|
644
|
+
hash: "55c27b299d485bf73adfaa418b77ac03d918e2ce579730d328208318c6af0da5",
|
|
644
645
|
|
|
645
646
|
refetch: {
|
|
646
647
|
update: "append",
|
|
@@ -683,7 +684,8 @@ test('embeds custom pagination query as a separate document', async function ()
|
|
|
683
684
|
endCursor
|
|
684
685
|
}
|
|
685
686
|
}
|
|
686
|
-
}
|
|
687
|
+
}
|
|
688
|
+
\`,
|
|
687
689
|
|
|
688
690
|
rootType: "Query",
|
|
689
691
|
|
|
@@ -1283,7 +1285,7 @@ test('generated query has same refetch spec', async function () {
|
|
|
1283
1285
|
export default {
|
|
1284
1286
|
name: "UserFriends_Pagination_Query",
|
|
1285
1287
|
kind: "HoudiniQuery",
|
|
1286
|
-
hash: "
|
|
1288
|
+
hash: "5aeb471edf15c5b3e709ddccc6014f073d2dfdc1259d04b7ee26887ea81ef23b",
|
|
1287
1289
|
|
|
1288
1290
|
refetch: {
|
|
1289
1291
|
update: "append",
|
|
@@ -1321,7 +1323,8 @@ test('generated query has same refetch spec', async function () {
|
|
|
1321
1323
|
endCursor
|
|
1322
1324
|
}
|
|
1323
1325
|
}
|
|
1324
|
-
}
|
|
1326
|
+
}
|
|
1327
|
+
\`,
|
|
1325
1328
|
|
|
1326
1329
|
rootType: "Query",
|
|
1327
1330
|
|
|
@@ -400,7 +400,7 @@ export default async function paginate(
|
|
|
400
400
|
kind: graphql.Kind.NAME,
|
|
401
401
|
value: refetchQueryName,
|
|
402
402
|
},
|
|
403
|
-
operation:
|
|
403
|
+
operation: 'query',
|
|
404
404
|
variableDefinitions: paginationArgs
|
|
405
405
|
.map(
|
|
406
406
|
(arg) =>
|
|
@@ -637,7 +637,7 @@ function objectNode([type, defaultValue]: [
|
|
|
637
637
|
number | string | undefined
|
|
638
638
|
]): graphql.ObjectValueNode {
|
|
639
639
|
const node = {
|
|
640
|
-
kind: graphql.Kind.OBJECT
|
|
640
|
+
kind: graphql.Kind.OBJECT,
|
|
641
641
|
fields: [
|
|
642
642
|
{
|
|
643
643
|
kind: graphql.Kind.OBJECT_FIELD,
|
|
@@ -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,58 @@ 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
|
+
const [url, req] = args
|
|
31
|
+
|
|
32
|
+
// process any files that could be included
|
|
33
|
+
const { clone, files } = extractFiles({
|
|
34
|
+
query: params.text,
|
|
35
|
+
variables: params.variables,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const operationJSON = JSON.stringify(clone)
|
|
39
|
+
|
|
40
|
+
// if there are files in the request
|
|
41
|
+
if (files.size) {
|
|
42
|
+
let headers: Record<string, string> = {}
|
|
43
|
+
|
|
44
|
+
// filters `content-type: application/json` if received by client.ts
|
|
45
|
+
if (req?.headers) {
|
|
46
|
+
const filtered = Object.entries(req?.headers).filter(([key, value]) => {
|
|
47
|
+
return !(
|
|
48
|
+
key.toLowerCase() == 'content-type' &&
|
|
49
|
+
value.toLowerCase() == 'application/json'
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
headers = Object.fromEntries(filtered)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// See the GraphQL multipart request spec:
|
|
56
|
+
// https://github.com/jaydenseric/graphql-multipart-request-spec
|
|
57
|
+
const form = new FormData()
|
|
58
|
+
|
|
59
|
+
form.set('operations', operationJSON)
|
|
60
|
+
|
|
61
|
+
const map: Record<string, Array<string>> = {}
|
|
62
|
+
|
|
63
|
+
let i = 0
|
|
64
|
+
files.forEach((paths) => {
|
|
65
|
+
map[++i] = paths
|
|
66
|
+
})
|
|
67
|
+
form.set('map', JSON.stringify(map))
|
|
68
|
+
|
|
69
|
+
i = 0
|
|
70
|
+
files.forEach((paths, file) => {
|
|
71
|
+
form.set(`${++i}`, file as Blob, (file as File).name)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return [url, { ...req, headers, body: form as any }]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
25
78
|
async sendRequest<_Data>(
|
|
26
79
|
ctx: FetchContext,
|
|
27
80
|
params: FetchParams
|
|
@@ -33,7 +86,11 @@ export class HoudiniClient {
|
|
|
33
86
|
// wrap the user's fetch function so we can identify SSR by checking
|
|
34
87
|
// the response.url
|
|
35
88
|
fetch: async (...args: Parameters<FetchContext['fetch']>) => {
|
|
36
|
-
|
|
89
|
+
// figure out if we need to do something special for multipart uploads
|
|
90
|
+
const newArgs = this.handleMultipart(params, args)
|
|
91
|
+
|
|
92
|
+
// use the new args if they exist, otherwise the old ones are good
|
|
93
|
+
const response = await ctx.fetch(...(newArgs || args))
|
|
37
94
|
if (response.url) {
|
|
38
95
|
url = response.url
|
|
39
96
|
}
|
|
@@ -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.
|