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.
Files changed (59) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/package.json +57 -0
  4. package/src/plugin/codegen/adapter.ts +45 -0
  5. package/src/plugin/codegen/components/index.ts +149 -0
  6. package/src/plugin/codegen/index.ts +28 -0
  7. package/src/plugin/codegen/routes/index.ts +307 -0
  8. package/src/plugin/codegen/routes/kit.test.ts +276 -0
  9. package/src/plugin/codegen/stores/fragment.ts +83 -0
  10. package/src/plugin/codegen/stores/index.ts +55 -0
  11. package/src/plugin/codegen/stores/mutation.ts +56 -0
  12. package/src/plugin/codegen/stores/query.test.ts +504 -0
  13. package/src/plugin/codegen/stores/query.ts +97 -0
  14. package/src/plugin/codegen/stores/subscription.ts +57 -0
  15. package/src/plugin/extract.test.ts +290 -0
  16. package/src/plugin/extract.ts +127 -0
  17. package/src/plugin/extractLoadFunction.test.ts +247 -0
  18. package/src/plugin/extractLoadFunction.ts +249 -0
  19. package/src/plugin/fsPatch.ts +238 -0
  20. package/src/plugin/imports.ts +28 -0
  21. package/src/plugin/index.ts +165 -0
  22. package/src/plugin/kit.ts +382 -0
  23. package/src/plugin/transforms/index.ts +90 -0
  24. package/src/plugin/transforms/kit/index.ts +20 -0
  25. package/src/plugin/transforms/kit/init.test.ts +28 -0
  26. package/src/plugin/transforms/kit/init.ts +75 -0
  27. package/src/plugin/transforms/kit/load.test.ts +1234 -0
  28. package/src/plugin/transforms/kit/load.ts +506 -0
  29. package/src/plugin/transforms/kit/session.test.ts +268 -0
  30. package/src/plugin/transforms/kit/session.ts +161 -0
  31. package/src/plugin/transforms/query.test.ts +99 -0
  32. package/src/plugin/transforms/query.ts +263 -0
  33. package/src/plugin/transforms/reactive.ts +126 -0
  34. package/src/plugin/transforms/tags.ts +20 -0
  35. package/src/plugin/transforms/types.ts +9 -0
  36. package/src/plugin/validate.test.ts +95 -0
  37. package/src/plugin/validate.ts +50 -0
  38. package/src/preprocess/index.ts +33 -0
  39. package/src/runtime/adapter.ts +21 -0
  40. package/src/runtime/fragments.ts +86 -0
  41. package/src/runtime/index.ts +72 -0
  42. package/src/runtime/network.ts +6 -0
  43. package/src/runtime/session.ts +187 -0
  44. package/src/runtime/stores/fragment.ts +48 -0
  45. package/src/runtime/stores/index.ts +5 -0
  46. package/src/runtime/stores/mutation.ts +185 -0
  47. package/src/runtime/stores/pagination/cursor.ts +265 -0
  48. package/src/runtime/stores/pagination/fetch.ts +7 -0
  49. package/src/runtime/stores/pagination/fragment.ts +236 -0
  50. package/src/runtime/stores/pagination/index.ts +7 -0
  51. package/src/runtime/stores/pagination/offset.ts +162 -0
  52. package/src/runtime/stores/pagination/pageInfo.test.ts +39 -0
  53. package/src/runtime/stores/pagination/pageInfo.ts +67 -0
  54. package/src/runtime/stores/pagination/query.ts +132 -0
  55. package/src/runtime/stores/query.ts +524 -0
  56. package/src/runtime/stores/store.ts +13 -0
  57. package/src/runtime/stores/subscription.ts +107 -0
  58. package/src/runtime/types.ts +40 -0
  59. package/src/test/index.ts +208 -0
