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