@livestore/graphql 0.0.0-snapshot-97ca7eac46b6a583b22d40189126d06a377ec1b0

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/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@livestore/graphql",
3
+ "version": "0.0.0-snapshot-97ca7eac46b6a583b22d40189126d06a377ec1b0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ }
10
+ },
11
+ "types": "./dist/index.d.ts",
12
+ "dependencies": {
13
+ "@graphql-typed-document-node/core": "3.2.0",
14
+ "@opentelemetry/api": "1.9.0",
15
+ "@livestore/common": "0.0.0-snapshot-97ca7eac46b6a583b22d40189126d06a377ec1b0",
16
+ "@livestore/livestore": "0.0.0-snapshot-97ca7eac46b6a583b22d40189126d06a377ec1b0",
17
+ "@livestore/utils": "0.0.0-snapshot-97ca7eac46b6a583b22d40189126d06a377ec1b0"
18
+ },
19
+ "devDependencies": {
20
+ "graphql": "^16.10.0"
21
+ },
22
+ "peerDependencies": {
23
+ "graphql": "~16.10.0"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "scripts": {
29
+ "test": "echo 'No tests'"
30
+ }
31
+ }
package/src/graphql.ts ADDED
@@ -0,0 +1,278 @@
1
+ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
+ import { getDurationMsFromSpan, type QueryInfo } from '@livestore/common'
3
+ import type { RefreshReason, SqliteDbWrapper, Store } from '@livestore/livestore'
4
+ import { LiveQueries, ReactiveGraph } from '@livestore/livestore/internal'
5
+ import { shouldNeverHappen } from '@livestore/utils'
6
+ import { Predicate, Schema, TreeFormatter } from '@livestore/utils/effect'
7
+ import * as otel from '@opentelemetry/api'
8
+ import type { GraphQLSchema } from 'graphql'
9
+ import * as graphql from 'graphql'
10
+
11
+ export type BaseGraphQLContext = {
12
+ queriedTables: Set<string>
13
+ /** Needed by Pothos Otel plugin for resolver tracing to work */
14
+ otelContext?: otel.Context
15
+ }
16
+
17
+ export type LazyGraphQLContextRef = {
18
+ current:
19
+ | {
20
+ _tag: 'pending'
21
+ make: (store: Store) => BaseGraphQLContext
22
+ }
23
+ | {
24
+ _tag: 'active'
25
+ value: BaseGraphQLContext
26
+ }
27
+ }
28
+
29
+ export type GraphQLOptions<TContext> = {
30
+ schema: GraphQLSchema
31
+ makeContext: (db: SqliteDbWrapper, tracer: otel.Tracer, sessionId: string) => TContext
32
+ }
33
+
34
+ export type MapResult<To, From> = ((res: From, get: LiveQueries.GetAtomResult) => To) | Schema.Schema<To, From>
35
+
36
+ export const queryGraphQL = <
37
+ TResult extends Record<string, any>,
38
+ TVariableValues extends Record<string, any>,
39
+ TResultMapped extends Record<string, any> = TResult,
40
+ >(
41
+ document: DocumentNode<TResult, TVariableValues>,
42
+ genVariableValues: TVariableValues | ((get: LiveQueries.GetAtomResult) => TVariableValues),
43
+ options: {
44
+ label?: string
45
+ // reactivityGraph?: ReactivityGraph
46
+ map?: MapResult<TResultMapped, TResult>
47
+ deps?: LiveQueries.DepKey
48
+ } = {},
49
+ ): LiveQueries.LiveQueryDef<TResultMapped, QueryInfo.None> => {
50
+ const documentName = graphql.getOperationAST(document)?.name?.value
51
+ const hash = options.deps
52
+ ? LiveQueries.depsToString(options.deps)
53
+ : (documentName ?? shouldNeverHappen('No document name found and no deps provided'))
54
+ const label = options.label ?? documentName ?? 'graphql'
55
+ const map = options.map
56
+
57
+ return {
58
+ _tag: 'def',
59
+ make: LiveQueries.withRCMap(hash, (ctx, _otelContext) => {
60
+ return new LiveStoreGraphQLQuery({
61
+ document,
62
+ genVariableValues,
63
+ label,
64
+ map,
65
+ reactivityGraph: ctx.reactivityGraph.deref()!,
66
+ })
67
+ }),
68
+ label,
69
+ hash,
70
+ queryInfo: { _tag: 'None' },
71
+ }
72
+ }
73
+
74
+ export class LiveStoreGraphQLQuery<
75
+ TResult extends Record<string, any>,
76
+ TVariableValues extends Record<string, any>,
77
+ TResultMapped extends Record<string, any> = TResult,
78
+ > extends LiveQueries.LiveStoreQueryBase<TResultMapped, QueryInfo.None> {
79
+ _tag: 'graphql' = 'graphql'
80
+
81
+ /** The abstract GraphQL query */
82
+ document: DocumentNode<TResult, TVariableValues>
83
+
84
+ /** A reactive thunk representing the query results */
85
+ results$: ReactiveGraph.Thunk<TResultMapped, LiveQueries.ReactivityGraphContext, RefreshReason>
86
+
87
+ variableValues$: ReactiveGraph.Thunk<TVariableValues, LiveQueries.ReactivityGraphContext, RefreshReason> | undefined
88
+
89
+ label: string
90
+
91
+ reactivityGraph: LiveQueries.ReactivityGraph
92
+
93
+ queryInfo: QueryInfo.None = { _tag: 'None' }
94
+
95
+ private mapResult
96
+
97
+ constructor({
98
+ document,
99
+ label,
100
+ genVariableValues,
101
+ reactivityGraph,
102
+ map,
103
+ }: {
104
+ document: DocumentNode<TResult, TVariableValues>
105
+ genVariableValues: TVariableValues | ((get: LiveQueries.GetAtomResult) => TVariableValues)
106
+ label?: string
107
+ reactivityGraph: LiveQueries.ReactivityGraph
108
+ map?: MapResult<TResultMapped, TResult>
109
+ }) {
110
+ super()
111
+
112
+ const labelWithDefault = label ?? graphql.getOperationAST(document)?.name?.value ?? 'graphql'
113
+
114
+ this.label = labelWithDefault
115
+ this.document = document
116
+
117
+ this.reactivityGraph = reactivityGraph
118
+
119
+ this.mapResult =
120
+ map === undefined
121
+ ? (res: TResult) => res as any as TResultMapped
122
+ : Schema.isSchema(map)
123
+ ? (res: TResult) => {
124
+ const parseResult = Schema.decodeEither(map as Schema.Schema<TResultMapped, TResult>)(res)
125
+ if (parseResult._tag === 'Left') {
126
+ console.error(`Error parsing GraphQL query result: ${TreeFormatter.formatErrorSync(parseResult.left)}`)
127
+ return shouldNeverHappen(`Error parsing SQL query result: ${parseResult.left}`)
128
+ } else {
129
+ return parseResult.right as TResultMapped
130
+ }
131
+ }
132
+ : typeof map === 'function'
133
+ ? map
134
+ : shouldNeverHappen(`Invalid map function ${map}`)
135
+
136
+ // TODO don't even create a thunk if variables are static
137
+ let variableValues$OrvariableValues
138
+
139
+ if (typeof genVariableValues === 'function') {
140
+ variableValues$OrvariableValues = this.reactivityGraph.makeThunk(
141
+ (get, _setDebugInfo, ctx, otelContext) => {
142
+ return genVariableValues(
143
+ LiveQueries.makeGetAtomResult(get, ctx, otelContext ?? ctx.rootOtelContext, this.dependencyQueriesRef),
144
+ )
145
+ },
146
+ { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphql.variables' } },
147
+ )
148
+ this.variableValues$ = variableValues$OrvariableValues
149
+ } else {
150
+ variableValues$OrvariableValues = genVariableValues
151
+ }
152
+
153
+ const resultsLabel = `${labelWithDefault}:results`
154
+ this.results$ = this.reactivityGraph.makeThunk<TResultMapped>(
155
+ (get, setDebugInfo, ctx, otelContext, debugRefreshReason) => {
156
+ const { store, otelTracer, rootOtelContext } = ctx
157
+ const variableValues = ReactiveGraph.isThunk(variableValues$OrvariableValues)
158
+ ? (get(variableValues$OrvariableValues, otelContext, debugRefreshReason) as TVariableValues)
159
+ : (variableValues$OrvariableValues as TVariableValues)
160
+ const { result, queriedTables, durationMs } = this.queryOnce({
161
+ document,
162
+ variableValues,
163
+ otelContext: otelContext ?? rootOtelContext,
164
+ otelTracer,
165
+ get: LiveQueries.makeGetAtomResult(get, ctx, otelContext ?? rootOtelContext, this.dependencyQueriesRef),
166
+ store,
167
+ })
168
+
169
+ // Add dependencies on any tables that were used
170
+ for (const tableName of queriedTables) {
171
+ const tableRef = store.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
172
+ get(tableRef)
173
+ }
174
+
175
+ setDebugInfo({ _tag: 'graphql', label: resultsLabel, query: graphql.print(document), durationMs })
176
+
177
+ return result
178
+ },
179
+ { label: resultsLabel, meta: { liveStoreThunkType: 'graphql.result' } },
180
+ // otelContext,
181
+ )
182
+ }
183
+
184
+ queryOnce = ({
185
+ document,
186
+ otelContext,
187
+ otelTracer,
188
+ variableValues,
189
+ get,
190
+ store,
191
+ }: {
192
+ document: graphql.DocumentNode
193
+ otelContext: otel.Context
194
+ otelTracer: otel.Tracer
195
+ variableValues: TVariableValues
196
+ get: LiveQueries.GetAtomResult
197
+ store: Store
198
+ }) => {
199
+ const { schema, context } = unpackStoreContext(store)
200
+
201
+ const operationName = graphql.getOperationAST(document)?.name?.value
202
+
203
+ return otelTracer.startActiveSpan(`executeGraphQLQuery: ${operationName}`, {}, otelContext, (span) => {
204
+ span.setAttribute('graphql.variables', JSON.stringify(variableValues))
205
+ span.setAttribute('graphql.query', graphql.print(document))
206
+
207
+ context.queriedTables.clear()
208
+
209
+ context.otelContext = otel.trace.setSpan(otel.context.active(), span)
210
+
211
+ const res = graphql.executeSync({
212
+ document,
213
+ contextValue: context,
214
+ schema: schema,
215
+ variableValues,
216
+ })
217
+
218
+ // TODO track number of nested SQL queries via Otel + debug info
219
+
220
+ if (res.errors) {
221
+ span.setStatus({ code: otel.SpanStatusCode.ERROR, message: 'GraphQL error' })
222
+ span.setAttribute('graphql.error', res.errors.join('\n'))
223
+ span.setAttribute('graphql.error-detail', JSON.stringify(res.errors))
224
+ console.error(`graphql error (${operationName}) - ${res.errors.length} errors`)
225
+ for (const error of res.errors) {
226
+ console.error(error)
227
+ }
228
+ debugger
229
+ shouldNeverHappen(`GraphQL error: ${res.errors.join('\n')}`)
230
+ }
231
+
232
+ span.end()
233
+
234
+ const result = this.mapResult(res.data as unknown as TResult, get)
235
+
236
+ const durationMs = getDurationMsFromSpan(span)
237
+
238
+ this.executionTimes.push(durationMs)
239
+
240
+ return {
241
+ result,
242
+ queriedTables: Array.from(context.queriedTables.values()),
243
+ durationMs,
244
+ }
245
+ })
246
+ }
247
+
248
+ destroy = () => {
249
+ if (this.variableValues$ !== undefined) {
250
+ this.reactivityGraph.destroyNode(this.variableValues$)
251
+ }
252
+
253
+ this.reactivityGraph.destroyNode(this.results$)
254
+
255
+ for (const query of this.dependencyQueriesRef) {
256
+ query.deref()
257
+ }
258
+ }
259
+ }
260
+
261
+ const unpackStoreContext = (store: Store): { schema: graphql.GraphQLSchema; context: BaseGraphQLContext } => {
262
+ if (Predicate.hasProperty(store.context, 'graphql') === false) {
263
+ return shouldNeverHappen('Store context does not contain graphql context')
264
+ }
265
+ if (Predicate.hasProperty(store.context.graphql, 'schema') === false) {
266
+ return shouldNeverHappen('Store context does not contain graphql.schema')
267
+ }
268
+ if (Predicate.hasProperty(store.context.graphql, 'context') === false) {
269
+ return shouldNeverHappen('Store context does not contain graphql.context')
270
+ }
271
+ const schema = store.context.graphql.schema as graphql.GraphQLSchema
272
+ const context = store.context.graphql.context as LazyGraphQLContextRef
273
+ if (context.current._tag === 'pending') {
274
+ const value = context.current.make(store)
275
+ context.current = { _tag: 'active', value }
276
+ }
277
+ return { schema, context: context.current.value }
278
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './graphql.js'
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "tsBuildInfoFile": "./dist/.tsbuildinfo"
7
+ },
8
+ "include": ["./src"],
9
+ "references": [{ "path": "../utils" }, { "path": "../livestore" }, { "path": "../common" }]
10
+ }