Skip to content

feat(router): support costs on arguments of directives#2781

Merged
ysmolski merged 27 commits intomainfrom
yury/eng-8700-router-support-cost-on-the-arguments-of-directives
Apr 29, 2026
Merged

feat(router): support costs on arguments of directives#2781
ysmolski merged 27 commits intomainfrom
yury/eng-8700-router-support-cost-on-the-arguments-of-directives

Conversation

@ysmolski
Copy link
Copy Markdown
Contributor

@ysmolski ysmolski commented Apr 21, 2026

This PR supports cost weights on the arguments of the directives. When a customer applies such a directive with the specific non-null argument to a field, then its cost will be added to the cost of the field.

Composition now tracks weights for arguments of directives for each field. This is a static data so it goes into the config of a schema. It is needed for the engine to understand where directives' arguments with costs were used.
Router/engine uses it to take those weights into costs accounting.

If such directive was applied to the implementing type of some interface, and if the interface type is being used in the operation, then all the implementing types are taken into an account and maximum weight is picked among them.

I have added just a simple e2e test to verify that feature is working. More extended tests are implemented in the corresponding engine PR.

Example how cost from the directive could be added to the employees' field of the Engineer` type:

directive @expensiveOp(applied: Boolean = true @cost(weight: 22)) on FIELD_DEFINITION

type Engineer implements RoleType {
  departments: [Department!]!
  title: [String!]!
  employees: [Employee!]! @goField(forceResolver: true) @expensiveOp
  engineerType: EngineerType!
}

See documentation preview for more.

Summary by CodeRabbit

  • New Features

    • Directive-argument weights now contribute to field cost calculations and are propagated through configuration and serialization so costs reflect directive argument values.
  • Documentation

    • Added guidance, examples, and activation rules for directive-argument weights; clarified behavior for directives applied to interface-implementing types.
  • Tests

    • Added/updated tests for directive-argument weighting, tightened assertions to ensure no top-level errors, and adjusted expected cost results.
  • Demo

    • Added an example directive and applied it to sample fields to demonstrate directive-argument weighting.

@mintlify
Copy link
Copy Markdown
Contributor

mintlify Bot commented Apr 21, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
wundergraphinc 🟢 Ready View Preview Apr 21, 2026, 8:22 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

❌ Patch coverage is 97.59036% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.29%. Comparing base (403bd79) to head (ab94e78).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...tion/src/v1/normalization/normalization-factory.ts 98.43% 1 Missing ⚠️
router/gen/proto/wg/cosmo/node/v1/node.pb.go 80.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2781   +/-   ##
=======================================
  Coverage   46.28%   46.29%           
=======================================
  Files        1045     1045           
  Lines      139782   139820   +38     
  Branches     8768     8789   +21     
=======================================
+ Hits        64702    64733   +31     
- Misses      73328    73334    +6     
- Partials     1752     1753    +1     
Files with missing lines Coverage Δ
router/core/factoryresolver.go 80.42% <100.00%> (+0.15%) ⬆️
...tion/src/v1/normalization/normalization-factory.ts 90.08% <98.43%> (+0.05%) ⬆️
router/gen/proto/wg/cosmo/node/v1/node.pb.go 29.65% <80.00%> (+0.08%) ⬆️

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

Router-nonroot image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-4543c9697385fd8b884accb400540c45aee3306b-nonroot

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds end-to-end support for directive-argument weights: type and proto schemas extended, normalization records active directive-argument weights per field, loaders/builders propagate them, tests and demo schema updated, and docs amended.

Changes

