@effect-gql/core 0.1.0 → 1.1.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/README.md +100 -0
- package/builder/index.cjs +1446 -0
- package/builder/index.cjs.map +1 -0
- package/builder/index.d.cts +260 -0
- package/{dist/builder/pipe-api.d.ts → builder/index.d.ts} +50 -21
- package/builder/index.js +1405 -0
- package/builder/index.js.map +1 -0
- package/index.cjs +3469 -0
- package/index.cjs.map +1 -0
- package/index.d.cts +529 -0
- package/index.d.ts +529 -0
- package/index.js +3292 -0
- package/index.js.map +1 -0
- package/package.json +19 -28
- package/schema-builder-DKvkzU_M.d.cts +965 -0
- package/schema-builder-DKvkzU_M.d.ts +965 -0
- package/server/index.cjs +1579 -0
- package/server/index.cjs.map +1 -0
- package/server/index.d.cts +682 -0
- package/server/index.d.ts +682 -0
- package/server/index.js +1548 -0
- package/server/index.js.map +1 -0
- package/dist/analyzer-extension.d.ts +0 -105
- package/dist/analyzer-extension.d.ts.map +0 -1
- package/dist/analyzer-extension.js +0 -137
- package/dist/analyzer-extension.js.map +0 -1
- package/dist/builder/execute.d.ts +0 -26
- package/dist/builder/execute.d.ts.map +0 -1
- package/dist/builder/execute.js +0 -104
- package/dist/builder/execute.js.map +0 -1
- package/dist/builder/field-builders.d.ts +0 -30
- package/dist/builder/field-builders.d.ts.map +0 -1
- package/dist/builder/field-builders.js +0 -200
- package/dist/builder/field-builders.js.map +0 -1
- package/dist/builder/index.d.ts +0 -7
- package/dist/builder/index.d.ts.map +0 -1
- package/dist/builder/index.js +0 -31
- package/dist/builder/index.js.map +0 -1
- package/dist/builder/pipe-api.d.ts.map +0 -1
- package/dist/builder/pipe-api.js +0 -151
- package/dist/builder/pipe-api.js.map +0 -1
- package/dist/builder/schema-builder.d.ts +0 -301
- package/dist/builder/schema-builder.d.ts.map +0 -1
- package/dist/builder/schema-builder.js +0 -566
- package/dist/builder/schema-builder.js.map +0 -1
- package/dist/builder/type-registry.d.ts +0 -80
- package/dist/builder/type-registry.d.ts.map +0 -1
- package/dist/builder/type-registry.js +0 -505
- package/dist/builder/type-registry.js.map +0 -1
- package/dist/builder/types.d.ts +0 -283
- package/dist/builder/types.d.ts.map +0 -1
- package/dist/builder/types.js +0 -3
- package/dist/builder/types.js.map +0 -1
- package/dist/cli/generate-schema.d.ts +0 -29
- package/dist/cli/generate-schema.d.ts.map +0 -1
- package/dist/cli/generate-schema.js +0 -233
- package/dist/cli/generate-schema.js.map +0 -1
- package/dist/cli/index.d.ts +0 -19
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -24
- package/dist/cli/index.js.map +0 -1
- package/dist/context.d.ts +0 -18
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -11
- package/dist/context.js.map +0 -1
- package/dist/error.d.ts +0 -45
- package/dist/error.d.ts.map +0 -1
- package/dist/error.js +0 -29
- package/dist/error.js.map +0 -1
- package/dist/extensions.d.ts +0 -130
- package/dist/extensions.d.ts.map +0 -1
- package/dist/extensions.js +0 -78
- package/dist/extensions.js.map +0 -1
- package/dist/index.d.ts +0 -12
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -47
- package/dist/index.js.map +0 -1
- package/dist/loader.d.ts +0 -169
- package/dist/loader.d.ts.map +0 -1
- package/dist/loader.js +0 -237
- package/dist/loader.js.map +0 -1
- package/dist/resolver-context.d.ts +0 -154
- package/dist/resolver-context.d.ts.map +0 -1
- package/dist/resolver-context.js +0 -184
- package/dist/resolver-context.js.map +0 -1
- package/dist/schema-mapping.d.ts +0 -30
- package/dist/schema-mapping.d.ts.map +0 -1
- package/dist/schema-mapping.js +0 -280
- package/dist/schema-mapping.js.map +0 -1
- package/dist/server/cache-control.d.ts +0 -96
- package/dist/server/cache-control.d.ts.map +0 -1
- package/dist/server/cache-control.js +0 -308
- package/dist/server/cache-control.js.map +0 -1
- package/dist/server/complexity.d.ts +0 -165
- package/dist/server/complexity.d.ts.map +0 -1
- package/dist/server/complexity.js +0 -433
- package/dist/server/complexity.js.map +0 -1
- package/dist/server/config.d.ts +0 -66
- package/dist/server/config.d.ts.map +0 -1
- package/dist/server/config.js +0 -104
- package/dist/server/config.js.map +0 -1
- package/dist/server/graphiql.d.ts +0 -5
- package/dist/server/graphiql.d.ts.map +0 -1
- package/dist/server/graphiql.js +0 -43
- package/dist/server/graphiql.js.map +0 -1
- package/dist/server/index.d.ts +0 -18
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/index.js +0 -48
- package/dist/server/index.js.map +0 -1
- package/dist/server/router.d.ts +0 -79
- package/dist/server/router.d.ts.map +0 -1
- package/dist/server/router.js +0 -232
- package/dist/server/router.js.map +0 -1
- package/dist/server/schema-builder-extensions.d.ts +0 -42
- package/dist/server/schema-builder-extensions.d.ts.map +0 -1
- package/dist/server/schema-builder-extensions.js +0 -48
- package/dist/server/schema-builder-extensions.js.map +0 -1
- package/dist/server/sse-adapter.d.ts +0 -64
- package/dist/server/sse-adapter.d.ts.map +0 -1
- package/dist/server/sse-adapter.js +0 -227
- package/dist/server/sse-adapter.js.map +0 -1
- package/dist/server/sse-types.d.ts +0 -192
- package/dist/server/sse-types.d.ts.map +0 -1
- package/dist/server/sse-types.js +0 -63
- package/dist/server/sse-types.js.map +0 -1
- package/dist/server/ws-adapter.d.ts +0 -39
- package/dist/server/ws-adapter.d.ts.map +0 -1
- package/dist/server/ws-adapter.js +0 -247
- package/dist/server/ws-adapter.js.map +0 -1
- package/dist/server/ws-types.d.ts +0 -169
- package/dist/server/ws-types.d.ts.map +0 -1
- package/dist/server/ws-types.js +0 -11
- package/dist/server/ws-types.js.map +0 -1
- package/dist/server/ws-utils.d.ts +0 -42
- package/dist/server/ws-utils.d.ts.map +0 -1
- package/dist/server/ws-utils.js +0 -99
- package/dist/server/ws-utils.js.map +0 -1
- package/src/analyzer-extension.ts +0 -254
- package/src/builder/execute.ts +0 -153
- package/src/builder/field-builders.ts +0 -322
- package/src/builder/index.ts +0 -48
- package/src/builder/pipe-api.ts +0 -312
- package/src/builder/schema-builder.ts +0 -970
- package/src/builder/type-registry.ts +0 -670
- package/src/builder/types.ts +0 -305
- package/src/context.ts +0 -23
- package/src/error.ts +0 -32
- package/src/extensions.ts +0 -240
- package/src/index.ts +0 -32
- package/src/loader.ts +0 -363
- package/src/resolver-context.ts +0 -253
- package/src/schema-mapping.ts +0 -307
- package/src/server/cache-control.ts +0 -590
- package/src/server/complexity.ts +0 -774
- package/src/server/config.ts +0 -174
- package/src/server/graphiql.ts +0 -38
- package/src/server/index.ts +0 -96
- package/src/server/router.ts +0 -432
- package/src/server/schema-builder-extensions.ts +0 -51
- package/src/server/sse-adapter.ts +0 -327
- package/src/server/sse-types.ts +0 -234
- package/src/server/ws-adapter.ts +0 -355
- package/src/server/ws-types.ts +0 -192
- package/src/server/ws-utils.ts +0 -136
package/src/loader.ts
DELETED
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import { Effect, Context, Layer } from "effect"
|
|
2
|
-
import DataLoader from "dataloader"
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Ergonomic DataLoader helpers for Effect-based GraphQL
|
|
6
|
-
*
|
|
7
|
-
* This module provides a type-safe, declarative way to define DataLoaders
|
|
8
|
-
* that integrate seamlessly with Effect's service system.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```typescript
|
|
12
|
-
* // Define loaders
|
|
13
|
-
* const loaders = Loader.define({
|
|
14
|
-
* UserById: Loader.single<string, User>({
|
|
15
|
-
* batch: (ids) => db.getUsersByIds(ids),
|
|
16
|
-
* key: (user) => user.id,
|
|
17
|
-
* }),
|
|
18
|
-
*
|
|
19
|
-
* PostsByAuthorId: Loader.grouped<string, Post>({
|
|
20
|
-
* batch: (ids) => db.getPostsForAuthors(ids),
|
|
21
|
-
* groupBy: (post) => post.authorId,
|
|
22
|
-
* }),
|
|
23
|
-
* })
|
|
24
|
-
*
|
|
25
|
-
* // Use in resolvers
|
|
26
|
-
* resolve: (parent) => loaders.load("UserById", parent.authorId)
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
// ============================================================================
|
|
31
|
-
// Types
|
|
32
|
-
// ============================================================================
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Configuration for a single-value loader (one key -> one value)
|
|
36
|
-
*/
|
|
37
|
-
interface SingleLoaderDef<K, V, R> {
|
|
38
|
-
readonly _tag: "single"
|
|
39
|
-
readonly batch: (keys: readonly K[]) => Effect.Effect<readonly V[], Error, R>
|
|
40
|
-
readonly key: (value: V) => K
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Configuration for a grouped loader (one key -> many values)
|
|
45
|
-
*/
|
|
46
|
-
interface GroupedLoaderDef<K, V, R> {
|
|
47
|
-
readonly _tag: "grouped"
|
|
48
|
-
readonly batch: (keys: readonly K[]) => Effect.Effect<readonly V[], Error, R>
|
|
49
|
-
readonly groupBy: (value: V) => K
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
type LoaderDef<K, V, R> = SingleLoaderDef<K, V, R> | GroupedLoaderDef<K, V, R>
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Runtime DataLoader instances
|
|
56
|
-
*/
|
|
57
|
-
type LoaderInstances<Defs extends Record<string, LoaderDef<any, any, any>>> = {
|
|
58
|
-
[Name in keyof Defs]: Defs[Name] extends SingleLoaderDef<infer K, infer V, any>
|
|
59
|
-
? DataLoader<K, V>
|
|
60
|
-
: Defs[Name] extends GroupedLoaderDef<infer K, infer V, any>
|
|
61
|
-
? DataLoader<K, V[]>
|
|
62
|
-
: never
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Extract the value type for a loader (accounting for grouped loaders)
|
|
67
|
-
*/
|
|
68
|
-
type LoaderValue<Def> =
|
|
69
|
-
Def extends SingleLoaderDef<any, infer V, any>
|
|
70
|
-
? V
|
|
71
|
-
: Def extends GroupedLoaderDef<any, infer V, any>
|
|
72
|
-
? V[]
|
|
73
|
-
: never
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Extract the key type for a loader
|
|
77
|
-
*/
|
|
78
|
-
type LoaderKey<Def> = Def extends LoaderDef<infer K, any, any> ? K : never
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Extract combined requirements from all loaders
|
|
82
|
-
*/
|
|
83
|
-
type LoaderRequirements<Defs extends Record<string, LoaderDef<any, any, any>>> = {
|
|
84
|
-
[K in keyof Defs]: Defs[K] extends LoaderDef<any, any, infer R> ? R : never
|
|
85
|
-
}[keyof Defs]
|
|
86
|
-
|
|
87
|
-
// ============================================================================
|
|
88
|
-
// Loader Builders
|
|
89
|
-
// ============================================================================
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Create a single-value loader definition.
|
|
93
|
-
* One key maps to one value.
|
|
94
|
-
*
|
|
95
|
-
* @example
|
|
96
|
-
* ```typescript
|
|
97
|
-
* Loader.single<string, User>({
|
|
98
|
-
* batch: (ids) => db.getUsersByIds(ids),
|
|
99
|
-
* key: (user) => user.id,
|
|
100
|
-
* })
|
|
101
|
-
* ```
|
|
102
|
-
*/
|
|
103
|
-
function single<K, V, R = never>(config: {
|
|
104
|
-
batch: (keys: readonly K[]) => Effect.Effect<readonly V[], Error, R>
|
|
105
|
-
key: (value: V) => K
|
|
106
|
-
}): SingleLoaderDef<K, V, R> {
|
|
107
|
-
return {
|
|
108
|
-
_tag: "single",
|
|
109
|
-
batch: config.batch,
|
|
110
|
-
key: config.key,
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Create a grouped loader definition.
|
|
116
|
-
* One key maps to many values.
|
|
117
|
-
*
|
|
118
|
-
* @example
|
|
119
|
-
* ```typescript
|
|
120
|
-
* Loader.grouped<string, Post>({
|
|
121
|
-
* batch: (authorIds) => db.getPostsForAuthors(authorIds),
|
|
122
|
-
* groupBy: (post) => post.authorId,
|
|
123
|
-
* })
|
|
124
|
-
* ```
|
|
125
|
-
*/
|
|
126
|
-
function grouped<K, V, R = never>(config: {
|
|
127
|
-
batch: (keys: readonly K[]) => Effect.Effect<readonly V[], Error, R>
|
|
128
|
-
groupBy: (value: V) => K
|
|
129
|
-
}): GroupedLoaderDef<K, V, R> {
|
|
130
|
-
return {
|
|
131
|
-
_tag: "grouped",
|
|
132
|
-
batch: config.batch,
|
|
133
|
-
groupBy: config.groupBy,
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ============================================================================
|
|
138
|
-
// Loader Registry
|
|
139
|
-
// ============================================================================
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* A registry of loader definitions with methods to create instances and layers
|
|
143
|
-
*/
|
|
144
|
-
class LoaderRegistry<Defs extends Record<string, LoaderDef<any, any, any>>> {
|
|
145
|
-
readonly _tag = "LoaderRegistry"
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* The Effect service tag for this loader registry
|
|
149
|
-
*/
|
|
150
|
-
readonly Service: Context.Tag<LoaderInstances<Defs>, LoaderInstances<Defs>>
|
|
151
|
-
|
|
152
|
-
constructor(readonly definitions: Defs) {
|
|
153
|
-
this.Service = Context.GenericTag<LoaderInstances<Defs>>(
|
|
154
|
-
`DataLoaders(${Object.keys(definitions).join(", ")})`
|
|
155
|
-
)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Create a Layer that provides fresh DataLoader instances.
|
|
160
|
-
* Call this once per request to get request-scoped loaders.
|
|
161
|
-
*/
|
|
162
|
-
toLayer(): Layer.Layer<LoaderInstances<Defs>, never, LoaderRequirements<Defs>> {
|
|
163
|
-
const self = this
|
|
164
|
-
return Layer.effect(
|
|
165
|
-
this.Service,
|
|
166
|
-
Effect.gen(function* () {
|
|
167
|
-
const instances: Record<string, DataLoader<any, any>> = {}
|
|
168
|
-
|
|
169
|
-
for (const [name, def] of Object.entries(self.definitions)) {
|
|
170
|
-
instances[name] = yield* createDataLoader(def)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return instances as LoaderInstances<Defs>
|
|
174
|
-
})
|
|
175
|
-
) as Layer.Layer<LoaderInstances<Defs>, never, LoaderRequirements<Defs>>
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Helper to use loaders in a resolver with a callback.
|
|
180
|
-
*/
|
|
181
|
-
use<A>(
|
|
182
|
-
fn: (loaders: LoaderInstances<Defs>) => Promise<A>
|
|
183
|
-
): Effect.Effect<A, Error, LoaderInstances<Defs>> {
|
|
184
|
-
const self = this
|
|
185
|
-
return Effect.gen(function* () {
|
|
186
|
-
const loaders = yield* self.Service
|
|
187
|
-
return yield* Effect.promise(() => fn(loaders))
|
|
188
|
-
})
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Load a single value by key.
|
|
193
|
-
* This is the most common operation in resolvers.
|
|
194
|
-
*/
|
|
195
|
-
load<Name extends keyof Defs & string>(
|
|
196
|
-
name: Name,
|
|
197
|
-
key: LoaderKey<Defs[Name]>
|
|
198
|
-
): Effect.Effect<LoaderValue<Defs[Name]>, Error, LoaderInstances<Defs>> {
|
|
199
|
-
const self = this
|
|
200
|
-
return Effect.gen(function* () {
|
|
201
|
-
const loaders = yield* self.Service
|
|
202
|
-
const loader = loaders[name] as DataLoader<any, any>
|
|
203
|
-
return yield* Effect.promise(() => loader.load(key))
|
|
204
|
-
})
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Load multiple values by keys.
|
|
209
|
-
* All keys are batched into a single request.
|
|
210
|
-
*/
|
|
211
|
-
loadMany<Name extends keyof Defs & string>(
|
|
212
|
-
name: Name,
|
|
213
|
-
keys: readonly LoaderKey<Defs[Name]>[]
|
|
214
|
-
): Effect.Effect<readonly LoaderValue<Defs[Name]>[], Error, LoaderInstances<Defs>> {
|
|
215
|
-
const self = this
|
|
216
|
-
return Effect.gen(function* () {
|
|
217
|
-
const loaders = yield* self.Service
|
|
218
|
-
const loader = loaders[name] as DataLoader<any, any>
|
|
219
|
-
const results = yield* Effect.promise(() => loader.loadMany(keys))
|
|
220
|
-
// Convert any errors to a failure
|
|
221
|
-
for (const result of results) {
|
|
222
|
-
if (result instanceof Error) {
|
|
223
|
-
return yield* Effect.fail(result)
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return results as readonly LoaderValue<Defs[Name]>[]
|
|
227
|
-
})
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Create a DataLoader from a loader definition
|
|
233
|
-
*/
|
|
234
|
-
function createDataLoader<K, V, R>(
|
|
235
|
-
def: LoaderDef<K, V, R>
|
|
236
|
-
): Effect.Effect<DataLoader<K, any>, never, R> {
|
|
237
|
-
return Effect.gen(function* () {
|
|
238
|
-
// Capture context for use in batch function
|
|
239
|
-
const context = yield* Effect.context<R>()
|
|
240
|
-
|
|
241
|
-
if (def._tag === "single") {
|
|
242
|
-
const loader = new DataLoader<K, V>(async (keys) => {
|
|
243
|
-
const items = await Effect.runPromise(def.batch(keys).pipe(Effect.provide(context)))
|
|
244
|
-
// Map items back to keys in order
|
|
245
|
-
return keys.map((key) => {
|
|
246
|
-
const item = items.find((i) => def.key(i) === key)
|
|
247
|
-
if (!item) return new Error(`Not found: ${key}`) as any
|
|
248
|
-
return item
|
|
249
|
-
})
|
|
250
|
-
})
|
|
251
|
-
return loader
|
|
252
|
-
} else {
|
|
253
|
-
// Grouped loader
|
|
254
|
-
const loader = new DataLoader<K, V[]>(async (keys) => {
|
|
255
|
-
const items = await Effect.runPromise(def.batch(keys).pipe(Effect.provide(context)))
|
|
256
|
-
// Group items by key with lazy array initialization
|
|
257
|
-
// Only create arrays for keys that have matching items
|
|
258
|
-
const map = new Map<K, V[]>()
|
|
259
|
-
for (const item of items) {
|
|
260
|
-
const key = def.groupBy(item)
|
|
261
|
-
let arr = map.get(key)
|
|
262
|
-
if (!arr) {
|
|
263
|
-
arr = []
|
|
264
|
-
map.set(key, arr)
|
|
265
|
-
}
|
|
266
|
-
arr.push(item)
|
|
267
|
-
}
|
|
268
|
-
// Return results in key order, defaulting to empty array for missing keys
|
|
269
|
-
return keys.map((key) => map.get(key) ?? [])
|
|
270
|
-
})
|
|
271
|
-
return loader
|
|
272
|
-
}
|
|
273
|
-
})
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ============================================================================
|
|
277
|
-
// Public API
|
|
278
|
-
// ============================================================================
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Define a set of loaders.
|
|
282
|
-
*
|
|
283
|
-
* @example
|
|
284
|
-
* ```typescript
|
|
285
|
-
* const loaders = Loader.define({
|
|
286
|
-
* UserById: Loader.single<string, User>({
|
|
287
|
-
* batch: (ids) => db.getUsersByIds(ids),
|
|
288
|
-
* key: (user) => user.id,
|
|
289
|
-
* }),
|
|
290
|
-
* PostsByAuthorId: Loader.grouped<string, Post>({
|
|
291
|
-
* batch: (ids) => db.getPostsForAuthors(ids),
|
|
292
|
-
* groupBy: (post) => post.authorId,
|
|
293
|
-
* }),
|
|
294
|
-
* })
|
|
295
|
-
*
|
|
296
|
-
* // In resolvers:
|
|
297
|
-
* loaders.load("UserById", "123")
|
|
298
|
-
* loaders.loadMany("UserById", ["1", "2", "3"])
|
|
299
|
-
* ```
|
|
300
|
-
*/
|
|
301
|
-
function define<Defs extends Record<string, LoaderDef<any, any, any>>>(
|
|
302
|
-
definitions: Defs
|
|
303
|
-
): LoaderRegistry<Defs> {
|
|
304
|
-
return new LoaderRegistry(definitions)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// ============================================================================
|
|
308
|
-
// Utility Functions
|
|
309
|
-
// ============================================================================
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Map an array of items to match requested keys.
|
|
313
|
-
* Returns items in the same order as keys, with errors for missing items.
|
|
314
|
-
*/
|
|
315
|
-
function mapByKey<K, V>(
|
|
316
|
-
keys: readonly K[],
|
|
317
|
-
items: readonly V[],
|
|
318
|
-
keyFn: (item: V) => K
|
|
319
|
-
): (V | Error)[] {
|
|
320
|
-
const map = new Map<K, V>()
|
|
321
|
-
for (const item of items) {
|
|
322
|
-
map.set(keyFn(item), item)
|
|
323
|
-
}
|
|
324
|
-
return keys.map((key) => map.get(key) ?? new Error(`Not found: ${key}`))
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Group an array of items by a key function.
|
|
329
|
-
* Returns a Map from key to array of matching items.
|
|
330
|
-
* Guarantees an entry (possibly empty) for each requested key.
|
|
331
|
-
*/
|
|
332
|
-
function groupByKey<K, V>(
|
|
333
|
-
keys: readonly K[],
|
|
334
|
-
items: readonly V[],
|
|
335
|
-
keyFn: (item: V) => K
|
|
336
|
-
): Map<K, V[]> {
|
|
337
|
-
const map = new Map<K, V[]>()
|
|
338
|
-
// Initialize empty arrays for all requested keys
|
|
339
|
-
for (const key of keys) {
|
|
340
|
-
map.set(key, [])
|
|
341
|
-
}
|
|
342
|
-
// Fill in items
|
|
343
|
-
for (const item of items) {
|
|
344
|
-
const key = keyFn(item)
|
|
345
|
-
const arr = map.get(key)
|
|
346
|
-
if (arr) arr.push(item)
|
|
347
|
-
}
|
|
348
|
-
return map
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// ============================================================================
|
|
352
|
-
// Export
|
|
353
|
-
// ============================================================================
|
|
354
|
-
|
|
355
|
-
export const Loader = {
|
|
356
|
-
define,
|
|
357
|
-
single,
|
|
358
|
-
grouped,
|
|
359
|
-
mapByKey,
|
|
360
|
-
groupByKey,
|
|
361
|
-
} as const
|
|
362
|
-
|
|
363
|
-
export type { LoaderRegistry, LoaderDef, LoaderInstances }
|
package/src/resolver-context.ts
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import { Effect, Context, Layer, Ref, Option, HashMap } from "effect"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A type-safe context system for passing values through the resolver hierarchy.
|
|
5
|
-
*
|
|
6
|
-
* Unlike simple property bags, this provides:
|
|
7
|
-
* - Type-safe slots that know their value type
|
|
8
|
-
* - Clear errors when required context is missing
|
|
9
|
-
* - Request-scoped storage that works across nested resolvers
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```typescript
|
|
13
|
-
* // Define a context slot
|
|
14
|
-
* const AuthPrincipal = ResolverContext.make<User>("AuthPrincipal")
|
|
15
|
-
*
|
|
16
|
-
* // Provide in a directive
|
|
17
|
-
* .directive({
|
|
18
|
-
* name: "auth",
|
|
19
|
-
* apply: () => (effect) => Effect.gen(function*() {
|
|
20
|
-
* const user = yield* validateJwt()
|
|
21
|
-
* yield* ResolverContext.set(AuthPrincipal, user)
|
|
22
|
-
* return yield* effect
|
|
23
|
-
* }),
|
|
24
|
-
* })
|
|
25
|
-
*
|
|
26
|
-
* // Access in any nested resolver
|
|
27
|
-
* .field("User", "posts", {
|
|
28
|
-
* resolve: (parent) => Effect.gen(function*() {
|
|
29
|
-
* const user = yield* ResolverContext.get(AuthPrincipal)
|
|
30
|
-
* // ...
|
|
31
|
-
* }),
|
|
32
|
-
* })
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Error thrown when trying to access a context value that hasn't been set
|
|
38
|
-
*/
|
|
39
|
-
export class MissingResolverContextError extends Error {
|
|
40
|
-
readonly _tag = "MissingResolverContextError"
|
|
41
|
-
|
|
42
|
-
constructor(readonly contextName: string) {
|
|
43
|
-
super(
|
|
44
|
-
`Resolver context "${contextName}" was not provided. Ensure a parent resolver or directive provides this context.`
|
|
45
|
-
)
|
|
46
|
-
this.name = "MissingResolverContextError"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* A typed context slot that can hold a value of type A
|
|
52
|
-
*/
|
|
53
|
-
export interface ResolverContextSlot<A> {
|
|
54
|
-
readonly _tag: "ResolverContextSlot"
|
|
55
|
-
readonly name: string
|
|
56
|
-
readonly _A: A // Phantom type for type inference
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Internal storage for resolver context values.
|
|
61
|
-
* This is a request-scoped service that holds all context values.
|
|
62
|
-
*/
|
|
63
|
-
export interface ResolverContextStore {
|
|
64
|
-
readonly ref: Ref.Ref<HashMap.HashMap<string, unknown>>
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export const ResolverContextStore = Context.GenericTag<ResolverContextStore>(
|
|
68
|
-
"effect-gql/ResolverContextStore"
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Create a Layer that provides the ResolverContextStore.
|
|
73
|
-
* This should be included in the request layer.
|
|
74
|
-
*/
|
|
75
|
-
export const makeStoreLayer = (): Effect.Effect<Layer.Layer<ResolverContextStore>> =>
|
|
76
|
-
Effect.map(Ref.make(HashMap.empty<string, unknown>()), (ref) =>
|
|
77
|
-
Layer.succeed(ResolverContextStore, { ref })
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Create a Layer that provides an empty ResolverContextStore.
|
|
82
|
-
* Convenience function for creating a fresh store layer.
|
|
83
|
-
*/
|
|
84
|
-
export const storeLayer: Layer.Layer<ResolverContextStore> = Layer.effect(
|
|
85
|
-
ResolverContextStore,
|
|
86
|
-
Effect.map(Ref.make(HashMap.empty<string, unknown>()), (ref) => ({ ref }))
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Create a new resolver context slot.
|
|
91
|
-
*
|
|
92
|
-
* The name is used for error messages when the context is accessed but not set.
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* ```typescript
|
|
96
|
-
* const AuthPrincipal = ResolverContext.make<User>("AuthPrincipal")
|
|
97
|
-
* const TenantId = ResolverContext.make<string>("TenantId")
|
|
98
|
-
* ```
|
|
99
|
-
*/
|
|
100
|
-
export const make = <A>(name: string): ResolverContextSlot<A> => ({
|
|
101
|
-
_tag: "ResolverContextSlot",
|
|
102
|
-
name,
|
|
103
|
-
_A: undefined as unknown as A,
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Get a value from the resolver context.
|
|
108
|
-
*
|
|
109
|
-
* Fails with MissingResolverContextError if the context was not set
|
|
110
|
-
* by a parent resolver or directive.
|
|
111
|
-
*
|
|
112
|
-
* @example
|
|
113
|
-
* ```typescript
|
|
114
|
-
* const effect = Effect.gen(function*() {
|
|
115
|
-
* const user = yield* ResolverContext.get(AuthPrincipal)
|
|
116
|
-
* // user is typed as User
|
|
117
|
-
* })
|
|
118
|
-
* ```
|
|
119
|
-
*/
|
|
120
|
-
export const get = <A>(
|
|
121
|
-
slot: ResolverContextSlot<A>
|
|
122
|
-
): Effect.Effect<A, MissingResolverContextError, ResolverContextStore> =>
|
|
123
|
-
Effect.flatMap(ResolverContextStore, (store) =>
|
|
124
|
-
Effect.flatMap(Ref.get(store.ref), (map) => {
|
|
125
|
-
const value = HashMap.get(map, slot.name)
|
|
126
|
-
return Option.match(value, {
|
|
127
|
-
onNone: () => Effect.fail(new MissingResolverContextError(slot.name)),
|
|
128
|
-
onSome: (v) => Effect.succeed(v as A),
|
|
129
|
-
})
|
|
130
|
-
})
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Get a value from the resolver context as an Option.
|
|
135
|
-
*
|
|
136
|
-
* Returns None if the context was not set, instead of failing.
|
|
137
|
-
* Useful when context is optional.
|
|
138
|
-
*/
|
|
139
|
-
export const getOption = <A>(
|
|
140
|
-
slot: ResolverContextSlot<A>
|
|
141
|
-
): Effect.Effect<Option.Option<A>, never, ResolverContextStore> =>
|
|
142
|
-
Effect.flatMap(ResolverContextStore, (store) =>
|
|
143
|
-
Effect.map(Ref.get(store.ref), (map) => HashMap.get(map, slot.name) as Option.Option<A>)
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Set a value in the resolver context.
|
|
148
|
-
*
|
|
149
|
-
* The value will be available to all subsequent resolver calls in this request.
|
|
150
|
-
* This mutates the request-scoped store, so nested resolvers will see the value.
|
|
151
|
-
*
|
|
152
|
-
* @example
|
|
153
|
-
* ```typescript
|
|
154
|
-
* // In a directive
|
|
155
|
-
* const withAuth = (effect) => Effect.gen(function*() {
|
|
156
|
-
* const user = yield* validateJwt()
|
|
157
|
-
* yield* ResolverContext.set(AuthPrincipal, user)
|
|
158
|
-
* return yield* effect
|
|
159
|
-
* })
|
|
160
|
-
* ```
|
|
161
|
-
*/
|
|
162
|
-
export const set = <A>(
|
|
163
|
-
slot: ResolverContextSlot<A>,
|
|
164
|
-
value: A
|
|
165
|
-
): Effect.Effect<void, never, ResolverContextStore> =>
|
|
166
|
-
Effect.flatMap(ResolverContextStore, (store) =>
|
|
167
|
-
Ref.update(store.ref, (map) => HashMap.set(map, slot.name, value))
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Set multiple context values at once.
|
|
172
|
-
*/
|
|
173
|
-
export const setMany = (
|
|
174
|
-
values: ReadonlyArray<readonly [ResolverContextSlot<any>, any]>
|
|
175
|
-
): Effect.Effect<void, never, ResolverContextStore> =>
|
|
176
|
-
Effect.flatMap(ResolverContextStore, (store) =>
|
|
177
|
-
Ref.update(store.ref, (map) => {
|
|
178
|
-
let result = map
|
|
179
|
-
for (const [slot, value] of values) {
|
|
180
|
-
result = HashMap.set(result, slot.name, value)
|
|
181
|
-
}
|
|
182
|
-
return result
|
|
183
|
-
})
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Check if a context slot has a value set.
|
|
188
|
-
*/
|
|
189
|
-
export const has = <A>(
|
|
190
|
-
slot: ResolverContextSlot<A>
|
|
191
|
-
): Effect.Effect<boolean, never, ResolverContextStore> =>
|
|
192
|
-
Effect.flatMap(ResolverContextStore, (store) =>
|
|
193
|
-
Effect.map(Ref.get(store.ref), (map) => HashMap.has(map, slot.name))
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Get a value or return a default if not set.
|
|
198
|
-
*/
|
|
199
|
-
export const getOrElse = <A>(
|
|
200
|
-
slot: ResolverContextSlot<A>,
|
|
201
|
-
orElse: () => A
|
|
202
|
-
): Effect.Effect<A, never, ResolverContextStore> =>
|
|
203
|
-
Effect.flatMap(ResolverContextStore, (store) =>
|
|
204
|
-
Effect.map(Ref.get(store.ref), (map) =>
|
|
205
|
-
Option.getOrElse(HashMap.get(map, slot.name) as Option.Option<A>, orElse)
|
|
206
|
-
)
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Run an effect with a temporary context value.
|
|
211
|
-
* The value is set before the effect runs and removed after.
|
|
212
|
-
* Useful for scoped context that shouldn't persist.
|
|
213
|
-
*/
|
|
214
|
-
export const scoped =
|
|
215
|
-
<A>(slot: ResolverContextSlot<A>, value: A) =>
|
|
216
|
-
<B, E, R>(effect: Effect.Effect<B, E, R>): Effect.Effect<B, E, R | ResolverContextStore> =>
|
|
217
|
-
Effect.flatMap(ResolverContextStore, (store) =>
|
|
218
|
-
Effect.acquireUseRelease(
|
|
219
|
-
// Acquire: save current value and set new one
|
|
220
|
-
Effect.flatMap(Ref.get(store.ref), (map) => {
|
|
221
|
-
const previous = HashMap.get(map, slot.name)
|
|
222
|
-
return Effect.as(Ref.set(store.ref, HashMap.set(map, slot.name, value)), previous)
|
|
223
|
-
}),
|
|
224
|
-
// Use: run the effect
|
|
225
|
-
() => effect,
|
|
226
|
-
// Release: restore previous value
|
|
227
|
-
(previous) =>
|
|
228
|
-
Ref.update(store.ref, (map) =>
|
|
229
|
-
Option.match(previous, {
|
|
230
|
-
onNone: () => HashMap.remove(map, slot.name),
|
|
231
|
-
onSome: (v) => HashMap.set(map, slot.name, v),
|
|
232
|
-
})
|
|
233
|
-
)
|
|
234
|
-
)
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Namespace for ResolverContext functions
|
|
239
|
-
*/
|
|
240
|
-
export const ResolverContext = {
|
|
241
|
-
make,
|
|
242
|
-
get,
|
|
243
|
-
getOption,
|
|
244
|
-
set,
|
|
245
|
-
setMany,
|
|
246
|
-
has,
|
|
247
|
-
getOrElse,
|
|
248
|
-
scoped,
|
|
249
|
-
storeLayer,
|
|
250
|
-
makeStoreLayer,
|
|
251
|
-
Store: ResolverContextStore,
|
|
252
|
-
MissingResolverContextError,
|
|
253
|
-
} as const
|