@@ -0,0 +1,72 @@
1
+ import { QueryStore } from './stores'
2
+
3
+ export * from './adapter'
4
+ export * from './stores'
5
+ export * from './fragments'
6
+ export * from './session'
7
+
8
+ type LoadResult = Promise<{ [key: string]: QueryStore<any, {}> }>
9
+ type LoadAllInput = LoadResult | Record<string, LoadResult>
10
+
11
+ // putting this here was the only way i could find to reliably avoid import issues
12
+ // its really the only thing from lib that users should import so it makes sense to have it here....
13
+ export async function loadAll(
14
+ ...loads: LoadAllInput[]
15
+ ): Promise<Record<string, QueryStore<any, {}>>> {
16
+ // we need to collect all of the promises in a single list that we will await in promise.all and then build up
17
+ const promises: LoadResult[] = []
18
+
19
+ // the question we have to answer is whether entry is a promise or an object of promises
20
+ const isPromise = (val: LoadAllInput): val is LoadResult =>
21
+ 'then' in val && 'finally' in val && 'catch' in val
22
+
23
+ for (const entry of loads) {
24
+ if (!isPromise(entry) && 'then' in entry) {
25
+ throw new Error('❌ `then` is not a valid key for an object passed to loadAll')
26
+ }
27
+
28
+ // identify an entry with the `.then` method
29
+ if (isPromise(entry)) {
30
+ promises.push(entry)
31
+ } else {
32
+ for (const [key, value] of Object.entries(entry)) {
33
+ if (isPromise(value)) {
34
+ promises.push(value)
35
+ } else {
36
+ throw new Error(
37
+ `❌ ${key} is not a valid value for an object passed to loadAll. You must pass the result of a load_Store function`
38
+ )
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ // now that we've collected all of the promises, wait for them
45
+ await Promise.all(promises)
46
+
47
+ // all of the promises are resolved so go back over the value we were given a reconstruct it
48
+ let result = {}
49
+
50
+ for (const entry of loads) {
51
+ // if we're looking at a promise, it will contain the key
52
+ if (isPromise(entry)) {
53
+ Object.assign(result, await entry)
54
+ } else {
55
+ Object.assign(
56
+ result,
57
+ // await every value in the object and assign it to result
58
+ Object.fromEntries(
59
+ await Promise.all(
60
+ Object.entries(entry).map(async ([key, value]) => [
61
+ key,
62
+ Object.values(await value)[0],
63
+ ])
64
+ )
65
+ )
66
+ )
67
+ }
68
+ }
69
+
70
+ // we're done
71
+ return result
72
+ }
@@ -0,0 +1,6 @@
1
+ import { HoudiniClient } from '$houdini/runtime/lib'
2
+
3
+ export async function getCurrentClient(): Promise<HoudiniClient> {
4
+ // @ts-ignore
5
+ return (await import('HOUDINI_CLIENT_PATH')).default
6
+ }
@@ -0,0 +1,187 @@
1
+ import { marshalInputs } from '$houdini/runtime/lib/scalars'
2
+ import {
3
+ MutationArtifact,
4
+ QueryArtifact,
5
+ QueryResult,
6
+ SubscriptionArtifact,
7
+ } from '$houdini/runtime/lib/types'
8
+ import { error, LoadEvent, redirect, RequestEvent } from '@sveltejs/kit'
9
+ import { GraphQLError } from 'graphql'
10
+ import { get } from 'svelte/store'
11
+
12
+ import { isBrowser } from './adapter'
13
+ import { AfterLoadArgs, BeforeLoadArgs, OnErrorArgs } from './types'
14
+
15
+ const sessionKeyName = '__houdini__session__'
16
+
17
+ export class RequestContext {
18
+ private loadEvent: LoadEvent
19
+ continue: boolean = true
20
+ returnValue: {} = {}
21
+
22
+ constructor(ctx: LoadEvent) {
23
+ this.loadEvent = ctx
24
+ }
25
+
26
+ error(status: number, message: string | Error): any {
27
+ throw error(status, typeof message === 'string' ? message : message.message)
28
+ }
29
+
30
+ redirect(status: number, location: string): any {
31
+ throw redirect(status, location)
32
+ }
33
+
34
+ fetch(input: RequestInfo, init?: RequestInit) {
35
+ // make sure to bind the window object to the fetch in a browser
36
+ const fetch =
37
+ typeof window !== 'undefined' ? this.loadEvent.fetch.bind(window) : this.loadEvent.fetch
38
+
39
+ return fetch(input, init)
40
+ }
41
+
42
+ graphqlErrors(payload: { errors?: GraphQLError[] }) {
43
+ // if we have a list of errors
44
+ if (payload.errors) {
45
+ return this.error(500, payload.errors.map(({ message }) => message).join('\n'))
46
+ }
47
+
48
+ return this.error(500, 'Encountered invalid response: ' + JSON.stringify(payload))
49
+ }
50
+
51
+ // This hook fires before executing any queries, it allows custom props to be passed to the component.
52
+ async invokeLoadHook({
53
+ variant,
54
+ hookFn,
55
+ input,
56
+ data,
57
+ error,
58
+ }: {
59
+ variant: 'before' | 'after' | 'error'
60
+ hookFn: KitBeforeLoad | KitAfterLoad | KitOnError
61
+ input: Record<string, any>
62
+ data: Record<string, any>
63
+ error: unknown
64
+ }) {
65
+ // call the onLoad function to match the framework
66
+ let hookCall
67
+ if (variant === 'before') {
68
+ hookCall = (hookFn as KitBeforeLoad).call(this, this.loadEvent as BeforeLoadArgs)
69
+ } else if (variant === 'after') {
70
+ // we have to assign input and data onto load so that we don't read values that
71
+ // are deprecated and generate warnings when read
72
+ hookCall = (hookFn as KitAfterLoad).call(this, {
73
+ event: this.loadEvent,
74
+ input,
75
+ data: Object.fromEntries(
76
+ Object.entries(data).map(([key, store]) => [
77
+ key,
78
+ get<QueryResult<any, any>>(store).data,
79
+ ])
80
+ ),
81
+ } as AfterLoadArgs)
82
+ } else if (variant === 'error') {
83
+ hookCall = (hookFn as KitOnError).call(this, {
84
+ event: this.loadEvent,
85
+ input,
86
+ error,
87
+ } as OnErrorArgs)
88
+ }
89
+
90
+ // make sure any promises are resolved
91
+ let result = await hookCall
92
+
93
+ // If the returnValue is already set through this.error or this.redirect return early
94
+ if (!this.continue) {
95
+ return
96
+ }
97
+ // If the result is null or undefined, or the result isn't an object return early
98
+ if (result == null || typeof result !== 'object') {
99
+ return
100
+ }
101
+
102
+ this.returnValue = result
103
+ }
104
+
105
+ // compute the inputs for an operation should reflect the framework's conventions.
106
+ async computeInput({
107
+ variableFunction,
108
+ artifact,
109
+ }: {
110
+ variableFunction: KitBeforeLoad
111
+ artifact: QueryArtifact | MutationArtifact | SubscriptionArtifact
112
+ }) {
113
+ // call the variable function to match the framework
114
+ let input = await variableFunction.call(this, this.loadEvent)
115
+
116
+ return await marshalInputs({ artifact, input })
117
+ }
118
+ }
119
+
120
+ type KitBeforeLoad = (ctx: BeforeLoadArgs) => Record<string, any> | Promise<Record<string, any>>
121
+ type KitAfterLoad = (ctx: AfterLoadArgs) => Record<string, any>
122
+ type KitOnError = (ctx: OnErrorArgs) => Record<string, any>
123
+
124
+ const sessionSentinel = {}
125
+ // @ts-ignore
126
+ let session: App.Session | {} = sessionSentinel
127
+
128
+ export function extractSession(val: {
129
+ [sessionKeyName]: // @ts-ignore
130
+ App.Session
131
+ }) {
132
+ return val[sessionKeyName]
133
+ }
134
+
135
+ export function buildSessionObject(event: RequestEvent) {
136
+ return {
137
+ [sessionKeyName]: extractSession(event.locals as any),
138
+ }
139
+ }
140
+
141
+ export function setClientSession(
142
+ // @ts-ignore
143
+ val: App.Session
144
+ ) {
145
+ if (!isBrowser) {
146
+ return
147
+ }
148
+
149
+ session = val
150
+ }
151
+
152
+ // @ts-ignore
153
+ export function getClientSession(): App.Session {
154
+ return session
155
+ }
156
+
157
+ export function setSession(
158
+ event: RequestEvent,
159
+ session: // @ts-ignore
160
+ App.Session
161
+ ) {
162
+ ;(event.locals as any)[sessionKeyName] = session
163
+ }
164
+
165
+ export async function getSession(event?: RequestEvent | LoadEvent): Promise<
166
+ | {}
167
+ // @ts-ignore
168
+ | App.Session
169
+ > {
170
+ if (event) {
171
+ // get the session either from the server side event or the client side event
172
+ if ('locals' in event) {
173
+ // this is a server side event (RequestEvent) -> extract the session from locals
174
+ return extractSession(event.locals as any) || sessionSentinel
175
+ }
176
+ // the session data could also already be present in the data field
177
+ else if ('data' in event && event.data && sessionKeyName in event.data) {
178
+ // @ts-ignore
179
+ return extractSession(event.data) || sessionSentinel
180
+ } else {
181
+ // this is a client side event -> await the parent data which include the session
182
+ return extractSession((await event.parent()) as any) || sessionSentinel
183
+ }
184
+ }
185
+
186
+ return session
187
+ }
@@ -0,0 +1,48 @@
1
+ import {
2
+ CompiledFragmentKind,
3
+ GraphQLObject,
4
+ FragmentArtifact,
5
+ HoudiniFetchContext,
6
+ } from '$houdini/runtime/lib/types'
7
+ import { Writable, writable } from 'svelte/store'
8
+ import type { Readable } from 'svelte/store'
9
+
10
+ import { BaseStore } from './store'
11
+
12
+ // a fragment store exists in multiple places in a given application so we
13
+ // can't just return a store directly, the user has to load the version of the
14
+ // fragment store for the object the store has been mixed into
15
+ export class FragmentStore<
16
+ _Data extends GraphQLObject,
17
+ _Input = {},
18
+ _ExtraFields = {}
19
+ > extends BaseStore {
20
+ artifact: FragmentArtifact
21
+ name: string
22
+ kind = CompiledFragmentKind
23
+
24
+ protected context: HoudiniFetchContext | null = null
25
+
26
+ constructor({ artifact, storeName }: { artifact: FragmentArtifact; storeName: string }) {
27
+ super()
28
+ this.artifact = artifact
29
+ this.name = storeName
30
+ }
31
+
32
+ get(initialValue: _Data | null) {
33
+ // at the moment a fragment store doesn't really do anything
34
+ // but we're going to keep it wrapped in a store so we can eventually
35
+ // optimize the updates
36
+ let store = writable(initialValue) as Writable<(_Data | null) & _ExtraFields>
37
+
38
+ return {
39
+ kind: CompiledFragmentKind,
40
+ subscribe: (
41
+ ...args: Parameters<Readable<(_Data | null) & _ExtraFields>['subscribe']>
42
+ ) => {
43
+ return store.subscribe(...args)
44
+ },
45
+ update: (val: (_Data | null) & _ExtraFields) => store?.set(val),
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,5 @@
1
+ export * from './pagination'
2
+ export { FragmentStore } from './fragment'
3
+ export { SubscriptionStore } from './subscription'
4
+ export { MutationStore, type MutationConfig } from './mutation'
5
+ export { QueryStore, type QueryStoreFetchParams } from './query'
@@ -0,0 +1,185 @@
1
+ import { getCache } from '$houdini/runtime'
2
+ import { executeQuery } from '$houdini/runtime/lib/network'
3
+ import { marshalInputs, marshalSelection, unmarshalSelection } from '$houdini/runtime/lib/scalars'
4
+ import type { SubscriptionSpec, MutationArtifact } from '$houdini/runtime/lib/types'
5
+ import { GraphQLObject } from '$houdini/runtime/lib/types'
6
+ import { Readable } from 'svelte/store'
7
+ import { Writable, writable } from 'svelte/store'
8
+
9
+ import { getCurrentClient } from '../network'
10
+ import { getSession } from '../session'
11
+ import { BaseStore } from './store'
12
+
13
+ export class MutationStore<
14
+ _Data extends GraphQLObject,
15
+ _Input extends {},
16
+ _Optimistic extends GraphQLObject
17
+ > extends BaseStore {
18
+ artifact: MutationArtifact
19
+ kind = 'HoudiniMutation' as const
20
+
21
+ private store: Writable<MutationResult<_Data, _Input>>
22
+
23
+ constructor({ artifact }: { artifact: MutationArtifact }) {
24
+ super()
25
+ this.artifact = artifact
26
+ this.store = writable(this.nullState)
27
+ }
28
+
29
+ async mutate(
30
+ variables: _Input,
31
+ {
32
+ metadata,
33
+ fetch,
34
+ ...mutationConfig
35
+ }: {
36
+ // @ts-ignore
37
+ metadata?: App.Metadata
38
+ fetch?: typeof globalThis.fetch
39
+ } & MutationConfig<_Data, _Input, _Optimistic> = {}
40
+ ): Promise<_Data> {
41
+ const cache = getCache()
42
+ const config = await this.getConfig()
43
+
44
+ this.store.update((c) => {
45
+ return { ...c, isFetching: true }
46
+ })
47
+
48
+ // treat a mutation like it has an optimistic layer regardless of
49
+ // whether there actually _is_ one. This ensures that a query which fires
50
+ // after this mutation has been sent will overwrite any return values from the mutation
51
+ //
52
+ // as far as I can tell, this is an arbitrary decision but it does give a
53
+ // well-defined ordering to a subtle situation so that seems like a win
54
+ //
55
+ const layer = cache._internal_unstable.storage.createLayer(true)
56
+
57
+ // if there is an optimistic response then we need to write the value immediately
58
+ const optimisticResponse = mutationConfig?.optimisticResponse
59
+ // hold onto the list of subscribers that we updated because of the optimistic response
60
+ // and make sure they are included in the final set of subscribers to notify
61
+ let toNotify: SubscriptionSpec[] = []
62
+ if (optimisticResponse) {
63
+ toNotify = cache.write({
64
+ selection: this.artifact.selection,
65
+ // make sure that any scalar values get processed into something we can cache
66
+ data: (await marshalSelection({
67
+ selection: this.artifact.selection,
68
+ data: optimisticResponse,
69
+ }))!,
70
+ variables,
71
+ layer: layer.id,
72
+ })
73
+ }
74
+
75
+ const newVariables = (await marshalInputs({
76
+ input: variables,
77
+ artifact: this.artifact,
78
+ })) as _Input
79
+
80
+ try {
81
+ // trigger the mutation
82
+ const { result } = await executeQuery({
83
+ client: await getCurrentClient(),
84
+ config,
85
+ artifact: this.artifact,
86
+ variables: newVariables,
87
+ session: await getSession(),
88
+ cached: false,
89
+ metadata,
90
+ fetch,
91
+ })
92
+
93
+ if (result.errors && result.errors.length > 0) {
94
+ this.store.update((s) => ({
95
+ ...s,
96
+ errors: result.errors,
97
+ isFetching: false,
98
+ isOptimisticResponse: false,
99
+ data: result.data,
100
+ variables: (newVariables || {}) as _Input,
101
+ }))
102
+ throw result.errors
103
+ }
104
+
105
+ // clear the layer holding any mutation results
106
+ layer.clear()
107
+
108
+ // write the result of the mutation to the cache
109
+ cache.write({
110
+ selection: this.artifact.selection,
111
+ data: result.data,
112
+ variables: newVariables,
113
+ // write to the mutation's layer
114
+ layer: layer.id,
115
+ // notify any subscribers that we updated with the optimistic response
116
+ // in order to address situations where the optimistic update was wrong
117
+ notifySubscribers: toNotify,
118
+ // make sure that we notify subscribers for any values that we overwrite
119
+ // in order to address any race conditions when comparing the previous value
120
+ forceNotify: true,
121
+ })
122
+
123
+ // merge the layer back into the cache
124
+ cache._internal_unstable.storage.resolveLayer(layer.id)
125
+
126
+ // prepare store data
127
+ const storeData: MutationResult<_Data, _Input> = {
128
+ data: unmarshalSelection(config, this.artifact.selection, result.data) as _Data,
129
+ errors: result.errors ?? null,
130
+ isFetching: false,
131
+ isOptimisticResponse: false,
132
+ variables: newVariables,
133
+ }
134
+
135
+ // update the store value
136
+ this.store.set(storeData)
137
+
138
+ // return the value to the caller
139
+ return storeData.data ?? ({} as _Data)
140
+ } catch (error) {
141
+ this.store.update((s) => ({
142
+ ...s,
143
+ errors: error as { message: string }[],
144
+ isFetching: false,
145
+ isOptimisticResponse: false,
146
+ data: null,
147
+ variables: newVariables,
148
+ }))
149
+
150
+ // if the mutation failed, roll the layer back and delete it
151
+ layer.clear()
152
+ cache._internal_unstable.storage.resolveLayer(layer.id)
153
+
154
+ // bubble the mutation error up to the caller
155
+ throw error
156
+ }
157
+ }
158
+
159
+ subscribe(...args: Parameters<Readable<MutationResult<_Data, _Input>>['subscribe']>) {
160
+ // use it's value
161
+ return this.store.subscribe(...args)
162
+ }
163
+
164
+ private get nullState() {
165
+ return {
166
+ data: null as _Data | null,
167
+ errors: null,
168
+ isFetching: false,
169
+ isOptimisticResponse: false,
170
+ variables: null,
171
+ }
172
+ }
173
+ }
174
+
175
+ export type MutationConfig<_Result, _Input, _Optimistic> = {
176
+ optimisticResponse?: _Optimistic
177
+ }
178
+
179
+ export type MutationResult<_Data, _Input> = {
180
+ data: _Data | null
181
+ errors: { message: string }[] | null
182
+ isFetching: boolean
183
+ isOptimisticResponse: boolean
184
+ variables: _Input | null
185
+ }