Cohort / File(s) Summary
Types & Router Configuration
composition/src/router-configuration/types.ts
Added directiveArgumentWeights: Map<DirectiveArgumentCoords, number> to FieldWeightConfiguration.
Normalization Types
composition/src/v1/normalization/types.ts
Added exported RecordDirectiveWeightOnFieldParams type for recording directive weights on fields.
Normalization Implementation
composition/src/v1/normalization/normalization-factory.ts
Refactored cost handling (getOrCreateFieldWeight), added recordDirectiveWeightOnField() to populate fieldWeight.directiveArgumentWeights, adjusted validateDirective() flow to record directive-argument weights (handles null/defaults and skips interface parents where appropriate).
Tests (TS & Go)
composition/tests/v1/directives/cost.test.ts, router-tests/security/costs_test.go
Added fieldWeight(...) test helper; updated assertions to expect directiveArgumentWeights; added cases for directive-argument weights (including null/default behavior) and strengthened router cost assertions.
Proto & Generated Code
proto/wg/cosmo/node/v1/node.proto, connect/src/wg/cosmo/node/v1/node_pb.ts
Added map<string,int32> directive_argument_weights = 5; to proto FieldWeightConfiguration and updated generated TS to include directiveArgumentWeights and metadata.
Router Loader & Config Builder
router/core/factoryresolver.go, shared/src/router-config/builder.ts
Loader maps protobuf DirectiveArgumentWeights into runtime FieldCost (w.DirectiveArgumentWeights); builder converts per-field directiveArgumentWeights into runtime config via Object.fromEntries.
Demo Schema & Config
demo/pkg/subgraphs/employees/subgraph/schema.graphqls, demo/pkg/subgraphs/employees/gqlgen.yml
Added @expensiveOp(applied: Boolean = true @cost(weight: 22)), applied it to implementing types' employees fields, and registered expensiveOp in gqlgen.yml.
Docs
docs-website/router/security/cost-control.mdx
Documented "Weights on Directive Arguments", activation rules (non-null usage or non-null defaults), interface aggregation behavior, and reworded list-field multiplication guidance.
Go Module Updates
router/go.mod, router-tests/go.mod
Bumped github.com/wundergraph/graphql-go-tools/v2 pre-release version in both modules.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature addition: support for costs on arguments of directives, which is the primary objective of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
composition/tests/v1/directives/cost.test.ts (1)

25-33: Add an explicit return type to fieldWeight and strongly type overrides.

Line 25 defines a TS helper without an explicit return type; this weakens contract clarity for reused test builders.

