@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 +31 -0
- package/src/graphql.ts +278 -0
- package/src/index.ts +1 -0
- package/tsconfig.json +10 -0
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
|
+
}
|