@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.
@@ -0,0 +1,170 @@
1
+ import type { FederationDirective, KeyDirective } from "./types"
2
+
3
+ // ============================================================================
4
+ // Type-Level Directive Factories
5
+ // ============================================================================
6
+
7
+ /**
8
+ * Create a @key directive for entity identification
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * entity({
13
+ * name: "User",
14
+ * schema: UserSchema,
15
+ * keys: [key({ fields: "id" })],
16
+ * resolveReference: (ref) => UserService.findById(ref.id),
17
+ * })
18
+ * ```
19
+ */
20
+ export const key = (config: KeyDirective): FederationDirective => ({
21
+ _tag: "key",
22
+ fields: config.fields,
23
+ resolvable: config.resolvable,
24
+ })
25
+
26
+ /**
27
+ * Create a @shareable directive
28
+ * Marks a type or field as resolvable by multiple subgraphs
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * entity({
33
+ * name: "Product",
34
+ * schema: ProductSchema,
35
+ * keys: [key({ fields: "id" })],
36
+ * directives: [shareable()],
37
+ * })
38
+ * ```
39
+ */
40
+ export const shareable = (): FederationDirective => ({
41
+ _tag: "shareable",
42
+ })
43
+
44
+ /**
45
+ * Create an @inaccessible directive
46
+ * Omits the type/field from the public API while keeping it available for federation
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * objectType({
51
+ * name: "InternalMetadata",
52
+ * schema: MetadataSchema,
53
+ * directives: [inaccessible()],
54
+ * })
55
+ * ```
56
+ */
57
+ export const inaccessible = (): FederationDirective => ({
58
+ _tag: "inaccessible",
59
+ })
60
+
61
+ /**
62
+ * Create an @interfaceObject directive
63
+ * Indicates this object represents an interface from another subgraph
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * objectType({
68
+ * name: "Media",
69
+ * schema: MediaSchema,
70
+ * directives: [interfaceObject()],
71
+ * })
72
+ * ```
73
+ */
74
+ export const interfaceObject = (): FederationDirective => ({
75
+ _tag: "interfaceObject",
76
+ })
77
+
78
+ /**
79
+ * Create a @tag directive for metadata annotation
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * entity({
84
+ * name: "Product",
85
+ * schema: ProductSchema,
86
+ * keys: [key({ fields: "id" })],
87
+ * directives: [tag("public"), tag("catalog")],
88
+ * })
89
+ * ```
90
+ */
91
+ export const tag = (name: string): FederationDirective => ({
92
+ _tag: "tag",
93
+ name,
94
+ })
95
+
96
+ // ============================================================================
97
+ // Field-Level Directive Factories
98
+ // ============================================================================
99
+
100
+ /**
101
+ * Create an @external directive
102
+ * Marks a field as defined in another subgraph
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * field("User", "externalId", {
107
+ * type: S.String,
108
+ * directives: [external()],
109
+ * resolve: (parent) => parent.externalId,
110
+ * })
111
+ * ```
112
+ */
113
+ export const external = (): FederationDirective => ({
114
+ _tag: "external",
115
+ })
116
+
117
+ /**
118
+ * Create a @requires directive
119
+ * Specifies fields that must be fetched from other subgraphs before this field can be resolved
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * field("Product", "shippingEstimate", {
124
+ * type: S.Int,
125
+ * directives: [requires({ fields: "weight dimensions { height width }" })],
126
+ * resolve: (product) => calculateShipping(product.weight, product.dimensions),
127
+ * })
128
+ * ```
129
+ */
130
+ export const requires = (config: { fields: string }): FederationDirective => ({
131
+ _tag: "requires",
132
+ fields: config.fields,
133
+ })
134
+
135
+ /**
136
+ * Create a @provides directive
137
+ * Router optimization hint - indicates this field provides additional fields on the returned type
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * field("Review", "author", {
142
+ * type: UserSchema,
143
+ * directives: [provides({ fields: "name email" })],
144
+ * resolve: (review) => UserService.findById(review.authorId),
145
+ * })
146
+ * ```
147
+ */
148
+ export const provides = (config: { fields: string }): FederationDirective => ({
149
+ _tag: "provides",
150
+ fields: config.fields,
151
+ })
152
+
153
+ /**
154
+ * Create an @override directive
155
+ * Transfers resolution responsibility from another subgraph
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * field("Product", "price", {
160
+ * type: S.Number,
161
+ * directives: [override({ from: "legacy-pricing" })],
162
+ * resolve: (product) => PricingService.getPrice(product.id),
163
+ * })
164
+ * ```
165
+ */
166
+ export const override = (config: { from: string; label?: string }): FederationDirective => ({
167
+ _tag: "override",
168
+ from: config.from,
169
+ label: config.label,
170
+ })
@@ -0,0 +1,90 @@
1
+ import { Effect, Runtime } from "effect"
2
+ import { GraphQLObjectType, GraphQLUnionType, GraphQLString } from "@effect-gql/core"
3
+ import type { EntityRegistration, EntityRepresentation } from "./types"
4
+
5
+ /**
6
+ * Create the _Entity union type from registered entities
7
+ */
8
+ export function createEntityUnion(
9
+ entities: Map<string, EntityRegistration<any, any>>,
10
+ typeRegistry: Map<string, GraphQLObjectType>
11
+ ): GraphQLUnionType {
12
+ const types = Array.from(entities.keys())
13
+ .map((name) => typeRegistry.get(name)!)
14
+ .filter(Boolean)
15
+
16
+ if (types.length === 0) {
17
+ throw new Error("At least one entity must be registered to create _Entity union")
18
+ }
19
+
20
+ return new GraphQLUnionType({
21
+ name: "_Entity",
22
+ description: "Union of all types that have @key directives",
23
+ types: () => types,
24
+ resolveType: (value: any) => value.__typename,
25
+ })
26
+ }
27
+
28
+ /**
29
+ * Create the _entities resolver
30
+ *
31
+ * This resolver receives an array of representations (objects with __typename and key fields)
32
+ * and returns the corresponding entities by calling each entity's resolveReference function.
33
+ *
34
+ * Uses Effect.all with unbounded concurrency to resolve all entities in parallel.
35
+ */
36
+ export function createEntitiesResolver<R>(entities: Map<string, EntityRegistration<any, R>>) {
37
+ return async (
38
+ _parent: any,
39
+ args: { representations: readonly EntityRepresentation[] },
40
+ context: { runtime: Runtime.Runtime<R> }
41
+ ): Promise<(any | null)[]> => {
42
+ const effects = args.representations.map((representation) => {
43
+ const entityName = representation.__typename
44
+ const entity = entities.get(entityName)
45
+
46
+ if (!entity) {
47
+ return Effect.fail(new Error(`Unknown entity type: ${entityName}`))
48
+ }
49
+
50
+ return entity.resolveReference(representation as any).pipe(
51
+ Effect.map((result) => {
52
+ // Add __typename to the result for union type resolution
53
+ if (result !== null && typeof result === "object") {
54
+ return { ...result, __typename: entityName }
55
+ }
56
+ return result
57
+ }),
58
+ // Catch individual entity resolution errors and return null
59
+ Effect.catchAll((error) =>
60
+ Effect.logError(`Failed to resolve entity ${entityName}`, error).pipe(Effect.as(null))
61
+ )
62
+ )
63
+ })
64
+
65
+ return Runtime.runPromise(context.runtime)(Effect.all(effects, { concurrency: "unbounded" }))
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Create the _Service type for SDL introspection
71
+ */
72
+ export function createServiceType(): GraphQLObjectType {
73
+ return new GraphQLObjectType({
74
+ name: "_Service",
75
+ description: "Provides SDL for the subgraph schema",
76
+ fields: {
77
+ sdl: {
78
+ type: GraphQLString,
79
+ description: "The SDL representing the subgraph schema",
80
+ },
81
+ },
82
+ })
83
+ }
84
+
85
+ /**
86
+ * Create the _service resolver
87
+ */
88
+ export function createServiceResolver(sdl: string) {
89
+ return () => ({ sdl })
90
+ }