🔧 Suggested typed helper
+interface FieldWeightExpectation {
+  typeName: TypeName;
+  fieldName: FieldName;
+  weight?: number;
+  argumentWeights: Map<string, number>;
+  directiveArgumentWeights: Map<string, number>;
+}
+
-function fieldWeight(typeName: TypeName, fieldName: FieldName, overrides: Record<string, unknown> = {}) {
+function fieldWeight(
+  typeName: TypeName,
+  fieldName: FieldName,
+  overrides: Partial<FieldWeightExpectation> = {},
+): FieldWeightExpectation {
   return {
     typeName,
     fieldName,
     argumentWeights: new Map(),
     directiveArgumentWeights: new Map(),
     ...overrides,
   };
 }
As per coding guidelines: `**/*.{ts,tsx}`: Use explicit type annotations for function parameters and return types in TypeScript.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composition/tests/v1/directives/cost.test.ts` around lines 25 - 33, Add an
explicit return type for the helper function fieldWeight and strongly type the
overrides parameter: declare or reuse a FieldWeight interface (with properties
typeName: TypeName, fieldName: FieldName, argumentWeights: Map<string, number>,
directiveArgumentWeights: Map<string, number>) and change the function signature
to return FieldWeight and accept overrides: Partial<FieldWeight> = {} so merged
properties are correctly typed; update any imports/usages if needed to reference
FieldWeight and ensure argumentWeights and directiveArgumentWeights are created
as Map<string, number>.
composition/src/v1/normalization/normalization-factory.ts (2)

2550-2591: Add explicit return type annotation.

The method is missing an explicit : void return type annotation. As per coding guidelines, TypeScript functions should have explicit type annotations for return types.

Suggested fix
 recordDirectiveWeightOnField({
   data,
   definitionData,
   directiveName,
   directiveNode,
-}: RecordDirectiveWeightOnFieldParams) {
+}: RecordDirectiveWeightOnFieldParams): void {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composition/src/v1/normalization/normalization-factory.ts` around lines 2550
- 2591, The method recordDirectiveWeightOnField is missing an explicit return
type; update its signature to declare a void return (e.g.,
recordDirectiveWeightOnField({...}: RecordDirectiveWeightOnFieldParams): void)
so TypeScript typing rules are satisfied and no implicit any/return types
remain—modify the method declaration where recordDirectiveWeightOnField is
defined (references: RecordDirectiveWeightOnFieldParams,
parentDefinitionDataByTypeName, getOrCreateFieldWeight).

2559-2561: Silent skip for interface types is intentional but undocumented.

The method silently returns when the parent type is an interface. This aligns with the design that interface field costs are derived from concrete implementations (as noted in costOnInterfaceFieldErrorMessage), but consider adding a brief comment explaining this design choice for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composition/src/v1/normalization/normalization-factory.ts` around lines 2559
- 2561, Add a brief inline comment above the early return in
normalization-factory.ts explaining that returning when parentTypeData is falsy
or when parentTypeData.kind === Kind.INTERFACE_TYPE_DEFINITION is intentional
because interface field costs are derived from their concrete implementations
(see costOnInterfaceFieldErrorMessage), so the function silently skips interface
types by design to avoid duplicate/ambiguous cost calculation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs-website/router/security/cost-control.mdx`:
- Around line 164-166: The sentence "When specified **on a directive argument
definition** — adds cost to every field where the directive is applied and the
argument is active." uses an em dash contrary to docs style; update the sentence
to avoid the em dash by splitting or restructuring (for example: "When specified
on a directive argument definition, the directive adds cost to every field where
the directive is applied and the argument is active." or two sentences) in
docs-website/router/security/cost-control.mdx near the "Weights on Directive
Arguments" reference so it conforms to the repo guideline banning em dashes in
MDX docs.

In `@router-tests/security/costs_test.go`:
- Around line 368-369: The inline comment showing the "Actual-cost formula" is
incorrect and doesn't match the asserted value in the test for
core.CostActualHeader; update the explanatory math above the assertion in
router-tests/security/costs_test.go so it reflects the real calculation that
yields "37" (or remove the misleading formula entirely). Locate the comment near
the require.Equal(t, "37", res.Response.Header.Get(core.CostActualHeader))
assertion and correct the numbers/operators to match the computed actual cost or
replace the comment with a concise note referencing core.CostActualHeader and
the expected total.

---

Nitpick comments:
In `@composition/src/v1/normalization/normalization-factory.ts`:
- Around line 2550-2591: The method recordDirectiveWeightOnField is missing an
explicit return type; update its signature to declare a void return (e.g.,
recordDirectiveWeightOnField({...}: RecordDirectiveWeightOnFieldParams): void)
so TypeScript typing rules are satisfied and no implicit any/return types
remain—modify the method declaration where recordDirectiveWeightOnField is
defined (references: RecordDirectiveWeightOnFieldParams,
parentDefinitionDataByTypeName, getOrCreateFieldWeight).
- Around line 2559-2561: Add a brief inline comment above the early return in
normalization-factory.ts explaining that returning when parentTypeData is falsy
or when parentTypeData.kind === Kind.INTERFACE_TYPE_DEFINITION is intentional
because interface field costs are derived from their concrete implementations
(see costOnInterfaceFieldErrorMessage), so the function silently skips interface
types by design to avoid duplicate/ambiguous cost calculation.

In `@composition/tests/v1/directives/cost.test.ts`:
- Around line 25-33: Add an explicit return type for the helper function
fieldWeight and strongly type the overrides parameter: declare or reuse a
FieldWeight interface (with properties typeName: TypeName, fieldName: FieldName,
argumentWeights: Map<string, number>, directiveArgumentWeights: Map<string,
number>) and change the function signature to return FieldWeight and accept
overrides: Partial<FieldWeight> = {} so merged properties are correctly typed;
update any imports/usages if needed to reference FieldWeight and ensure
argumentWeights and directiveArgumentWeights are created as Map<string, number>.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 076bd9c6-780f-4a65-b19a-5672b6ef81c6

📥 Commits

Reviewing files that changed from the base of the PR and between ced6759 and f08b68a.

⛔ Files ignored due to path filters (4)
  • connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • router-tests/go.sum is excluded by !**/*.sum
  • router/gen/proto/wg/cosmo/node/v1/node.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • router/go.sum is excluded by !**/*.sum
📒 Files selected for processing (14)
  • composition/src/router-configuration/types.ts
  • composition/src/v1/normalization/normalization-factory.ts
  • composition/src/v1/normalization/types.ts
  • composition/tests/v1/directives/cost.test.ts
  • connect/src/wg/cosmo/node/v1/node_pb.ts
  • demo/pkg/subgraphs/employees/subgraph/schema.graphqls
  • docs-website/router/security/cost-control.mdx
  • proto/wg/cosmo/node/v1/node.proto
  • router-tests/go.mod
  • router-tests/security/costs_test.go
  • router-tests/testenv/testdata/config.json
  • router/core/factoryresolver.go
  • router/go.mod
  • shared/src/router-config/builder.ts

Comment thread docs-website/router/security/cost-control.mdx Outdated
Comment thread router-tests/security/costs_test.go Outdated
Comment thread composition/src/v1/normalization/normalization-factory.ts Outdated
Comment thread composition/src/v1/normalization/normalization-factory.ts Outdated
@wundergraph wundergraph deleted a comment from Aenimus Apr 23, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
composition/src/v1/normalization/normalization-factory.ts (1)

2553-2558: Add an explicit return type to the new helper.

Annotating this as : void keeps the change aligned with the repo’s TypeScript rules.

As per coding guidelines, "Use explicit type annotations for function parameters and return types in TypeScript".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composition/src/v1/normalization/normalization-factory.ts` around lines 2553
- 2558, The helper function recordDirectiveWeightOnField lacks an explicit
return type; update its signature to declare a return type of void (e.g., change
"recordDirectiveWeightOnField({ data, definitionData, directiveName,
directiveNode, }: RecordDirectiveWeightOnFieldParams)" to include ": void") so
it conforms to the project's TypeScript rules for explicit return types while
keeping the existing parameter typings (RecordDirectiveWeightOnFieldParams) and
body unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@composition/src/v1/normalization/normalization-factory.ts`:
- Around line 2559-2563: The early return that skips recording directive
argument weights when parentTypeData.kind === Kind.INTERFACE_TYPE_DEFINITION
hides `@cost` directives on interface fields; update normalization-factory.ts so
this branch either raises the same composition error as handleCostDirective when
an `@cost` is present on an interface field (surface a clear rejection) or instead
implement the interface->max-resolution handling path before returning so
directive-derived weights are applied to concrete implementations; locate the
check using typeName and parentDefinitionDataByTypeName and modify that
conditional to perform the chosen behavior (throw the same error as
handleCostDirective or resolve interface->concrete weighting) rather than
silently returning.

---

Nitpick comments:
In `@composition/src/v1/normalization/normalization-factory.ts`:
- Around line 2553-2558: The helper function recordDirectiveWeightOnField lacks
an explicit return type; update its signature to declare a return type of void
(e.g., change "recordDirectiveWeightOnField({ data, definitionData,
directiveName, directiveNode, }: RecordDirectiveWeightOnFieldParams)" to include
": void") so it conforms to the project's TypeScript rules for explicit return
types while keeping the existing parameter typings
(RecordDirectiveWeightOnFieldParams) and body unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77910375-7136-494c-9e7b-0524df9921a7

