@effect-gql/federation 0.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/LICENSE +7 -0
- package/dist/directives.d.ts +136 -0
- package/dist/directives.d.ts.map +1 -0
- package/dist/directives.js +171 -0
- package/dist/directives.js.map +1 -0
- package/dist/entities.d.ts +31 -0
- package/dist/entities.d.ts.map +1 -0
- package/dist/entities.js +76 -0
- package/dist/entities.js.map +1 -0
- package/dist/federated-builder.d.ts +182 -0
- package/dist/federated-builder.d.ts.map +1 -0
- package/dist/federated-builder.js +442 -0
- package/dist/federated-builder.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/pipe-api.d.ts +163 -0
- package/dist/pipe-api.d.ts.map +1 -0
- package/dist/pipe-api.js +127 -0
- package/dist/pipe-api.js.map +1 -0
- package/dist/scalars.d.ts +12 -0
- package/dist/scalars.d.ts.map +1 -0
- package/dist/scalars.js +59 -0
- package/dist/scalars.js.map +1 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +41 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -0
- package/src/directives.ts +170 -0
- package/src/entities.ts +90 -0
- package/src/federated-builder.ts +593 -0
- package/src/index.ts +47 -0
- package/src/pipe-api.ts +263 -0
- package/src/scalars.ts +59 -0
- package/src/types.ts +114 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import { Effect, Pipeable } from "effect"
|
|
2
|
+
import * as S from "effect/Schema"
|
|
3
|
+
import {
|
|
4
|
+
GraphQLSchemaBuilder,
|
|
5
|
+
GraphQLSchema,
|
|
6
|
+
GraphQLObjectType,
|
|
7
|
+
GraphQLList,
|
|
8
|
+
GraphQLNonNull,
|
|
9
|
+
printSchema,
|
|
10
|
+
type DirectiveApplication,
|
|
11
|
+
} from "@effect-gql/core"
|
|
12
|
+
import { AnyScalar, FieldSetScalar } from "./scalars"
|
|
13
|
+
import {
|
|
14
|
+
createEntityUnion,
|
|
15
|
+
createEntitiesResolver,
|
|
16
|
+
createServiceType,
|
|
17
|
+
createServiceResolver,
|
|
18
|
+
} from "./entities"
|
|
19
|
+
import type { EntityRegistration, FederatedSchemaConfig, FederatedSchemaResult } from "./types"
|
|
20
|
+
import { toDirectiveApplication } from "./types"
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Internal state for the federated builder
|
|
24
|
+
*/
|
|
25
|
+
interface FederatedBuilderState<R> {
|
|
26
|
+
/** Underlying core builder */
|
|
27
|
+
coreBuilder: GraphQLSchemaBuilder<R>
|
|
28
|
+
/** Registered entities */
|
|
29
|
+
entities: Map<string, EntityRegistration<any, any>>
|
|
30
|
+
/** Federation version */
|
|
31
|
+
version: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Federation-aware schema builder that extends the core GraphQLSchemaBuilder
|
|
36
|
+
* with Apollo Federation 2.x support.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* const schema = FederatedSchemaBuilder.empty
|
|
41
|
+
* .pipe(
|
|
42
|
+
* entity({
|
|
43
|
+
* name: "User",
|
|
44
|
+
* schema: UserSchema,
|
|
45
|
+
* keys: [key({ fields: "id" })],
|
|
46
|
+
* resolveReference: (ref) => UserService.findById(ref.id),
|
|
47
|
+
* }),
|
|
48
|
+
* query("me", {
|
|
49
|
+
* type: UserSchema,
|
|
50
|
+
* resolve: () => UserService.getCurrentUser(),
|
|
51
|
+
* }),
|
|
52
|
+
* )
|
|
53
|
+
* .buildFederatedSchema()
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export class FederatedSchemaBuilder<R = never> implements Pipeable.Pipeable {
|
|
57
|
+
private constructor(private readonly state: FederatedBuilderState<R>) {}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Pipeable interface implementation
|
|
61
|
+
*/
|
|
62
|
+
pipe<A>(this: A): A
|
|
63
|
+
pipe<A, B>(this: A, ab: (a: A) => B): B
|
|
64
|
+
pipe<A, B, C>(this: A, ab: (a: A) => B, bc: (b: B) => C): C
|
|
65
|
+
pipe<A, B, C, D>(this: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D
|
|
66
|
+
pipe<A, B, C, D, E>(
|
|
67
|
+
this: A,
|
|
68
|
+
ab: (a: A) => B,
|
|
69
|
+
bc: (b: B) => C,
|
|
70
|
+
cd: (c: C) => D,
|
|
71
|
+
de: (d: D) => E
|
|
72
|
+
): E
|
|
73
|
+
pipe<A, B, C, D, E, F>(
|
|
74
|
+
this: A,
|
|
75
|
+
ab: (a: A) => B,
|
|
76
|
+
bc: (b: B) => C,
|
|
77
|
+
cd: (c: C) => D,
|
|
78
|
+
de: (d: D) => E,
|
|
79
|
+
ef: (e: E) => F
|
|
80
|
+
): F
|
|
81
|
+
pipe() {
|
|
82
|
+
return Pipeable.pipeArguments(this, arguments)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create an empty federated schema builder
|
|
87
|
+
*/
|
|
88
|
+
static empty = new FederatedSchemaBuilder<never>({
|
|
89
|
+
coreBuilder: GraphQLSchemaBuilder.empty,
|
|
90
|
+
entities: new Map(),
|
|
91
|
+
version: "2.3",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a builder with custom configuration
|
|
96
|
+
*/
|
|
97
|
+
static create(config: FederatedSchemaConfig = {}) {
|
|
98
|
+
return new FederatedSchemaBuilder<never>({
|
|
99
|
+
coreBuilder: GraphQLSchemaBuilder.empty,
|
|
100
|
+
entities: new Map(),
|
|
101
|
+
version: config.version ?? "2.3",
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a new builder with updated state
|
|
107
|
+
*/
|
|
108
|
+
private with<R2>(updates: Partial<FederatedBuilderState<R2>>): FederatedSchemaBuilder<R | R2> {
|
|
109
|
+
return new FederatedSchemaBuilder({
|
|
110
|
+
...this.state,
|
|
111
|
+
...updates,
|
|
112
|
+
} as FederatedBuilderState<R | R2>)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the underlying core builder for advanced usage
|
|
117
|
+
*/
|
|
118
|
+
get coreBuilder(): GraphQLSchemaBuilder<R> {
|
|
119
|
+
return this.state.coreBuilder
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// Entity Registration
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Register an entity type with @key directive(s) and reference resolver.
|
|
128
|
+
*
|
|
129
|
+
* Entities are the core building block of Apollo Federation. They represent
|
|
130
|
+
* types that can be resolved across subgraph boundaries using their key fields.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* builder.entity({
|
|
135
|
+
* name: "User",
|
|
136
|
+
* schema: UserSchema,
|
|
137
|
+
* keys: [key({ fields: "id" })],
|
|
138
|
+
* resolveReference: (ref) => UserService.findById(ref.id),
|
|
139
|
+
* })
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
entity<A, R2>(config: EntityRegistration<A, R2>): FederatedSchemaBuilder<R | R2> {
|
|
143
|
+
const { name, schema, keys, directives } = config
|
|
144
|
+
|
|
145
|
+
// Build directive applications for the object type
|
|
146
|
+
const typeDirectives: DirectiveApplication[] = [
|
|
147
|
+
// Add @key directives
|
|
148
|
+
...keys.map(
|
|
149
|
+
(k): DirectiveApplication => ({
|
|
150
|
+
name: "key",
|
|
151
|
+
args: {
|
|
152
|
+
fields: k.fields,
|
|
153
|
+
...(k.resolvable !== undefined ? { resolvable: k.resolvable } : {}),
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
),
|
|
157
|
+
// Add additional directives
|
|
158
|
+
...(directives?.map(toDirectiveApplication) ?? []),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
// Register the entity as an object type with directives
|
|
162
|
+
const newCoreBuilder = this.state.coreBuilder.objectType({
|
|
163
|
+
name,
|
|
164
|
+
schema,
|
|
165
|
+
directives: typeDirectives,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// Add to entities map
|
|
169
|
+
const newEntities = new Map(this.state.entities)
|
|
170
|
+
newEntities.set(name, config)
|
|
171
|
+
|
|
172
|
+
return this.with({
|
|
173
|
+
coreBuilder: newCoreBuilder as GraphQLSchemaBuilder<any>,
|
|
174
|
+
entities: newEntities,
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// Delegate to Core Builder
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Add a query field
|
|
184
|
+
*/
|
|
185
|
+
query<A, E, R2, Args = void>(
|
|
186
|
+
name: string,
|
|
187
|
+
config: {
|
|
188
|
+
type: S.Schema<A, any, any>
|
|
189
|
+
args?: S.Schema<Args, any, any>
|
|
190
|
+
description?: string
|
|
191
|
+
directives?: readonly DirectiveApplication[]
|
|
192
|
+
resolve: (args: Args) => Effect.Effect<A, E, R2>
|
|
193
|
+
}
|
|
194
|
+
): FederatedSchemaBuilder<R | R2> {
|
|
195
|
+
return this.with({
|
|
196
|
+
coreBuilder: this.state.coreBuilder.query(name, config) as GraphQLSchemaBuilder<any>,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Add a mutation field
|
|
202
|
+
*/
|
|
203
|
+
mutation<A, E, R2, Args = void>(
|
|
204
|
+
name: string,
|
|
205
|
+
config: {
|
|
206
|
+
type: S.Schema<A, any, any>
|
|
207
|
+
args?: S.Schema<Args, any, any>
|
|
208
|
+
description?: string
|
|
209
|
+
directives?: readonly DirectiveApplication[]
|
|
210
|
+
resolve: (args: Args) => Effect.Effect<A, E, R2>
|
|
211
|
+
}
|
|
212
|
+
): FederatedSchemaBuilder<R | R2> {
|
|
213
|
+
return this.with({
|
|
214
|
+
coreBuilder: this.state.coreBuilder.mutation(name, config) as GraphQLSchemaBuilder<any>,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Add a subscription field
|
|
220
|
+
*/
|
|
221
|
+
subscription<A, E, R2, Args = void>(
|
|
222
|
+
name: string,
|
|
223
|
+
config: {
|
|
224
|
+
type: S.Schema<A, any, any>
|
|
225
|
+
args?: S.Schema<Args, any, any>
|
|
226
|
+
description?: string
|
|
227
|
+
directives?: readonly DirectiveApplication[]
|
|
228
|
+
subscribe: (args: Args) => Effect.Effect<import("effect").Stream.Stream<A, E, R2>, E, R2>
|
|
229
|
+
resolve?: (value: A, args: Args) => Effect.Effect<A, E, R2>
|
|
230
|
+
}
|
|
231
|
+
): FederatedSchemaBuilder<R | R2> {
|
|
232
|
+
return this.with({
|
|
233
|
+
coreBuilder: this.state.coreBuilder.subscription(name, config) as GraphQLSchemaBuilder<any>,
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Register an object type (non-entity)
|
|
239
|
+
*/
|
|
240
|
+
objectType<A, R2 = never>(config: {
|
|
241
|
+
name?: string
|
|
242
|
+
schema: S.Schema<A, any, any>
|
|
243
|
+
implements?: readonly string[]
|
|
244
|
+
directives?: readonly DirectiveApplication[]
|
|
245
|
+
}): FederatedSchemaBuilder<R | R2> {
|
|
246
|
+
return this.with({
|
|
247
|
+
coreBuilder: this.state.coreBuilder.objectType(config) as GraphQLSchemaBuilder<any>,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Register an interface type
|
|
253
|
+
*/
|
|
254
|
+
interfaceType(config: {
|
|
255
|
+
name?: string
|
|
256
|
+
schema: S.Schema<any, any, any>
|
|
257
|
+
resolveType?: (value: any) => string
|
|
258
|
+
directives?: readonly DirectiveApplication[]
|
|
259
|
+
}): FederatedSchemaBuilder<R> {
|
|
260
|
+
return this.with({
|
|
261
|
+
coreBuilder: this.state.coreBuilder.interfaceType(config),
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Register an enum type
|
|
267
|
+
*/
|
|
268
|
+
enumType(config: {
|
|
269
|
+
name: string
|
|
270
|
+
values: readonly string[]
|
|
271
|
+
description?: string
|
|
272
|
+
directives?: readonly DirectiveApplication[]
|
|
273
|
+
}): FederatedSchemaBuilder<R> {
|
|
274
|
+
return this.with({
|
|
275
|
+
coreBuilder: this.state.coreBuilder.enumType(config),
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Register a union type
|
|
281
|
+
*/
|
|
282
|
+
unionType(config: {
|
|
283
|
+
name: string
|
|
284
|
+
types: readonly string[]
|
|
285
|
+
resolveType?: (value: any) => string
|
|
286
|
+
directives?: readonly DirectiveApplication[]
|
|
287
|
+
}): FederatedSchemaBuilder<R> {
|
|
288
|
+
return this.with({
|
|
289
|
+
coreBuilder: this.state.coreBuilder.unionType(config),
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Register an input type
|
|
295
|
+
*/
|
|
296
|
+
inputType(config: {
|
|
297
|
+
name?: string
|
|
298
|
+
schema: S.Schema<any, any, any>
|
|
299
|
+
description?: string
|
|
300
|
+
directives?: readonly DirectiveApplication[]
|
|
301
|
+
}): FederatedSchemaBuilder<R> {
|
|
302
|
+
return this.with({
|
|
303
|
+
coreBuilder: this.state.coreBuilder.inputType(config),
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Add a computed/relational field to an object type
|
|
309
|
+
*/
|
|
310
|
+
field<Parent, A, E, R2, Args = void>(
|
|
311
|
+
typeName: string,
|
|
312
|
+
fieldName: string,
|
|
313
|
+
config: {
|
|
314
|
+
type: S.Schema<A, any, any>
|
|
315
|
+
args?: S.Schema<Args, any, any>
|
|
316
|
+
description?: string
|
|
317
|
+
directives?: readonly DirectiveApplication[]
|
|
318
|
+
resolve: (parent: Parent, args: Args) => Effect.Effect<A, E, R2>
|
|
319
|
+
}
|
|
320
|
+
): FederatedSchemaBuilder<R | R2> {
|
|
321
|
+
return this.with({
|
|
322
|
+
coreBuilder: this.state.coreBuilder.field(
|
|
323
|
+
typeName,
|
|
324
|
+
fieldName,
|
|
325
|
+
config
|
|
326
|
+
) as GraphQLSchemaBuilder<any>,
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// Schema Building
|
|
332
|
+
// ============================================================================
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Build the federated GraphQL schema with _entities and _service queries.
|
|
336
|
+
*
|
|
337
|
+
* Returns both the executable schema and the Federation-compliant SDL.
|
|
338
|
+
*/
|
|
339
|
+
buildFederatedSchema(): FederatedSchemaResult {
|
|
340
|
+
// Add a dummy query if no queries exist to ensure schema builds properly
|
|
341
|
+
// This ensures all registered types are included in the schema
|
|
342
|
+
let builderForSchema = this.state.coreBuilder
|
|
343
|
+
|
|
344
|
+
// Check if we need a placeholder query by attempting to build
|
|
345
|
+
// We need at least one query for GraphQL schema to be valid
|
|
346
|
+
const needsPlaceholder = !this.hasQueryFields()
|
|
347
|
+
|
|
348
|
+
if (needsPlaceholder) {
|
|
349
|
+
builderForSchema = builderForSchema.query("_placeholder", {
|
|
350
|
+
type: S.String,
|
|
351
|
+
resolve: () => Effect.succeed("placeholder"),
|
|
352
|
+
}) as GraphQLSchemaBuilder<any>
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Build the base schema with all types included
|
|
356
|
+
const baseSchema = builderForSchema.buildSchema()
|
|
357
|
+
|
|
358
|
+
// Get the type registry from the base schema
|
|
359
|
+
const typeRegistry = new Map<string, GraphQLObjectType>()
|
|
360
|
+
const typeMap = baseSchema.getTypeMap()
|
|
361
|
+
|
|
362
|
+
for (const [name, type] of Object.entries(typeMap)) {
|
|
363
|
+
// Use constructor name check instead of instanceof to handle multiple graphql instances
|
|
364
|
+
const isObjectType = type.constructor.name === "GraphQLObjectType"
|
|
365
|
+
if (isObjectType && !name.startsWith("__")) {
|
|
366
|
+
typeRegistry.set(name, type as GraphQLObjectType)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Create federation types
|
|
371
|
+
const entityUnion =
|
|
372
|
+
this.state.entities.size > 0 ? createEntityUnion(this.state.entities, typeRegistry) : null
|
|
373
|
+
const serviceType = createServiceType()
|
|
374
|
+
|
|
375
|
+
// Build federation query fields
|
|
376
|
+
const federationQueryFields: Record<string, any> = {}
|
|
377
|
+
|
|
378
|
+
if (entityUnion) {
|
|
379
|
+
federationQueryFields._entities = {
|
|
380
|
+
type: new GraphQLNonNull(new GraphQLList(entityUnion)),
|
|
381
|
+
args: {
|
|
382
|
+
representations: {
|
|
383
|
+
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(AnyScalar))),
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
resolve: createEntitiesResolver(this.state.entities),
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Generate SDL before adding _service (to avoid circular reference)
|
|
391
|
+
const sdl = this.generateFederatedSDL(baseSchema, needsPlaceholder)
|
|
392
|
+
|
|
393
|
+
federationQueryFields._service = {
|
|
394
|
+
type: new GraphQLNonNull(serviceType),
|
|
395
|
+
resolve: createServiceResolver(sdl),
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Build the final schema by extending the base Query type
|
|
399
|
+
const baseQueryType = baseSchema.getQueryType()
|
|
400
|
+
const baseQueryFields = baseQueryType?.getFields() ?? {}
|
|
401
|
+
|
|
402
|
+
const queryType = new GraphQLObjectType({
|
|
403
|
+
name: "Query",
|
|
404
|
+
fields: () => {
|
|
405
|
+
const fields: Record<string, any> = {}
|
|
406
|
+
|
|
407
|
+
// Copy base query fields (excluding placeholder)
|
|
408
|
+
for (const [name, field] of Object.entries(baseQueryFields)) {
|
|
409
|
+
if (name === "_placeholder") continue
|
|
410
|
+
fields[name] = {
|
|
411
|
+
type: field.type,
|
|
412
|
+
args: field.args.reduce(
|
|
413
|
+
(acc, arg) => {
|
|
414
|
+
acc[arg.name] = {
|
|
415
|
+
type: arg.type,
|
|
416
|
+
description: arg.description,
|
|
417
|
+
defaultValue: arg.defaultValue,
|
|
418
|
+
}
|
|
419
|
+
return acc
|
|
420
|
+
},
|
|
421
|
+
{} as Record<string, any>
|
|
422
|
+
),
|
|
423
|
+
description: field.description,
|
|
424
|
+
resolve: field.resolve,
|
|
425
|
+
extensions: field.extensions,
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Add federation fields
|
|
430
|
+
Object.assign(fields, federationQueryFields)
|
|
431
|
+
|
|
432
|
+
return fields
|
|
433
|
+
},
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
// Collect all types for the schema
|
|
437
|
+
const types: any[] = [AnyScalar, FieldSetScalar, serviceType]
|
|
438
|
+
|
|
439
|
+
if (entityUnion) {
|
|
440
|
+
types.push(entityUnion)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Add all types from base schema except Query
|
|
444
|
+
for (const [name, type] of Object.entries(typeMap)) {
|
|
445
|
+
if (!name.startsWith("__") && name !== "Query") {
|
|
446
|
+
types.push(type)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const schema = new GraphQLSchema({
|
|
451
|
+
query: queryType,
|
|
452
|
+
mutation: baseSchema.getMutationType() ?? undefined,
|
|
453
|
+
subscription: baseSchema.getSubscriptionType() ?? undefined,
|
|
454
|
+
types,
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
return { schema, sdl }
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Check if the core builder has any query fields registered
|
|
462
|
+
*/
|
|
463
|
+
private hasQueryFields(): boolean {
|
|
464
|
+
// We need to check if there are any queries registered
|
|
465
|
+
// Since we can't access private state, we try building and check
|
|
466
|
+
try {
|
|
467
|
+
const schema = this.state.coreBuilder.buildSchema()
|
|
468
|
+
const queryType = schema.getQueryType()
|
|
469
|
+
return queryType !== null && queryType !== undefined
|
|
470
|
+
} catch {
|
|
471
|
+
return false
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Build a standard (non-federated) schema.
|
|
477
|
+
* Useful for testing or running without a gateway.
|
|
478
|
+
*/
|
|
479
|
+
buildSchema(): GraphQLSchema {
|
|
480
|
+
return this.state.coreBuilder.buildSchema()
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ============================================================================
|
|
484
|
+
// SDL Generation
|
|
485
|
+
// ============================================================================
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Generate Federation-compliant SDL with directive annotations.
|
|
489
|
+
*/
|
|
490
|
+
private generateFederatedSDL(schema: GraphQLSchema, excludePlaceholder: boolean = false): string {
|
|
491
|
+
// Start with federation schema extension
|
|
492
|
+
const lines: string[] = [
|
|
493
|
+
`extend schema @link(url: "https://specs.apollo.dev/federation/v${this.state.version}", import: ["@key", "@shareable", "@external", "@requires", "@provides", "@override", "@inaccessible", "@interfaceObject", "@tag"])`,
|
|
494
|
+
"",
|
|
495
|
+
]
|
|
496
|
+
|
|
497
|
+
// Print the base schema SDL
|
|
498
|
+
let baseSDL = printSchema(schema)
|
|
499
|
+
|
|
500
|
+
// Remove placeholder query if it was added
|
|
501
|
+
if (excludePlaceholder) {
|
|
502
|
+
baseSDL = baseSDL.replace(/\s*_placeholder:\s*String\n?/g, "")
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Process the SDL to add directive annotations
|
|
506
|
+
const annotatedSDL = this.annotateSDLWithDirectives(baseSDL, schema)
|
|
507
|
+
|
|
508
|
+
lines.push(annotatedSDL)
|
|
509
|
+
|
|
510
|
+
return lines.join("\n")
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Annotate SDL types with their federation directives from extensions.
|
|
515
|
+
*/
|
|
516
|
+
private annotateSDLWithDirectives(sdl: string, schema: GraphQLSchema): string {
|
|
517
|
+
const typeMap = schema.getTypeMap()
|
|
518
|
+
let result = sdl
|
|
519
|
+
|
|
520
|
+
for (const [typeName, type] of Object.entries(typeMap)) {
|
|
521
|
+
if (typeName.startsWith("__")) continue
|
|
522
|
+
|
|
523
|
+
const directives = (type.extensions as any)?.directives as DirectiveApplication[] | undefined
|
|
524
|
+
if (!directives || directives.length === 0) continue
|
|
525
|
+
|
|
526
|
+
const directiveStr = directives.map(formatDirective).join(" ")
|
|
527
|
+
|
|
528
|
+
// Match type definition and add directives
|
|
529
|
+
const typePattern = new RegExp(
|
|
530
|
+
`(type\\s+${typeName}(?:\\s+implements\\s+[^{]+)?)(\\s*\\{)`,
|
|
531
|
+
"g"
|
|
532
|
+
)
|
|
533
|
+
result = result.replace(typePattern, `$1 ${directiveStr}$2`)
|
|
534
|
+
|
|
535
|
+
const interfacePattern = new RegExp(`(interface\\s+${typeName})(\\s*\\{)`, "g")
|
|
536
|
+
result = result.replace(interfacePattern, `$1 ${directiveStr}$2`)
|
|
537
|
+
|
|
538
|
+
const enumPattern = new RegExp(`(enum\\s+${typeName})(\\s*\\{)`, "g")
|
|
539
|
+
result = result.replace(enumPattern, `$1 ${directiveStr}$2`)
|
|
540
|
+
|
|
541
|
+
const unionPattern = new RegExp(`(union\\s+${typeName})(\\s*=)`, "g")
|
|
542
|
+
result = result.replace(unionPattern, `$1 ${directiveStr}$2`)
|
|
543
|
+
|
|
544
|
+
const inputPattern = new RegExp(`(input\\s+${typeName})(\\s*\\{)`, "g")
|
|
545
|
+
result = result.replace(inputPattern, `$1 ${directiveStr}$2`)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Also annotate fields with directives
|
|
549
|
+
for (const [typeName, type] of Object.entries(typeMap)) {
|
|
550
|
+
if (typeName.startsWith("__")) continue
|
|
551
|
+
if (!(type instanceof GraphQLObjectType)) continue
|
|
552
|
+
|
|
553
|
+
const fields = type.getFields()
|
|
554
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
555
|
+
const fieldDirectives = (field.extensions as any)?.directives as
|
|
556
|
+
| DirectiveApplication[]
|
|
557
|
+
| undefined
|
|
558
|
+
if (!fieldDirectives || fieldDirectives.length === 0) continue
|
|
559
|
+
|
|
560
|
+
const directiveStr = fieldDirectives.map(formatDirective).join(" ")
|
|
561
|
+
|
|
562
|
+
// Only replace within the context of this type
|
|
563
|
+
const typeBlockPattern = new RegExp(
|
|
564
|
+
`(type\\s+${typeName}[^{]*\\{[\\s\\S]*?)(${fieldName}(?:\\([^)]*\\))?:\\s*[^\\n]+?)([\\n}])`,
|
|
565
|
+
"g"
|
|
566
|
+
)
|
|
567
|
+
result = result.replace(typeBlockPattern, `$1$2 ${directiveStr}$3`)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return result
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Format a DirectiveApplication as SDL string
|
|
577
|
+
*/
|
|
578
|
+
function formatDirective(directive: DirectiveApplication): string {
|
|
579
|
+
if (!directive.args || Object.keys(directive.args).length === 0) {
|
|
580
|
+
return `@${directive.name}`
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const args = Object.entries(directive.args)
|
|
584
|
+
.map(([key, value]) => {
|
|
585
|
+
if (typeof value === "string") {
|
|
586
|
+
return `${key}: "${value}"`
|
|
587
|
+
}
|
|
588
|
+
return `${key}: ${JSON.stringify(value)}`
|
|
589
|
+
})
|
|
590
|
+
.join(", ")
|
|
591
|
+
|
|
592
|
+
return `@${directive.name}(${args})`
|
|
593
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Core builder
|
|
2
|
+
export { FederatedSchemaBuilder } from "./federated-builder"
|
|
3
|
+
|
|
4
|
+
// Types
|
|
5
|
+
export type {
|
|
6
|
+
KeyDirective,
|
|
7
|
+
FederationDirective,
|
|
8
|
+
EntityRepresentation,
|
|
9
|
+
EntityRegistration,
|
|
10
|
+
FederatedSchemaConfig,
|
|
11
|
+
FederatedSchemaResult,
|
|
12
|
+
} from "./types"
|
|
13
|
+
export { toDirectiveApplication } from "./types"
|
|
14
|
+
|
|
15
|
+
// Directive factories
|
|
16
|
+
export {
|
|
17
|
+
key,
|
|
18
|
+
shareable,
|
|
19
|
+
inaccessible,
|
|
20
|
+
interfaceObject,
|
|
21
|
+
tag,
|
|
22
|
+
external,
|
|
23
|
+
requires,
|
|
24
|
+
provides,
|
|
25
|
+
override,
|
|
26
|
+
} from "./directives"
|
|
27
|
+
|
|
28
|
+
// Pipe-able API
|
|
29
|
+
export {
|
|
30
|
+
entity,
|
|
31
|
+
query,
|
|
32
|
+
mutation,
|
|
33
|
+
subscription,
|
|
34
|
+
objectType,
|
|
35
|
+
interfaceType,
|
|
36
|
+
enumType,
|
|
37
|
+
unionType,
|
|
38
|
+
inputType,
|
|
39
|
+
field,
|
|
40
|
+
externalField,
|
|
41
|
+
requiresField,
|
|
42
|
+
providesField,
|
|
43
|
+
overrideField,
|
|
44
|
+
} from "./pipe-api"
|
|
45
|
+
|
|
46
|
+
// Federation scalars (for advanced usage)
|
|
47
|
+
export { AnyScalar, FieldSetScalar } from "./scalars"
|