Skip to content

Commit f370929

Browse files
ysmolskiAenimusStarpTech
authored andcommitted
feat: expose costs (#2470)
Integrate static and dynamic cost calculation into the router. Expose the static (estimated) cost value in the modules. Expose both costs via telemetry and response headers. Metrics for costs should be enabled individually in the corresponding telemetry section. Composition was reviewed in another PR and merged here. End-to-end benchmark for a big query to measure the impact of cost control: │ SequentialBig │ SequentialBigCostAnalysis │ │ sec/op │ sec/op vs base │ *-14 584.1µ ± 0% 586.7µ ± 0% +0.45% (p=0.000 n=40) │ SequentialBig │ SequentialBigCostAnalysis │ │ B/s │ B/s vs base │ *-14 5.527Mi ± 0% 5.503Mi ± 0% -0.43% (p=0.000 n=40) │ SequentialBig │ SequentialBigCostAnalysis │ │ B/op │ B/op vs base │ *-14 456.1Ki ± 0% 456.1Ki ± 0% ~ (p=0.822 n=40) │ SequentialBig │ SequentialBigCostAnalysis │ │ allocs/op │ allocs/op vs base │ *-14 6.444k ± 0% 6.452k ± 0% +0.12% (p=0.000 n=40) Fixes ENG-8844 Fixes ENG-8986 Co-authored-by: Aenimus <47415099+Aenimus@users.noreply.github.com> Co-authored-by: StarpTech <deusdustin@gmail.com>
1 parent 2f059f2 commit f370929

File tree

81 files changed

+8978
-2452
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+8978
-2452
lines changed

cli/src/commands/router/commands/compose.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ function constructRouterSubgraph(result: FederationSuccess, s: SubgraphMetadata,
112112
const subgraphConfig = result.subgraphConfigBySubgraphName.get(s.name);
113113
const schema = subgraphConfig?.schema;
114114
const configurationDataByTypeName = subgraphConfig?.configurationDataByTypeName;
115+
const costs = subgraphConfig?.costs;
115116

116117
if (s.kind === SubgraphKind.Standard) {
117118
const composedSubgraph: ComposedSubgraph = {
@@ -125,6 +126,7 @@ function constructRouterSubgraph(result: FederationSuccess, s: SubgraphMetadata,
125126
websocketSubprotocol: s.websocketSubprotocol,
126127
schema,
127128
configurationDataByTypeName,
129+
costs,
128130
};
129131
return composedSubgraph;
130132
}
@@ -141,6 +143,7 @@ function constructRouterSubgraph(result: FederationSuccess, s: SubgraphMetadata,
141143
version: s.version,
142144
schema,
143145
configurationDataByTypeName,
146+
costs,
144147
};
145148
return composedSubgraphPlugin;
146149
}
@@ -155,6 +158,7 @@ function constructRouterSubgraph(result: FederationSuccess, s: SubgraphMetadata,
155158
mapping: s.mapping,
156159
schema,
157160
configurationDataByTypeName,
161+
costs,
158162
};
159163
return composedSubgraphGRPC;
160164
}
@@ -648,6 +652,7 @@ async function buildFeatureFlagsConfig(
648652
const subgraphConfig = featureResult.subgraphConfigBySubgraphName.get(s.name);
649653
const schema = subgraphConfig?.schema;
650654
const configurationDataByTypeName = subgraphConfig?.configurationDataByTypeName;
655+
const costs = subgraphConfig?.costs;
651656

652657
const composedSubgraph: ComposedSubgraph = {
653658
kind: SubgraphKind.Standard,
@@ -660,6 +665,7 @@ async function buildFeatureFlagsConfig(
660665
websocketSubprotocol: s.websocketSubprotocol,
661666
schema,
662667
configurationDataByTypeName,
668+
costs,
663669
};
664670
return composedSubgraph;
665671
}),

composition-go/index.global.js

Lines changed: 201 additions & 201 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

composition/ARCHITECTURE.md

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
# Composition Architecture and Onboarding (TypeScript)
2+
3+
This is a beginner-friendly guide to the `composition` package.
4+
5+
Scope:
6+
7+
- In scope: `composition/*`
8+
- Out of scope: `composition-go/*`
9+
10+
## Who this is for
11+
12+
Use this doc if you are:
13+
14+
- new to this repository,
15+
- trying to understand where composition errors come from,
16+
- adding or shipping custom directives.
17+
18+
## 1. What this package produces
19+
20+
Given one or more subgraph SDLs, composition produces:
21+
22+
- a federated router schema,
23+
- a federated client schema,
24+
- router configuration metadata,
25+
- warnings and errors.
26+
27+
Main API exports:
28+
29+
- `composition/src/index.ts`
30+
31+
Main functions:
32+
33+
- `federateSubgraphs(...)`
34+
- `federateSubgraphsWithContracts(...)`
35+
- `federateSubgraphsContract(...)`
36+
- `normalizeSubgraph(...)`
37+
- `batchNormalize(...)`
38+
39+
## 2. Mental model (simple)
40+
41+
Think of composition as a 4-stage pipeline:
42+
43+
1. Normalize each subgraph.
44+
2. Batch-check all normalized subgraphs together.
45+
3. Merge everything into one federated model.
46+
4. Validate resolvability and emit final schemas.
47+
48+
```mermaid
49+
flowchart LR
50+
A["Subgraph SDLs"] --> B["NormalizationFactory<br/>(per subgraph)"]
51+
B --> C["batchNormalize<br/>(cross-subgraph checks)"]
52+
C --> D["FederationFactory<br/>(merge + build schemas)"]
53+
D --> E["Graph.validate<br/>(resolvability)"]
54+
E --> F["FederationResult<br/>AST + schema + config + warnings/errors"]
55+
```
56+
57+
## 3. Where each stage lives
58+
59+
### Stage 1: Normalize one subgraph
60+
61+
Main files:
62+
63+
- `composition/src/v1/normalization/normalization-factory.ts`
64+
- `composition/src/v1/normalization/walkers.ts`
65+
- `composition/src/v1/normalization/utils.ts`
66+
67+
What happens here:
68+
69+
- Parse and validate SDL and directives.
70+
- Build internal type/field model (`ParentDefinitionData`, `FieldData`, etc.).
71+
- Validate federation directives (`@key`, `@requires`, `@provides`, `@override`, etc.).
72+
- Build type-level router configuration (`ConfigurationData`).
73+
- Record graph edges for later resolvability checks.
74+
75+
### Stage 2: Batch normalization
76+
77+
Main file:
78+
79+
- `composition/src/v1/normalization/normalization-factory.ts` (`batchNormalize`)
80+
81+
What happens here:
82+
83+
- Validate subgraph naming/uniqueness.
84+
- Normalize each subgraph into shared structures.
85+
- Merge cross-subgraph metadata (entities, auth, overrides, coordinates).
86+
- Produce `internalSubgraphBySubgraphName` used by federation.
87+
88+
### Stage 3: Federation merge
89+
90+
Main file:
91+
92+
- `composition/src/v1/federation/federation-factory.ts`
93+
94+
What happens here:
95+
96+
- Merge all normalized types and fields.
97+
- Reconcile incompatible shapes and directive usage.
98+
- Build router/client schema definitions.
99+
- Build final field configuration output.
100+
101+
### Stage 4: Resolvability validation
102+
103+
Main files:
104+
105+
- `composition/src/resolvability-graph/graph.ts`
106+
- `composition/src/resolvability-graph/graph-nodes.ts`
107+
108+
What happens here:
109+
110+
- Validate that required field paths are actually reachable.
111+
- Return errors if fields cannot be resolved across subgraphs.
112+
113+
## 4. Request flow from public API
114+
115+
Top-level function:
116+
117+
- `composition/src/federation/federation.ts`
118+
119+
Current compatibility:
120+
121+
- only version `"1"` is supported (`composition/src/router-compatibility-version/router-compatibility-version.ts`).
122+
123+
Effective flow:
124+
125+
1. `federateSubgraphs(...)`
126+
2. `initializeFederationFactory(...)`
127+
3. `batchNormalize(...)`
128+
4. `FederationFactory.federateSubgraphData()`
129+
5. `FederationFactory.buildFederationResult()`
130+
131+
## 5. Core data structures (quick glossary)
132+
133+
Result types:
134+
135+
- `composition/src/federation/types.ts`
136+
- `composition/src/normalization/types.ts`
137+
- `composition/src/subgraph/types.ts`
138+
139+
Most important internals:
140+
141+
- `InternalSubgraph`: normalized per-subgraph state fed into federation.
142+
- `ParentDefinitionData`: canonical representation of a type in normalization/federation.
143+
- `PersistedDirectiveDefinitionData`: executable directive definition info considered for federated schema persistence.
144+
- `ConfigurationData`: per-type router configuration source.
145+
146+
## 6. Directives: how the system is designed
147+
148+
There are two broad categories:
149+
150+
1. Built-in directives with composition semantics.
151+
2. Custom executable directives that can be validated and persisted.
152+
153+
### Built-in directives
154+
155+
Definition and registration:
156+
157+
- `composition/src/v1/constants/directive-definitions.ts`
158+
- `composition/src/v1/constants/constants.ts`
159+
- `composition/src/v1/constants/strings.ts`
160+
- `composition/src/utils/string-constants.ts`
161+
162+
Normalization uses a directive registry (`directiveDefinitionDataByName`) to validate use sites and arguments.
163+
164+
### Custom directives
165+
166+
During normalization, unknown directive definitions are treated as custom:
167+
168+
- parsed and validated,
169+
- added to normalized directive maps,
170+
- included in normalized subgraph AST.
171+
172+
Key implementation points:
173+
174+
- `upsertDirectiveSchemaAndEntityDefinitions(...)`
175+
- `addDirectiveDefinitionDataByNode(...)`
176+
177+
Both are in:
178+
179+
- `composition/src/v1/normalization/normalization-factory.ts`
180+
- `composition/src/v1/normalization/walkers.ts`
181+
182+
### Directive persistence across subgraphs
183+
184+
During federation, executable directive definitions are only persisted when compatible across subgraphs.
185+
186+
Compatibility rules (important):
187+
188+
- The definition must exist consistently across participating subgraphs.
189+
- Executable locations are intersected (must remain non-empty).
190+
- Argument definitions must merge cleanly.
191+
- Repeatability must remain compatible.
192+
193+
Core methods:
194+
195+
- `upsertPersistedDirectiveDefinitionData(...)`
196+
- `addValidPersistedDirectiveDefinitionNodeByData(...)`
197+
198+
Files:
199+
200+
- `composition/src/v1/federation/federation-factory.ts`
201+
- `composition/src/schema-building/utils.ts`
202+
203+
### Caveat: `@composeDirective`
204+
205+
`@composeDirective` is declared but currently unimplemented in behavior.
206+
207+
Reference:
208+
209+
- `composition/src/v1/constants/directive-definitions.ts`
210+
211+
Treat it as not available for feature design right now.
212+
213+
## 7. Shipping a custom directive (practical playbook)
214+
215+
If you want a custom executable directive to survive composition:
216+
217+
1. Define the directive in every relevant subgraph SDL.
218+
2. Keep argument names/types/defaults compatible.
219+
3. Keep executable locations compatible.
220+
4. Use it where needed.
221+
5. Compose and verify in the federated output.
222+
223+
Validation checklist:
224+
225+
- Definition exists in federated schema.
226+
- Usages appear on expected nodes.
227+
- No invalid repeatability errors.
228+
- No cross-subgraph definition merge errors.
229+
230+
Best tests to copy:
231+
232+
- `composition/tests/v1/directives/directives.test.ts`
233+
- `composition/tests/v1/normalization.test.ts`
234+
235+
## 8. Adding a new built-in directive (when persistence is not enough)
236+
237+
Use this path if you need new behavior (not just persistence), for example router config generation.
238+
239+
Implementation sequence:
240+
241+
1. Add directive name constants.
242+
2. Add canonical directive definition node.
243+
3. Add directive definition data (arg/location validation model).
244+
4. Register in directive maps.
245+
5. Add validation/extraction behavior in normalization.
246+
6. Add federation persistence/merge behavior.
247+
7. Add dependencies for helper scalars/inputs if needed.
248+
8. Add tests (invalid, valid, merge, config impact).
249+
250+
Files usually touched:
251+
252+
- `composition/src/utils/string-constants.ts`
253+
- `composition/src/v1/constants/directive-definitions.ts`
254+
- `composition/src/v1/normalization/directive-definition-data.ts`
255+
- `composition/src/v1/constants/constants.ts`
256+
- `composition/src/v1/constants/strings.ts`
257+
- `composition/src/v1/normalization/normalization-factory.ts`
258+
- `composition/src/v1/federation/federation-factory.ts`
259+
- `composition/tests/v1/directives/*.test.ts`
260+
261+
## 9. Contracts in one minute
262+
263+
`federateSubgraphsWithContracts(...)` flow:
264+
265+
1. Build the base federated graph.
266+
2. Clone federation state per contract.
267+
3. Apply tag include/exclude options per contract.
268+
4. Return one result per contract.
269+
270+
If base federation fails, contracts are not attempted.
271+
272+
Main implementation:
273+
274+
- `composition/src/v1/federation/federation-factory.ts`
275+
276+
## 10. Debugging workflow for beginners
277+
278+
When composition fails:
279+
280+
1. Start at API entry:
281+
282+
- `composition/src/federation/federation.ts`
283+
284+
2. Reproduce with minimal SDL in tests:
285+
286+
- `composition/tests/v1/directives/*`
287+
288+
3. Inspect normalization output first:
289+
290+
- `directiveDefinitionByName`
291+
- `parentDefinitionDataByTypeName`
292+
- `configurationDataByTypeName`
293+
294+
4. Inspect federation state second:
295+
296+
- `parentDefinitionDataByTypeName`
297+
- `potentialPersistedDirectiveDefinitionDataByDirectiveName`
298+
- `referencedPersistedDirectiveNames`
299+
- `errors` / `warnings`
300+
301+
5. If merge looks fine but final result fails, inspect resolvability:
302+
303+
- `Graph.validate()` path in `composition/src/resolvability-graph/graph.ts`
304+
305+
## 11. Recommended reading order (first week)
306+
307+
1. `composition/src/federation/federation.ts`
308+
2. `composition/src/v1/federation/federation-factory.ts` (start from exported functions at the bottom, then move upward)
309+
3. `composition/src/v1/normalization/normalization-factory.ts`
310+
4. `composition/src/v1/normalization/walkers.ts`
311+
5. `composition/src/resolvability-graph/graph.ts`
312+
6. `composition/tests/v1/directives/*`
313+
314+
## 12. Quick FAQ
315+
316+
### Why do I get normalization errors before federation errors?
317+
318+
Because federation only runs after batch normalization succeeds. Normalization is the gate.
319+
320+
### Why did my custom directive disappear from federated output?
321+
322+
Usually because the definition is incompatible across subgraphs (locations, args, repeatability, or presence).
323+
324+
### Can I rely on `@composeDirective` behavior?
325+
326+
Not yet. It is declared, but composition behavior for it is not implemented.

composition/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
The WunderGraph composition library facilitates the federation of multiple subgraph schemas into a
66
single federated GraphQL schema.
77

8+
## Architecture and onboarding
9+
10+
For an implementation-level walkthrough of the composition pipeline and extension points (including shipping custom directives), see:
11+
12+
- [ARCHITECTURE.md](./ARCHITECTURE.md)
13+
814
### Prerequisites
915

1016
- [Node.js 16 LTS or higher](https://nodejs.dev/en/about/releases/)

0 commit comments

Comments
 (0)