@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,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
|
+
})
|
package/src/entities.ts
ADDED
|
@@ -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
|
+
}
|