houdini-svelte 0.17.0-next.0
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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/package.json +57 -0
- package/src/plugin/codegen/adapter.ts +45 -0
- package/src/plugin/codegen/components/index.ts +149 -0
- package/src/plugin/codegen/index.ts +28 -0
- package/src/plugin/codegen/routes/index.ts +307 -0
- package/src/plugin/codegen/routes/kit.test.ts +276 -0
- package/src/plugin/codegen/stores/fragment.ts +83 -0
- package/src/plugin/codegen/stores/index.ts +55 -0
- package/src/plugin/codegen/stores/mutation.ts +56 -0
- package/src/plugin/codegen/stores/query.test.ts +504 -0
- package/src/plugin/codegen/stores/query.ts +97 -0
- package/src/plugin/codegen/stores/subscription.ts +57 -0
- package/src/plugin/extract.test.ts +290 -0
- package/src/plugin/extract.ts +127 -0
- package/src/plugin/extractLoadFunction.test.ts +247 -0
- package/src/plugin/extractLoadFunction.ts +249 -0
- package/src/plugin/fsPatch.ts +238 -0
- package/src/plugin/imports.ts +28 -0
- package/src/plugin/index.ts +165 -0
- package/src/plugin/kit.ts +382 -0
- package/src/plugin/transforms/index.ts +90 -0
- package/src/plugin/transforms/kit/index.ts +20 -0
- package/src/plugin/transforms/kit/init.test.ts +28 -0
- package/src/plugin/transforms/kit/init.ts +75 -0
- package/src/plugin/transforms/kit/load.test.ts +1234 -0
- package/src/plugin/transforms/kit/load.ts +506 -0
- package/src/plugin/transforms/kit/session.test.ts +268 -0
- package/src/plugin/transforms/kit/session.ts +161 -0
- package/src/plugin/transforms/query.test.ts +99 -0
- package/src/plugin/transforms/query.ts +263 -0
- package/src/plugin/transforms/reactive.ts +126 -0
- package/src/plugin/transforms/tags.ts +20 -0
- package/src/plugin/transforms/types.ts +9 -0
- package/src/plugin/validate.test.ts +95 -0
- package/src/plugin/validate.ts +50 -0
- package/src/preprocess/index.ts +33 -0
- package/src/runtime/adapter.ts +21 -0
- package/src/runtime/fragments.ts +86 -0
- package/src/runtime/index.ts +72 -0
- package/src/runtime/network.ts +6 -0
- package/src/runtime/session.ts +187 -0
- package/src/runtime/stores/fragment.ts +48 -0
- package/src/runtime/stores/index.ts +5 -0
- package/src/runtime/stores/mutation.ts +185 -0
- package/src/runtime/stores/pagination/cursor.ts +265 -0
- package/src/runtime/stores/pagination/fetch.ts +7 -0
- package/src/runtime/stores/pagination/fragment.ts +236 -0
- package/src/runtime/stores/pagination/index.ts +7 -0
- package/src/runtime/stores/pagination/offset.ts +162 -0
- package/src/runtime/stores/pagination/pageInfo.test.ts +39 -0
- package/src/runtime/stores/pagination/pageInfo.ts +67 -0
- package/src/runtime/stores/pagination/query.ts +132 -0
- package/src/runtime/stores/query.ts +524 -0
- package/src/runtime/stores/store.ts +13 -0
- package/src/runtime/stores/subscription.ts +107 -0
- package/src/runtime/types.ts +40 -0
- package/src/test/index.ts +208 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { getCache } from '$houdini/runtime'
|
|
2
|
+
import { ConfigFile } from '$houdini/runtime/lib/config'
|
|
3
|
+
import { siteURL } from '$houdini/runtime/lib/constants'
|
|
4
|
+
import { deepEquals } from '$houdini/runtime/lib/deepEquals'
|
|
5
|
+
import { executeQuery } from '$houdini/runtime/lib/network'
|
|
6
|
+
import { GraphQLObject, QueryArtifact, QueryResult } from '$houdini/runtime/lib/types'
|
|
7
|
+
import { Writable, writable } from 'svelte/store'
|
|
8
|
+
|
|
9
|
+
import { getCurrentClient } from '../../network'
|
|
10
|
+
import { getSession } from '../../session'
|
|
11
|
+
import { QueryStoreFetchParams } from '../query'
|
|
12
|
+
import { fetchParams } from '../query'
|
|
13
|
+
import { FetchFn } from './fetch'
|
|
14
|
+
import { countPage, extractPageInfo, missingPageSizeError, PageInfo } from './pageInfo'
|
|
15
|
+
|
|
16
|
+
export function cursorHandlers<_Data extends GraphQLObject, _Input>({
|
|
17
|
+
artifact,
|
|
18
|
+
queryVariables: extraVariables,
|
|
19
|
+
setFetching,
|
|
20
|
+
fetch,
|
|
21
|
+
storeName,
|
|
22
|
+
getValue,
|
|
23
|
+
getConfig,
|
|
24
|
+
}: {
|
|
25
|
+
artifact: QueryArtifact
|
|
26
|
+
getValue: () => _Data | null
|
|
27
|
+
queryVariables: () => Promise<_Input | null>
|
|
28
|
+
setFetching: (val: boolean) => void
|
|
29
|
+
fetch: FetchFn<_Data, _Input>
|
|
30
|
+
storeName: string
|
|
31
|
+
getConfig: () => Promise<ConfigFile>
|
|
32
|
+
}): CursorHandlers<_Data, _Input> {
|
|
33
|
+
const pageInfo = writable<PageInfo>(extractPageInfo(getValue(), artifact.refetch!.path))
|
|
34
|
+
|
|
35
|
+
// dry up the page-loading logic
|
|
36
|
+
const loadPage = async ({
|
|
37
|
+
pageSizeVar,
|
|
38
|
+
input,
|
|
39
|
+
functionName,
|
|
40
|
+
metadata = {},
|
|
41
|
+
fetch,
|
|
42
|
+
}: {
|
|
43
|
+
pageSizeVar: string
|
|
44
|
+
functionName: string
|
|
45
|
+
input: {}
|
|
46
|
+
metadata?: {}
|
|
47
|
+
fetch?: typeof globalThis.fetch
|
|
48
|
+
}) => {
|
|
49
|
+
const config = await getConfig()
|
|
50
|
+
const client = await getCurrentClient()
|
|
51
|
+
|
|
52
|
+
// set the loading state to true
|
|
53
|
+
setFetching(true)
|
|
54
|
+
|
|
55
|
+
// build up the variables to pass to the query
|
|
56
|
+
const loadVariables: Record<string, any> = {
|
|
57
|
+
...(await extraVariables?.()),
|
|
58
|
+
...input,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// if we don't have a value for the page size, tell the user
|
|
62
|
+
if (!loadVariables[pageSizeVar] && !artifact.refetch!.pageSize) {
|
|
63
|
+
throw missingPageSizeError(functionName)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// send the query
|
|
67
|
+
const { result } = await executeQuery<GraphQLObject, {}>({
|
|
68
|
+
client,
|
|
69
|
+
artifact,
|
|
70
|
+
variables: loadVariables,
|
|
71
|
+
session: await getSession(),
|
|
72
|
+
cached: false,
|
|
73
|
+
config,
|
|
74
|
+
fetch,
|
|
75
|
+
metadata,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// if the query is embedded in a node field (paginated fragments)
|
|
79
|
+
// make sure we look down one more for the updated page info
|
|
80
|
+
const resultPath = [...artifact.refetch!.path]
|
|
81
|
+
if (artifact.refetch!.embedded) {
|
|
82
|
+
const { targetType } = artifact.refetch!
|
|
83
|
+
// make sure we have a type config for the pagination target type
|
|
84
|
+
if (!config.types?.[targetType]?.resolve) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Missing type resolve configuration for ${targetType}. For more information, see ${siteURL}/guides/pagination#paginated-fragments`
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// make sure that we pull the value out of the correct query field
|
|
91
|
+
resultPath.unshift(config.types[targetType].resolve!.queryField)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// we need to find the connection object holding the current page info
|
|
95
|
+
pageInfo.set(extractPageInfo(result.data, resultPath))
|
|
96
|
+
|
|
97
|
+
// update cache with the result
|
|
98
|
+
getCache().write({
|
|
99
|
+
selection: artifact.selection,
|
|
100
|
+
data: result.data,
|
|
101
|
+
variables: loadVariables,
|
|
102
|
+
applyUpdates: true,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// we're not loading any more
|
|
106
|
+
setFetching(false)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
loadNextPage: async ({
|
|
111
|
+
first,
|
|
112
|
+
after,
|
|
113
|
+
fetch,
|
|
114
|
+
metadata,
|
|
115
|
+
}: {
|
|
116
|
+
first?: number
|
|
117
|
+
after?: string
|
|
118
|
+
fetch?: typeof globalThis.fetch
|
|
119
|
+
metadata?: {}
|
|
120
|
+
} = {}) => {
|
|
121
|
+
// we need to find the connection object holding the current page info
|
|
122
|
+
const currentPageInfo = extractPageInfo(getValue(), artifact.refetch!.path)
|
|
123
|
+
|
|
124
|
+
// if there is no next page, we're done
|
|
125
|
+
if (!currentPageInfo.hasNextPage) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// only specify the page count if we're given one
|
|
130
|
+
const input: Record<string, any> = {
|
|
131
|
+
after: after ?? currentPageInfo.endCursor,
|
|
132
|
+
}
|
|
133
|
+
if (first) {
|
|
134
|
+
input.first = first
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// load the page
|
|
138
|
+
return await loadPage({
|
|
139
|
+
pageSizeVar: 'first',
|
|
140
|
+
functionName: 'loadNextPage',
|
|
141
|
+
input,
|
|
142
|
+
fetch,
|
|
143
|
+
metadata,
|
|
144
|
+
})
|
|
145
|
+
},
|
|
146
|
+
loadPreviousPage: async ({
|
|
147
|
+
last,
|
|
148
|
+
before,
|
|
149
|
+
fetch,
|
|
150
|
+
metadata,
|
|
151
|
+
}: {
|
|
152
|
+
last?: number
|
|
153
|
+
before?: string
|
|
154
|
+
fetch?: typeof globalThis.fetch
|
|
155
|
+
metadata?: {}
|
|
156
|
+
} = {}) => {
|
|
157
|
+
// we need to find the connection object holding the current page info
|
|
158
|
+
const currentPageInfo = extractPageInfo(getValue(), artifact.refetch!.path)
|
|
159
|
+
|
|
160
|
+
// if there is no next page, we're done
|
|
161
|
+
if (!currentPageInfo.hasPreviousPage) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// only specify the page count if we're given one
|
|
166
|
+
const input: Record<string, any> = {
|
|
167
|
+
before: before ?? currentPageInfo.startCursor,
|
|
168
|
+
}
|
|
169
|
+
if (last) {
|
|
170
|
+
input.last = last
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// load the page
|
|
174
|
+
return await loadPage({
|
|
175
|
+
pageSizeVar: 'last',
|
|
176
|
+
functionName: 'loadPreviousPage',
|
|
177
|
+
input,
|
|
178
|
+
fetch,
|
|
179
|
+
metadata,
|
|
180
|
+
})
|
|
181
|
+
},
|
|
182
|
+
pageInfo,
|
|
183
|
+
async fetch(
|
|
184
|
+
args?: QueryStoreFetchParams<_Data, _Input>
|
|
185
|
+
): Promise<QueryResult<_Data, _Input>> {
|
|
186
|
+
// validate and prepare the request context for the current environment (client vs server)
|
|
187
|
+
const { params } = await fetchParams(artifact, storeName, args)
|
|
188
|
+
|
|
189
|
+
const { variables } = params ?? {}
|
|
190
|
+
|
|
191
|
+
// build up the variables to pass to the query
|
|
192
|
+
const extra = await extraVariables()
|
|
193
|
+
const queryVariables: Record<string, any> = {
|
|
194
|
+
...extra,
|
|
195
|
+
...variables,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// if the input is different than the query variables then we just do everything like normal
|
|
199
|
+
if (variables && !deepEquals(extra, variables)) {
|
|
200
|
+
const result = await fetch({
|
|
201
|
+
...params,
|
|
202
|
+
then(data) {
|
|
203
|
+
pageInfo.set(extractPageInfo(data, artifact.refetch!.path))
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
return result
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// we are updating the current set of items, count the number of items that currently exist
|
|
210
|
+
// and ask for the full data set
|
|
211
|
+
const count =
|
|
212
|
+
countPage(artifact.refetch!.path.concat('edges'), getValue()) ||
|
|
213
|
+
artifact.refetch!.pageSize
|
|
214
|
+
|
|
215
|
+
// if there are more records than the first page, we need fetch to load everything
|
|
216
|
+
if (count && count > artifact.refetch!.pageSize) {
|
|
217
|
+
// reverse cursors need the last entries in the list
|
|
218
|
+
queryVariables[artifact.refetch!.update === 'prepend' ? 'last' : 'first'] = count
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// set the loading state to true
|
|
222
|
+
setFetching(true)
|
|
223
|
+
|
|
224
|
+
// send the query
|
|
225
|
+
const result = await fetch({
|
|
226
|
+
...params,
|
|
227
|
+
variables: queryVariables as _Input,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
// keep the page info store up to date
|
|
231
|
+
pageInfo.set(extractPageInfo(result.data, artifact.refetch!.path))
|
|
232
|
+
|
|
233
|
+
// we're not loading any more
|
|
234
|
+
setFetching(false)
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
data: result.data,
|
|
238
|
+
variables: queryVariables as _Input,
|
|
239
|
+
isFetching: false,
|
|
240
|
+
partial: result.partial,
|
|
241
|
+
errors: null,
|
|
242
|
+
source: result.source,
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export type CursorHandlers<_Data extends GraphQLObject, _Input> = {
|
|
249
|
+
loadNextPage: (args?: {
|
|
250
|
+
first?: number
|
|
251
|
+
after?: string
|
|
252
|
+
fetch?: typeof globalThis.fetch
|
|
253
|
+
metadata: {}
|
|
254
|
+
}) => Promise<void>
|
|
255
|
+
loadPreviousPage: (args?: {
|
|
256
|
+
last?: number
|
|
257
|
+
before?: string
|
|
258
|
+
fetch?: typeof globalThis.fetch
|
|
259
|
+
metadata?: {}
|
|
260
|
+
}) => Promise<void>
|
|
261
|
+
pageInfo: Writable<PageInfo>
|
|
262
|
+
fetch(
|
|
263
|
+
args?: QueryStoreFetchParams<_Data, _Input> | undefined
|
|
264
|
+
): Promise<QueryResult<_Data, _Input>>
|
|
265
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { GraphQLObject, QueryResult } from '$houdini/runtime/lib/types'
|
|
2
|
+
|
|
3
|
+
import { QueryStoreFetchParams } from '../query'
|
|
4
|
+
|
|
5
|
+
export type FetchFn<_Data extends GraphQLObject, _Input = any> = (
|
|
6
|
+
params?: QueryStoreFetchParams<_Data, _Input>
|
|
7
|
+
) => Promise<QueryResult<_Data, _Input>>
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { keyFieldsForType, getCurrentConfig } from '$houdini/runtime/lib/config'
|
|
2
|
+
import { siteURL } from '$houdini/runtime/lib/constants'
|
|
3
|
+
import {
|
|
4
|
+
GraphQLObject,
|
|
5
|
+
FragmentArtifact,
|
|
6
|
+
QueryArtifact,
|
|
7
|
+
HoudiniFetchContext,
|
|
8
|
+
CompiledFragmentKind,
|
|
9
|
+
} from '$houdini/runtime/lib/types'
|
|
10
|
+
import { derived, get, Readable, Subscriber, Writable, writable } from 'svelte/store'
|
|
11
|
+
|
|
12
|
+
import { StoreConfig } from '../query'
|
|
13
|
+
import { BaseStore } from '../store'
|
|
14
|
+
import { cursorHandlers, CursorHandlers } from './cursor'
|
|
15
|
+
import { offsetHandlers } from './offset'
|
|
16
|
+
import { nullPageInfo, PageInfo } from './pageInfo'
|
|
17
|
+
|
|
18
|
+
type FragmentStoreConfig<_Data extends GraphQLObject, _Input> = StoreConfig<
|
|
19
|
+
_Data,
|
|
20
|
+
_Input,
|
|
21
|
+
FragmentArtifact
|
|
22
|
+
> & { paginationArtifact: QueryArtifact }
|
|
23
|
+
|
|
24
|
+
class BasePaginatedFragmentStore<_Data extends GraphQLObject, _Input> extends BaseStore {
|
|
25
|
+
// all paginated stores need to have a flag to distinguish from other fragment stores
|
|
26
|
+
paginated = true
|
|
27
|
+
|
|
28
|
+
protected paginationArtifact: QueryArtifact
|
|
29
|
+
name: string
|
|
30
|
+
kind = CompiledFragmentKind
|
|
31
|
+
|
|
32
|
+
constructor(config: FragmentStoreConfig<_Data, _Input>) {
|
|
33
|
+
super()
|
|
34
|
+
this.paginationArtifact = config.paginationArtifact
|
|
35
|
+
this.name = config.storeName
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected async queryVariables(
|
|
39
|
+
store: Readable<FragmentPaginatedResult<_Data, unknown>>
|
|
40
|
+
): Promise<_Input> {
|
|
41
|
+
const config = await getCurrentConfig()
|
|
42
|
+
|
|
43
|
+
const { targetType } = this.paginationArtifact.refetch || {}
|
|
44
|
+
const typeConfig = config.types?.[targetType || '']
|
|
45
|
+
if (!typeConfig) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Missing type refetch configuration for ${targetType}. For more information, see ${siteURL}/guides/pagination#paginated-fragments`
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// if we have a specific function to use when computing the variables
|
|
52
|
+
// then we need to collect those fields
|
|
53
|
+
let idVariables = {}
|
|
54
|
+
const value = get(store).data
|
|
55
|
+
if (typeConfig.resolve?.arguments) {
|
|
56
|
+
// @ts-ignore
|
|
57
|
+
idVariables = (typeConfig.resolve!.arguments?.(value) || {}) as _Input
|
|
58
|
+
} else {
|
|
59
|
+
const keys = keyFieldsForType(config, targetType || '')
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
idVariables = Object.fromEntries(keys.map((key) => [key, value[key]])) as _Input
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// add the id variables to the query variables
|
|
65
|
+
return {
|
|
66
|
+
...idVariables,
|
|
67
|
+
} as _Input
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// both cursor paginated stores add a page info to their subscribe
|
|
72
|
+
class FragmentStoreCursor<_Data extends GraphQLObject, _Input> extends BasePaginatedFragmentStore<
|
|
73
|
+
_Data,
|
|
74
|
+
_Input
|
|
75
|
+
> {
|
|
76
|
+
// we want to add the cursor-based fetch to the return value of get
|
|
77
|
+
get(initialValue: _Data | null) {
|
|
78
|
+
const store = writable<FragmentPaginatedResult<_Data, { pageInfo: PageInfo }>>({
|
|
79
|
+
data: initialValue,
|
|
80
|
+
isFetching: false,
|
|
81
|
+
pageInfo: nullPageInfo(),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// track the loading state
|
|
85
|
+
const loading = writable(false)
|
|
86
|
+
|
|
87
|
+
// generate the pagination handlers
|
|
88
|
+
const handlers = this.storeHandlers(store, loading.set)
|
|
89
|
+
|
|
90
|
+
const subscribe = (
|
|
91
|
+
run: Subscriber<FragmentPaginatedResult<_Data, { pageInfo: PageInfo }>>,
|
|
92
|
+
invalidate?:
|
|
93
|
+
| ((
|
|
94
|
+
value?: FragmentPaginatedResult<_Data, { pageInfo: PageInfo }> | undefined
|
|
95
|
+
) => void)
|
|
96
|
+
| undefined
|
|
97
|
+
): (() => void) => {
|
|
98
|
+
const combined = derived(
|
|
99
|
+
[store, handlers.pageInfo],
|
|
100
|
+
([$parent, $pageInfo]) =>
|
|
101
|
+
({
|
|
102
|
+
...$parent,
|
|
103
|
+
pageInfo: $pageInfo,
|
|
104
|
+
} as FragmentPaginatedResult<_Data, { pageInfo: PageInfo }>)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return combined.subscribe(run, invalidate)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
kind: CompiledFragmentKind,
|
|
112
|
+
data: store,
|
|
113
|
+
subscribe: subscribe,
|
|
114
|
+
loading: loading as Readable<boolean>,
|
|
115
|
+
fetch: handlers.fetch,
|
|
116
|
+
pageInfo: handlers.pageInfo,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
protected storeHandlers(
|
|
121
|
+
store: Readable<FragmentPaginatedResult<_Data, unknown>>,
|
|
122
|
+
setFetching: (val: boolean) => void
|
|
123
|
+
): CursorHandlers<_Data, _Input> {
|
|
124
|
+
return cursorHandlers<_Data, _Input>({
|
|
125
|
+
artifact: this.paginationArtifact,
|
|
126
|
+
fetch: async () => {
|
|
127
|
+
return {} as any
|
|
128
|
+
},
|
|
129
|
+
getValue: () => get(store).data,
|
|
130
|
+
queryVariables: () => this.queryVariables(store),
|
|
131
|
+
setFetching,
|
|
132
|
+
storeName: this.name,
|
|
133
|
+
getConfig: () => this.getConfig(),
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// FragmentStoreForwardCursor adds loadNextPage to FragmentStoreCursor
|
|
139
|
+
export class FragmentStoreForwardCursor<
|
|
140
|
+
_Data extends GraphQLObject,
|
|
141
|
+
_Input
|
|
142
|
+
> extends FragmentStoreCursor<_Data, _Input> {
|
|
143
|
+
get(initialValue: _Data | null) {
|
|
144
|
+
// get the base class
|
|
145
|
+
const parent = super.get(initialValue)
|
|
146
|
+
|
|
147
|
+
// generate the pagination handlers
|
|
148
|
+
const handlers = this.storeHandlers(
|
|
149
|
+
parent,
|
|
150
|
+
// it really is a writable under the hood :(
|
|
151
|
+
(parent.loading as unknown as Writable<boolean>).set
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
...parent,
|
|
156
|
+
// add the specific handlers for this situation
|
|
157
|
+
loadNextPage: handlers.loadNextPage,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// BackwardFragmentStoreCursor adds loadPreviousPage to FragmentStoreCursor
|
|
163
|
+
export class FragmentStoreBackwardCursor<
|
|
164
|
+
_Data extends GraphQLObject,
|
|
165
|
+
_Input
|
|
166
|
+
> extends FragmentStoreCursor<_Data, _Input> {
|
|
167
|
+
get(initialValue: _Data | null) {
|
|
168
|
+
const parent = super.get(initialValue)
|
|
169
|
+
|
|
170
|
+
// generate the pagination handlers
|
|
171
|
+
const handlers = this.storeHandlers(
|
|
172
|
+
parent,
|
|
173
|
+
// it really is a writable under the hood :(
|
|
174
|
+
(isFetching: boolean) => parent.data.update((p) => ({ ...p, isFetching }))
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...parent,
|
|
179
|
+
// add the specific handlers for this situation
|
|
180
|
+
loadPreviousPage: handlers.loadPreviousPage,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export class FragmentStoreOffset<
|
|
186
|
+
_Data extends GraphQLObject,
|
|
187
|
+
_Input
|
|
188
|
+
> extends BasePaginatedFragmentStore<_Data, _Input> {
|
|
189
|
+
get(initialValue: _Data | null) {
|
|
190
|
+
const parent = writable<FragmentPaginatedResult<_Data>>({
|
|
191
|
+
data: initialValue,
|
|
192
|
+
isFetching: false,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// create the offset handlers we'll add to the store
|
|
196
|
+
const handlers = offsetHandlers<_Data, _Input>({
|
|
197
|
+
artifact: this.paginationArtifact,
|
|
198
|
+
fetch: async () => ({} as any),
|
|
199
|
+
getValue: () => get(parent).data,
|
|
200
|
+
setFetching: (isFetching: boolean) => parent.update((p) => ({ ...p, isFetching })),
|
|
201
|
+
queryVariables: () => this.queryVariables({ subscribe: parent.subscribe }),
|
|
202
|
+
storeName: this.name,
|
|
203
|
+
getConfig: () => this.getConfig(),
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// add the offset handlers
|
|
207
|
+
return {
|
|
208
|
+
...parent,
|
|
209
|
+
kind: CompiledFragmentKind,
|
|
210
|
+
fetch: handlers.fetch,
|
|
211
|
+
loadNextPage: handlers.loadNextPage,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type FragmentStorePaginated<_Data extends GraphQLObject, _Input> = Readable<{
|
|
217
|
+
data: _Data
|
|
218
|
+
isFetching: boolean
|
|
219
|
+
pageInfo: PageInfo
|
|
220
|
+
}> & {
|
|
221
|
+
loadNextPage(
|
|
222
|
+
pageCount?: number,
|
|
223
|
+
after?: string | number,
|
|
224
|
+
houdiniContext?: HoudiniFetchContext
|
|
225
|
+
): Promise<void>
|
|
226
|
+
loadPreviousPage(
|
|
227
|
+
pageCount?: number,
|
|
228
|
+
before?: string,
|
|
229
|
+
houdiniContext?: HoudiniFetchContext
|
|
230
|
+
): Promise<void>
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export type FragmentPaginatedResult<_Data, _ExtraFields = {}> = {
|
|
234
|
+
data: _Data | null
|
|
235
|
+
isFetching: boolean
|
|
236
|
+
} & _ExtraFields
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { getCache } from '$houdini/runtime'
|
|
2
|
+
import { ConfigFile } from '$houdini/runtime/lib/config'
|
|
3
|
+
import { deepEquals } from '$houdini/runtime/lib/deepEquals'
|
|
4
|
+
import { executeQuery } from '$houdini/runtime/lib/network'
|
|
5
|
+
import { GraphQLObject, QueryArtifact, QueryResult } from '$houdini/runtime/lib/types'
|
|
6
|
+
|
|
7
|
+
import { getCurrentClient } from '../../network'
|
|
8
|
+
import { getSession } from '../../session'
|
|
9
|
+
import { QueryStoreFetchParams } from '../query'
|
|
10
|
+
import { fetchParams } from '../query'
|
|
11
|
+
import { FetchFn } from './fetch'
|
|
12
|
+
import { countPage, missingPageSizeError } from './pageInfo'
|
|
13
|
+
|
|
14
|
+
export function offsetHandlers<_Data extends GraphQLObject, _Input>({
|
|
15
|
+
artifact,
|
|
16
|
+
queryVariables: extraVariables,
|
|
17
|
+
fetch,
|
|
18
|
+
getValue,
|
|
19
|
+
setFetching,
|
|
20
|
+
storeName,
|
|
21
|
+
getConfig,
|
|
22
|
+
}: {
|
|
23
|
+
artifact: QueryArtifact
|
|
24
|
+
queryVariables: () => Promise<_Input | null>
|
|
25
|
+
fetch: FetchFn<_Data, _Input>
|
|
26
|
+
getValue: () => _Data | null
|
|
27
|
+
storeName: string
|
|
28
|
+
setFetching: (val: boolean) => void
|
|
29
|
+
getConfig: () => Promise<ConfigFile>
|
|
30
|
+
}) {
|
|
31
|
+
// we need to track the most recent offset for this handler
|
|
32
|
+
let getOffset = () =>
|
|
33
|
+
(artifact.refetch?.start as number) ||
|
|
34
|
+
countPage(artifact.refetch!.path, getValue()) ||
|
|
35
|
+
artifact.refetch!.pageSize
|
|
36
|
+
|
|
37
|
+
let currentOffset = getOffset() ?? 0
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
loadNextPage: async ({
|
|
41
|
+
limit,
|
|
42
|
+
offset,
|
|
43
|
+
fetch,
|
|
44
|
+
metadata,
|
|
45
|
+
}: {
|
|
46
|
+
limit?: number
|
|
47
|
+
offset?: number
|
|
48
|
+
fetch?: typeof globalThis.fetch
|
|
49
|
+
metadata?: {}
|
|
50
|
+
} = {}) => {
|
|
51
|
+
const config = await getConfig()
|
|
52
|
+
|
|
53
|
+
// if the offset is zero then we want to count it just to make sure
|
|
54
|
+
// hence why (|| and not ??)
|
|
55
|
+
offset ??= currentOffset || getOffset()
|
|
56
|
+
|
|
57
|
+
// build up the variables to pass to the query
|
|
58
|
+
const queryVariables: Record<string, any> = {
|
|
59
|
+
...(await extraVariables()),
|
|
60
|
+
offset,
|
|
61
|
+
}
|
|
62
|
+
if (limit || limit === 0) {
|
|
63
|
+
queryVariables.limit = limit
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// if we made it this far without a limit argument and there's no default page size,
|
|
67
|
+
// they made a mistake
|
|
68
|
+
if (!queryVariables.limit && !artifact.refetch!.pageSize) {
|
|
69
|
+
throw missingPageSizeError('loadNextPage')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// set the loading state to true
|
|
73
|
+
setFetching(true)
|
|
74
|
+
|
|
75
|
+
// send the query
|
|
76
|
+
const { result } = await executeQuery<GraphQLObject, {}>({
|
|
77
|
+
client: await getCurrentClient(),
|
|
78
|
+
artifact,
|
|
79
|
+
variables: queryVariables,
|
|
80
|
+
session: await getSession(),
|
|
81
|
+
cached: false,
|
|
82
|
+
config,
|
|
83
|
+
fetch,
|
|
84
|
+
metadata,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// update cache with the result
|
|
88
|
+
getCache().write({
|
|
89
|
+
selection: artifact.selection,
|
|
90
|
+
data: result.data,
|
|
91
|
+
variables: queryVariables,
|
|
92
|
+
applyUpdates: true,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// add the page size to the offset so we load the next page next time
|
|
96
|
+
const pageSize = queryVariables.limit || artifact.refetch!.pageSize
|
|
97
|
+
currentOffset = offset + pageSize
|
|
98
|
+
|
|
99
|
+
// we're not loading any more
|
|
100
|
+
setFetching(false)
|
|
101
|
+
},
|
|
102
|
+
async fetch(
|
|
103
|
+
args?: QueryStoreFetchParams<_Data, _Input>
|
|
104
|
+
): Promise<QueryResult<_Data, _Input>> {
|
|
105
|
+
const { params } = await fetchParams(artifact, storeName, args)
|
|
106
|
+
|
|
107
|
+
const { variables } = params ?? {}
|
|
108
|
+
|
|
109
|
+
const extra = await extraVariables()
|
|
110
|
+
|
|
111
|
+
// if the input is different than the query variables then we just do everything like normal
|
|
112
|
+
if (variables && !deepEquals(extra, variables)) {
|
|
113
|
+
return fetch.call(this, params)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// we are updating the current set of items, count the number of items that currently exist
|
|
117
|
+
// and ask for the full data set
|
|
118
|
+
const count = currentOffset || getOffset()
|
|
119
|
+
|
|
120
|
+
// build up the variables to pass to the query
|
|
121
|
+
const queryVariables: Record<string, any> = {
|
|
122
|
+
...extra,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// if there are more records than the first page, we need fetch to load everything
|
|
126
|
+
if (!artifact.refetch!.pageSize || count > artifact.refetch!.pageSize) {
|
|
127
|
+
queryVariables.limit = count
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// set the loading state to true
|
|
131
|
+
setFetching(true)
|
|
132
|
+
|
|
133
|
+
// send the query
|
|
134
|
+
const result = await fetch.call(this, {
|
|
135
|
+
...params,
|
|
136
|
+
variables: queryVariables as _Input,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// we're not loading any more
|
|
140
|
+
setFetching(false)
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
data: result.data,
|
|
144
|
+
variables: queryVariables as _Input,
|
|
145
|
+
isFetching: false,
|
|
146
|
+
partial: result.partial,
|
|
147
|
+
errors: null,
|
|
148
|
+
source: result.source,
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type OffsetHandlers<_Data extends GraphQLObject, _Input, _ReturnType> = {
|
|
155
|
+
loadNextPage: (args?: {
|
|
156
|
+
limit?: number
|
|
157
|
+
offset?: number
|
|
158
|
+
metadata?: {}
|
|
159
|
+
fetch?: typeof globalThis.fetch
|
|
160
|
+
}) => Promise<void>
|
|
161
|
+
fetch(args?: QueryStoreFetchParams<_Data, _Input> | undefined): Promise<_ReturnType>
|
|
162
|
+
}
|