📥 Commits

Reviewing files that changed from the base of the PR and between 368e8b1 and f18390d.

📒 Files selected for processing (2)
  • composition-go/index.global.js
  • composition/src/v1/normalization/normalization-factory.ts

Comment thread composition/src/v1/normalization/normalization-factory.ts
@ysmolski ysmolski requested a review from Aenimus April 24, 2026 08:10
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
composition/tests/v1/directives/cost.test.ts (1)

541-556: Consider adding a test for omitted nullable argument without default.

The tests cover explicit null and arguments with defaults, but there's no test for an omitted nullable argument without a default value. Per the implementation, this should also not record a weight.

📝 Suggested additional test case
test('that a field with a directive whose nullable argument is omitted and has no default does not record directiveArgumentWeights', () => {
  const { costs } = normalizeSubgraphSuccess(
    {
      name: 'subgraph-omitted-nullable-arg',
      url: '',
      definitions: parse(`
        directive `@myDirective`(arg1: String `@cost`(weight: 5)) on FIELD_DEFINITION
        type Query {
          field: String! `@myDirective`
        }
      `),
    },
    ROUTER_COMPATIBILITY_VERSION_ONE,
  );
  expect(costs.fieldWeights.get('Query.field')).toBeUndefined();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composition/tests/v1/directives/cost.test.ts` around lines 541 - 556, Add a
new test that mirrors the existing null/default tests but verifies that omitting
a nullable directive argument (with no default) also does not record
directiveArgumentWeights: use normalizeSubgraphSuccess with a schema where
directive `@myDirective`(arg1: String `@cost`(weight: 5)) is applied to Query.field
without providing arg1, run with ROUTER_COMPATIBILITY_VERSION_ONE, and assert
costs.fieldWeights.get('Query.field') is undefined; reference the existing test
pattern and the helpers normalizeSubgraphSuccess and
ROUTER_COMPATIBILITY_VERSION_ONE to place and name the test similarly (e.g.,
'subgraph-omitted-nullable-arg').
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@composition/tests/v1/directives/cost.test.ts`:
- Around line 541-556: Add a new test that mirrors the existing null/default
tests but verifies that omitting a nullable directive argument (with no default)
also does not record directiveArgumentWeights: use normalizeSubgraphSuccess with
a schema where directive `@myDirective`(arg1: String `@cost`(weight: 5)) is applied
to Query.field without providing arg1, run with
ROUTER_COMPATIBILITY_VERSION_ONE, and assert
costs.fieldWeights.get('Query.field') is undefined; reference the existing test
pattern and the helpers normalizeSubgraphSuccess and
ROUTER_COMPATIBILITY_VERSION_ONE to place and name the test similarly (e.g.,
'subgraph-omitted-nullable-arg').

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fe6204af-11c7-41a9-8a67-cdd1055f9872

📥 Commits

Reviewing files that changed from the base of the PR and between 6cb40a0 and f9857fc.

📒 Files selected for processing (2)
  • composition/src/v1/normalization/normalization-factory.ts
  • composition/tests/v1/directives/cost.test.ts

Comment thread composition/src/v1/normalization/normalization-factory.ts
Comment thread composition/src/v1/normalization/normalization-factory.ts Outdated
Comment thread composition/tests/v1/directives/cost.test.ts Outdated
Copy link
Copy Markdown
Member

@Aenimus Aenimus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM from composition side.

Comment thread demo/pkg/subgraphs/employees/subgraph/schema.graphqls Outdated
Comment thread router/core/factoryresolver.go Outdated
@ysmolski ysmolski merged commit b70a55b into main Apr 29, 2026
56 checks passed
@ysmolski ysmolski deleted the yury/eng-8700-router-support-cost-on-the-arguments-of-directives branch April 29, 2026 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants