Skip to content

feat(composition): @openfed__queryCache and @openfed__is directives (5/5)#2986

Merged
SkArchon merged 12 commits into
milinda/requestScoped-directivefrom
milinda/queryCache-directive
Jun 23, 2026
Merged

feat(composition): @openfed__queryCache and @openfed__is directives (5/5)#2986
SkArchon merged 12 commits into
milinda/requestScoped-directivefrom
milinda/queryCache-directive

Conversation

@SkArchon

@SkArchon SkArchon commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

@openfed__queryCache + @openfed__is — query-field caching with argument-to-@key mappings for cache-key construction. Adds QueryCacheConfiguration/EntityKeyMapping/EntityCacheFieldMapping proto (field 5) + bindings, config types, normalization (@openfed__is mapping incl. batch), error messages, a warning, and the query-cache + serializer tests.

More documentation on this: https://app.notion.com/p/wundergraph/Directives-37bb19e0a0ec80b9b505d896beed5897

Summary by CodeRabbit

  • New Features

    • Added @openfed__queryCache directive for per-query entity caching with configurable TTL, header inclusion, and shadow mode settings
    • Added @openfed__is directive to map query arguments to entity key fields, supporting scalar, composite, nested, and batch mapping scenarios
  • Tests

    • Added comprehensive test coverage validating query cache directive configuration extraction and validation

@SkArchon SkArchon requested review from a team as code owners June 19, 2026 06:12
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a61a23b8-8796-44fc-bc10-31797d1caa33

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds two new OpenFED directives—@openfed__queryCache (on FIELD_DEFINITION) and @openfed__is (on ARGUMENT_DEFINITION)—for per-query entity caching. The change covers directive definition nodes and constants, TypeScript and proto data types, normalization-factory extraction and validation logic, a router config builder conversion, and test coverage.

Changes

@openfed__queryCache and @openfed__is directive support

Layer / File(s) Summary
String constants, directive definition nodes, and registry wiring
composition/src/utils/string-constants.ts, composition/src/v1/constants/directive-definitions.ts, composition/src/directive-definition-data/directive-definition-data.ts, composition/src/v1/constants/constants.ts, composition/src/v1/normalization/utils.ts
Adds OPENFED_IS, OPENFED_QUERY_CACHE, and LITERAL_OPEN_BRACE constants; exports OPENFED_QUERY_CACHE_DEFINITION and OPENFED_IS_DEFINITION GraphQL directive definition nodes; exports QUERY_CACHE_DEFINITION_DATA and IS_DEFINITION_DATA; registers both in DIRECTIVE_DEFINITION_BY_NAME and initializeDirectiveDefinitionDatas().
Router config types and proto schema
composition/src/router-configuration/types.ts, composition/src/v1/normalization/types/types.ts, proto/wg/cosmo/node/v1/node.proto, connect/src/wg/cosmo/node/v1/node_pb.ts
Adds FieldMappingConfig, EntityKeyMappingConfig, QueryCacheConfig TypeScript types and extends EntityCachingConfiguration with queryCacheConfigurations; adds QueryCacheDirectiveNode/QueryCacheArgumentNode AST wrappers; extends EntityCaching proto message with query_cache_configurations and introduces QueryCacheConfiguration, EntityKeyMapping, and EntityCacheFieldMapping proto messages and generated TypeScript classes.
Error message helpers and warning infrastructure
composition/src/errors/errors.ts, composition/src/v1/warnings/params.ts, composition/src/v1/warnings/warnings.ts
Adds 23 exported error-message functions for @openfed__queryCache placement/return-type validation and @openfed__is mapping validation (type mismatches, list/scalar incompatibilities, composite key completeness, batch restrictions, nested input-object errors); adds QueryCacheReturnEntityMissingEntityCacheWarningParams type and queryCacheReturnEntityMissingEntityCacheWarning function.
NormalizationFactory imports and dispatch
composition/src/v1/normalization/normalization-factory.ts (imports, processRootFieldCacheDirectives)
Extends factory imports for new error/warning/type/constant symbols; introduces hasQueryCache boolean; dispatches to extractQueryCacheConfig() when hasQueryCache is true; runs validateIsDirectivePlacement() afterward; updates JSDoc on affected members.
NormalizationFactory extraction and mapping methods
composition/src/v1/normalization/normalization-factory.ts (new methods)
Adds extractQueryCacheConfig(), validateIsDirectivePlacement(), extractKeyFieldInfos(), unwrapListType(), namedTypesMatch(), typesMatchIncludingListShape(), getIsFieldValue(), validateNestedInputObjectMapping(), buildArgumentKeyMappings(), buildExplicitMappings(), and buildCompositeIsMapping() covering the full argument-to-key-field mapping extraction, structural type validation, and error reporting pipeline.
Router config builder: QueryCacheConfiguration proto conversion
shared/src/router-config/builder.ts
Extends toEntityCaching() to convert queryCacheConfigurations into nested QueryCacheConfiguration / EntityKeyMapping / EntityCacheFieldMapping proto messages; updates the empty-config guard and includes the built array in the returned EntityCaching message.
Tests
composition/tests/v1/directives/query-cache.test.ts, shared/test/entity-caching.test.ts
Adds 1138-line composition test suite covering config extraction and all validation-failure scenarios for both directives; adds 166-line integration test suite validating end-to-end proto builder output for all caching directives including batch isBatch flag and the absent-EntityCaching case.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% 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 clearly and specifically identifies the main feature being introduced: the @openfed__queryCache and @openfed__is directives, with the part numbering indicating this is the implementation phase of a multi-part series.
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.

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

Router-nonroot image scan failed

❌ Security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-e96a8018b0a11b3573cb3a77b4162ccacff3ae9c-nonroot

Please check the security vulnerabilities found in the PR.

If you believe this is a false positive, please add the vulnerability to the .trivyignore file and re-run the scan.

@SkArchon SkArchon force-pushed the milinda/requestScoped-directive branch from 2859cba to faca30a Compare June 19, 2026 06:24
@SkArchon SkArchon force-pushed the milinda/queryCache-directive branch from 37a198e to 4c90da9 Compare June 19, 2026 06:24
@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.70130% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.24%. Comparing base (840f24d) to head (fc06034).

Files with missing lines Patch % Lines
...tion/src/v1/normalization/normalization-factory.ts 98.15% 14 Missing ⚠️
Additional details and impacted files
@@                         Coverage Diff                         @@
##           milinda/requestScoped-directive    #2986      +/-   ##
===================================================================
+ Coverage                            47.04%   47.24%   +0.20%     
===================================================================
  Files                                 1119     1119              
  Lines                               152204   153380    +1176     
  Branches                             10007    10193     +186     
===================================================================
+ Hits                                 71602    72470     +868     
- Misses                               78798    79082     +284     
- Partials                              1804     1828      +24     
Files with missing lines Coverage Δ
...ctive-definition-data/directive-definition-data.ts 100.00% <100.00%> (ø)
composition/src/errors/errors.ts 84.35% <100.00%> (+1.89%) ⬆️
composition/src/utils/string-constants.ts 100.00% <100.00%> (ø)
composition/src/v1/constants/constants.ts 100.00% <100.00%> (ø)
...position/src/v1/constants/directive-definitions.ts 100.00% <100.00%> (ø)
composition/src/v1/normalization/utils.ts 90.10% <100.00%> (+0.03%) ⬆️
composition/src/v1/warnings/params.ts 100.00% <ø> (ø)
composition/src/v1/warnings/warnings.ts 90.09% <100.00%> (+0.71%) ⬆️
router/gen/proto/wg/cosmo/node/v1/node.pb.go 26.20% <ø> (-1.50%) ⬇️
...tion/src/v1/normalization/normalization-factory.ts 91.74% <98.15%> (+1.26%) ⬆️

... and 21 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.

@SkArchon

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
composition/src/errors/errors.ts (1)

2066-2328: ⚠️ Potential issue | 🟡 Minor

Replace string concatenation with template literals in new error message helpers.

Found 17 instances of string concatenation using the + operator across multiple error message functions. Per the coding guidelines for **/*.{ts,tsx} files: "Use template literals instead of string concatenation." These should be consolidated into single template literals for consistency and readability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composition/src/errors/errors.ts` around lines 2066 - 2328, Replace string
concatenation using the `+` operator with template literals in all the error
message helper functions throughout this file. Identify functions like
queryCacheOnNonQueryFieldErrorMessage,
queryCacheOnNonEntityReturnTypeErrorMessage,
cacheInvalidateAndPopulateMutualExclusionErrorMessage,
isReferencesUnknownKeyFieldErrorMessage,
listArgumentToScalarKeySpecErrorMessage,
scalarArgumentToListKeySpecErrorMessage,
multipleListArgumentsBatchFactoryMessage,
inputObjectCompositeTypeMismatchErrorMessage,
inputObjectCompositeMissingFieldErrorMessage,
nestedKeyRequiresInputObjectErrorMessage,
nestedInputObjectTypeMismatchErrorMessage,
nestedInputObjectMissingFieldErrorMessage, and
nonInputArgumentCannotTargetCompositeKeyErrorMessage that currently use the `+`
operator to join string parts, and convert them to use backticks with embedded
expressions instead, consolidating each function's return statement into a
single template literal.

Source: Coding guidelines

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

1-25: ⚠️ Potential issue | 🟡 Minor

Tighten the new helper types: add explicit return annotations and replace any[] with SelectionNode.

Three helper methods lack explicit void return type annotations (processRootFieldCacheDirectives, extractQueryCacheConfig, validateIsDirectivePlacement), and walkSelections at line 4417 uses readonly any[] instead of a specific type. Import SelectionNode from graphql and add the missing return type annotations.

Per coding guidelines: "Use explicit type annotations for function parameters and return types in TypeScript" and "Avoid any type in TypeScript; use specific types or generics".

Proposed typing cleanup
 import {
   BREAK,
   type ConstDirectiveNode,
   type ConstValueNode,
   type DefinitionNode,
   type DirectiveDefinitionNode,
   type DocumentNode,
   type EnumValueDefinitionNode,
   type FieldDefinitionNode,
   type InputValueDefinitionNode,
   type InterfaceTypeDefinitionNode,
   type IntValueNode,
   Kind,
   type ListValueNode,
   type NamedTypeNode,
   type OperationTypeDefinitionNode,
   OperationTypeNode,
   print,
+  type SelectionNode,
   type StringValueNode,
   type TypeDefinitionNode,
   type TypeExtensionNode,
   type TypeNode,
   type ValueNode,
   visit,
 } from 'graphql';
-  processRootFieldCacheDirectives() {
+  processRootFieldCacheDirectives(): void {
   extractQueryCacheConfig(
     parentTypeName: string,
     configurationTypeName: string,
     fieldName: string,
     fieldData: FieldData,
     operationType: OperationTypeNode | undefined,
-  ) {
+  ): void {
-  validateIsDirectivePlacement(fieldCoords: string, fieldData: FieldData, hasQueryCache: boolean) {
+  validateIsDirectivePlacement(fieldCoords: string, fieldData: FieldData, hasQueryCache: boolean): void {
-    const walkSelections = (selections: readonly any[], currentTypeName: string, pathPrefix: string) => {
+    const walkSelections = (selections: readonly SelectionNode[], currentTypeName: string, pathPrefix: string): void => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composition/src/v1/normalization/normalization-factory.ts` around lines 1 -
25, Add SelectionNode to the graphql imports at the top of the file. Then locate
the three helper methods processRootFieldCacheDirectives,
extractQueryCacheConfig, and validateIsDirectivePlacement and add explicit void
return type annotations to each one. Finally, find the walkSelections method
(around line 4417) and replace the readonly any[] type with readonly
SelectionNode[] to use the specific type instead of the generic any type.

Source: Coding guidelines

🧹 Nitpick comments (3)
composition/tests/v1/directives/query-cache.test.ts (2)

77-83: ⚡ Quick win

Guard array access before dereferencing extracted configs.

Several success-path tests access rootFieldConfigs![0] before asserting array length; if extraction regresses, this can throw and mask the real mismatch. Add existence/length assertions first, then index.

Suggested pattern
 const rootFieldConfigs = result.configurationDataByTypeName.get('Query')!.entityCaching?.queryCacheConfigurations;
-expect(rootFieldConfigs![0]).toMatchObject({
+expect(rootFieldConfigs).toBeDefined();
+expect(rootFieldConfigs).toHaveLength(1);
+expect(rootFieldConfigs![0]).toMatchObject({
   fieldName: 'user',
   maxAgeSeconds: 120,
   includeHeaders: true,
   shadowMode: true,
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composition/tests/v1/directives/query-cache.test.ts` around lines 77 - 83,
The test in query-cache.test.ts accesses rootFieldConfigs![0] without first
verifying that rootFieldConfigs exists and contains at least one element. If the
extraction regresses, this will throw an error rather than providing a clear
test failure message. Add explicit assertions to check that rootFieldConfigs
exists and has a length of at least 1 before attempting to access the first
element with toMatchObject, ensuring failures are clear and traceable rather
than masked by runtime errors.

371-1134: ⚡ Quick win

Assert single-error cardinality in failure cases.

Most validation tests only compare errors[0]; extra unexpected errors would still pass. Add expect(errors).toHaveLength(1) before checking the exact error payload.

Suggested pattern
 const { errors } = normalizeSubgraphFailure(...);
+expect(errors).toHaveLength(1);
 expect(errors[0]).toStrictEqual(
   invalidDirectiveError(...)
 );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composition/tests/v1/directives/query-cache.test.ts` around lines 371 - 1134,
Add single-error cardinality assertions to all validation test cases in the
file. For each test that validates a failure scenario (where errors[0] is
checked), insert `expect(errors).toHaveLength(1)` immediately before the
existing `expect(errors[0]).toStrictEqual(...)` assertions. This applies to all
test cases within the `@openfed__queryCache` and `@openfed__is` describe blocks,
including those testing directive validation, batch mappings, and input-object
composite mappings. This ensures that exactly one error is produced rather than
silently allowing multiple unexpected errors to pass the test.
composition/src/v1/normalization/normalization-factory.ts (1)

4692-4698: Use an interface for ArgumentInfo.

This is an object shape and should follow the repository's TypeScript interface preference per coding guidelines.

Proposed change
-    type ArgumentInfo = {
+    interface ArgumentInfo {
       name: string;
       data: InputValueData;
       isFieldValue: string | undefined; // `@openfed__is`(fields: "...") value
       isList: boolean;
       typeNode: TypeNode;
-    };
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composition/src/v1/normalization/normalization-factory.ts` around lines 4692
- 4698, Convert the ArgumentInfo type definition from a type alias to an
interface to align with the repository's TypeScript coding guidelines. Replace
the type ArgumentInfo declaration syntax with an interface ArgumentInfo
declaration, keeping all the same properties (name, data, isFieldValue, isList,
typeNode) and their type annotations intact.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@composition/src/router-configuration/types.ts`:
- Around line 137-161: Convert the three object-shape type declarations
(FieldMappingConfig, EntityKeyMappingConfig, and QueryCacheConfig) from type
aliases to interfaces for consistency with the coding style guide. Replace the
`type Name = { ... }` syntax with `interface Name { ... }` syntax for each of
these three declarations, removing the equals sign and preserving all property
definitions within the interface body.

In `@composition/src/v1/normalization/normalization-factory.ts`:
- Around line 4967-4969: The early return when explicitMappings.length === 0
skips validation checks that should also apply to composite-only mappings. Move
the unused-argument validation logic so it runs before the early return
statement that returns compositeMappings, ensuring these checks apply regardless
of whether explicit mappings exist. Additionally, in the buildCompositeIsMapping
function, add a validation check to reject cases where isListReturn is true but
argInfo.isList is false, and perform this validation before emitting any field
mappings to prevent invalid batch mapping configurations.
- Around line 4973-4974: The filter in the extraArgs assignment at
argumentInfos.filter is incorrectly exempting arguments based solely on their
name matching entries in allKeyFieldPaths, without verifying they have proper
annotation. An argument named like a key field but lacking the `@openfed__is`
annotation should still be treated as an extra argument and included in the
filter result, as mappings are explicit-only. Modify the filter condition to not
just check !allKeyFieldPaths.has(a.name), but instead verify that an argument is
only exempted if it is actually properly annotated/mapped as a key field,
otherwise include it in extraArgs to prevent cache collisions.
- Around line 4866-4867: The `namedTypesMatch` function ignores list wrappers,
allowing types with remaining list ranks (like `[ID!]!`) to incorrectly match
scalar key types (like `ID!`) after a single `unwrapListType` call. In the
validation at the `namedTypesMatch` check, after calling
`this.unwrapListType(unwrapped)` to get `innerType`, verify that `innerType`
contains no remaining list wrappers before comparing with `keyFieldTypeNode`, or
explicitly check that the full type structure (including all list ranks)
matches, not just the named type component. This applies to all instances
mentioned in the additional locations as well.
- Around line 5090-5110: The error handling in the normalization factory is
incorrectly reporting incomplete keys even when alternative keys are satisfied.
Instead of immediately pushing an error when a key has explicit mappings but
incomplete fields (the condition checking explicitForThisKey.length > 0 &&
unmappedFields.length > 0), you should accumulate these incomplete key
candidates and only emit the error if no key produced a valid mapping at all.
Move the error reporting logic to occur after evaluating all keys, checking if
the keyFullySatisfied condition was never true across any of the keys being
processed, rather than erroring immediately upon encountering the first
incomplete key.

In `@composition/src/v1/normalization/types/types.ts`:
- Around line 172-186: The type declarations QueryCacheDirectiveNode and
QueryCacheArgumentNode are declared using `type` aliases but should be declared
using `interface` instead, as per the repository's TypeScript conventions for
object-shaped types. Convert both `export type QueryCacheDirectiveNode` and
`export type QueryCacheArgumentNode` declarations to use the `interface`
keyword, keeping all the property definitions exactly as they are, including the
union type in the value property of QueryCacheArgumentNode.

In `@composition/src/v1/warnings/params.ts`:
- Around line 26-30: Replace the `export type` keyword with `export interface`
for all five warning params definitions in this file:
SingleSubgraphInputFieldOneOfWarningParams,
SingleFederatedInputFieldOneOfWarningParams,
InvalidRepeatedComposedDirectiveWarningParams,
RequestScopedSingleFieldWarningParams, and
QueryCacheReturnEntityMissingEntityCacheWarningParams. This aligns with
TypeScript coding guidelines that prefer interfaces over type aliases for object
shapes.

In `@composition/src/v1/warnings/warnings.ts`:
- Around line 264-268: The message property in the warnings section uses string
concatenation with + operators across multiple lines instead of a single
template literal. Refactor this to use a single template literal that spans the
entire message, removing all the + operators and combining all the text portions
(from the fieldCoords field through the final sentence about config extraction)
into one continuous template literal block.

---

Outside diff comments:
In `@composition/src/errors/errors.ts`:
- Around line 2066-2328: Replace string concatenation using the `+` operator
with template literals in all the error message helper functions throughout this
file. Identify functions like queryCacheOnNonQueryFieldErrorMessage,
queryCacheOnNonEntityReturnTypeErrorMessage,
cacheInvalidateAndPopulateMutualExclusionErrorMessage,
isReferencesUnknownKeyFieldErrorMessage,
listArgumentToScalarKeySpecErrorMessage,
scalarArgumentToListKeySpecErrorMessage,
multipleListArgumentsBatchFactoryMessage,
inputObjectCompositeTypeMismatchErrorMessage,
inputObjectCompositeMissingFieldErrorMessage,
nestedKeyRequiresInputObjectErrorMessage,
nestedInputObjectTypeMismatchErrorMessage,
nestedInputObjectMissingFieldErrorMessage, and
nonInputArgumentCannotTargetCompositeKeyErrorMessage that currently use the `+`
operator to join string parts, and convert them to use backticks with embedded
expressions instead, consolidating each function's return statement into a
single template literal.

In `@composition/src/v1/normalization/normalization-factory.ts`:
- Around line 1-25: Add SelectionNode to the graphql imports at the top of the
file. Then locate the three helper methods processRootFieldCacheDirectives,
extractQueryCacheConfig, and validateIsDirectivePlacement and add explicit void
return type annotations to each one. Finally, find the walkSelections method
(around line 4417) and replace the readonly any[] type with readonly
SelectionNode[] to use the specific type instead of the generic any type.

---

Nitpick comments:
In `@composition/src/v1/normalization/normalization-factory.ts`:
- Around line 4692-4698: Convert the ArgumentInfo type definition from a type
alias to an interface to align with the repository's TypeScript coding
guidelines. Replace the type ArgumentInfo declaration syntax with an interface
ArgumentInfo declaration, keeping all the same properties (name, data,
isFieldValue, isList, typeNode) and their type annotations intact.

In `@composition/tests/v1/directives/query-cache.test.ts`:
- Around line 77-83: The test in query-cache.test.ts accesses
rootFieldConfigs![0] without first verifying that rootFieldConfigs exists and
contains at least one element. If the extraction regresses, this will throw an
error rather than providing a clear test failure message. Add explicit
assertions to check that rootFieldConfigs exists and has a length of at least 1
before attempting to access the first element with toMatchObject, ensuring
failures are clear and traceable rather than masked by runtime errors.
- Around line 371-1134: Add single-error cardinality assertions to all
validation test cases in the file. For each test that validates a failure
scenario (where errors[0] is checked), insert `expect(errors).toHaveLength(1)`
immediately before the existing `expect(errors[0]).toStrictEqual(...)`
assertions. This applies to all test cases within the `@openfed__queryCache` and
`@openfed__is` describe blocks, including those testing directive validation,
batch mappings, and input-object composite mappings. This ensures that exactly
one error is produced rather than silently allowing multiple unexpected errors
to pass the test.
🪄 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: 7a69b121-add2-4b02-8892-ef42f11c2281

📥 Commits

Reviewing files that changed from the base of the PR and between 49f93f1 and ee1ea98.

⛔ Files ignored due to path filters (2)
  • connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • router/gen/proto/wg/cosmo/node/v1/node.pb.go is excluded by !**/*.pb.go, !**/gen/**
📒 Files selected for processing (16)
  • composition/src/directive-definition-data/directive-definition-data.ts
  • composition/src/errors/errors.ts
  • composition/src/router-configuration/types.ts
  • composition/src/utils/string-constants.ts
  • composition/src/v1/constants/constants.ts
  • composition/src/v1/constants/directive-definitions.ts
  • composition/src/v1/normalization/normalization-factory.ts
  • composition/src/v1/normalization/types/types.ts
  • composition/src/v1/normalization/utils.ts
  • composition/src/v1/warnings/params.ts
  • composition/src/v1/warnings/warnings.ts
  • composition/tests/v1/directives/query-cache.test.ts
  • connect/src/wg/cosmo/node/v1/node_pb.ts
  • proto/wg/cosmo/node/v1/node.proto
  • shared/src/router-config/builder.ts
  • shared/test/entity-caching.test.ts

Comment thread composition/src/router-configuration/types.ts
Comment on lines +4866 to +4867
const innerType = this.unwrapListType(unwrapped);
if (!this.namedTypesMatch(innerType, keyFieldTypeNode)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve list rank when validating mapped argument types.

namedTypesMatch ignores list wrappers, so values like [[ID!]!]! can pass for a scalar ID! key after one unwrap, and deeper list ranks can pass list-key checks. Compare the post-batch element shape against the key shape, or explicitly reject remaining list wrappers for scalar keys.

Example direction
           // List of lists - check inner type matches
-          const innerType = this.unwrapListType(unwrapped);
-          if (!this.namedTypesMatch(innerType, keyFieldTypeNode)) {
+          if (!this.typesMatchIncludingListShape(unwrapped, keyFieldTypeNode)) {
             // Check element type
             const unwrapped = this.unwrapListType(argTypeNode);
-            if (!this.namedTypesMatch(unwrapped, keyFieldTypeNode)) {
+            if (isTypeNodeListType(unwrapped) || !this.namedTypesMatch(unwrapped, keyFieldTypeNode)) {
-        if (!this.namedTypesMatch(argTypeNode, keyFieldTypeNode)) {
+        if (
+          argIsList && keyIsList
+            ? !this.typesMatchIncludingListShape(argTypeNode, keyFieldTypeNode)
+            : !this.namedTypesMatch(argTypeNode, keyFieldTypeNode)
+        ) {

Also applies to: 4884-4887, 4945-4946

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composition/src/v1/normalization/normalization-factory.ts` around lines 4866
- 4867, The `namedTypesMatch` function ignores list wrappers, allowing types
with remaining list ranks (like `[ID!]!`) to incorrectly match scalar key types
(like `ID!`) after a single `unwrapListType` call. In the validation at the
`namedTypesMatch` check, after calling `this.unwrapListType(unwrapped)` to get
`innerType`, verify that `innerType` contains no remaining list wrappers before
comparing with `keyFieldTypeNode`, or explicitly check that the full type
structure (including all list ranks) matches, not just the named type component.
This applies to all instances mentioned in the additional locations as well.

Comment on lines +4967 to +4969
if (explicitMappings.length === 0) {
return compositeMappings;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate composite-only mappings before returning them.

The early return compositeMappings skips the extra-argument and batch checks below. A composite-only field can therefore cache on input @openfed__is(...) while ignoring another argument, and a list-return field can accept a non-list composite input without establishing a batch mapping.

Move the unused-argument validation so it also runs when explicitMappings.length === 0, and reject isListReturn && !argInfo.isList in buildCompositeIsMapping before emitting field mappings.

Also applies to: 5185-5198

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composition/src/v1/normalization/normalization-factory.ts` around lines 4967
- 4969, The early return when explicitMappings.length === 0 skips validation
checks that should also apply to composite-only mappings. Move the
unused-argument validation logic so it runs before the early return statement
that returns compositeMappings, ensuring these checks apply regardless of
whether explicit mappings exist. Additionally, in the buildCompositeIsMapping
function, add a validation check to reject cases where isListReturn is true but
argInfo.isList is false, and perform this validation before emitting any field
mappings to prevent invalid batch mapping configurations.

Comment on lines +4973 to +4974
// Check for extra non-key arguments FIRST (not @openfed__is and not a key field in any key)
const extraArgs = argumentInfos.filter((a) => !a.isFieldValue && !allKeyFieldPaths.has(a.name));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not exempt unannotated key-named arguments.

Mappings are explicit-only, so an argument named like another @key field but without @openfed__is is still outside the cache key. The current filter can ignore such arguments and create cache collisions.

Proposed filter change
-      const extraArgs = argumentInfos.filter((a) => !a.isFieldValue && !allKeyFieldPaths.has(a.name));
+      const extraArgs = argumentInfos.filter((a) => !a.isFieldValue);
-      const globalExtraArgs = argumentInfos.filter((a) => !a.isFieldValue && !allKeyFieldPaths.has(a.name));
+      const globalExtraArgs = argumentInfos.filter((a) => !a.isFieldValue);

Also applies to: 5013-5014

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composition/src/v1/normalization/normalization-factory.ts` around lines 4973
- 4974, The filter in the extraArgs assignment at argumentInfos.filter is
incorrectly exempting arguments based solely on their name matching entries in
allKeyFieldPaths, without verifying they have proper annotation. An argument
named like a key field but lacking the `@openfed__is` annotation should still be
treated as an extra argument and included in the filter result, as mappings are
explicit-only. Modify the filter condition to not just check
!allKeyFieldPaths.has(a.name), but instead verify that an argument is only
exempted if it is actually properly annotated/mapped as a key field, otherwise
include it in extraArgs to prevent cache collisions.

Comment on lines +5090 to +5110
if (!keyFullySatisfied) {
// If this key has explicit mappings targeting it but is incomplete, error
if (explicitForThisKey.length > 0 && unmappedFields.length > 0) {
this.errors.push(
invalidDirectiveError(
OPENFED_IS,
`${fieldCoords}(${explicitForThisKey[0].argumentName}: ...)`,
FIRST_ORDINAL,
[
explicitIncompleteCompositeKeyErrorMessage(
fieldCoords,
explicitForThisKey[0].argumentName,
explicitForThisKey[0].isFieldValue,
entityTypeName,
normalizedFieldSet,
unmappedFields[0],
),
],
),
);
return [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only report incomplete keys when no alternative key is satisfied.

With overlapping keys such as @key(fields: "id") and @key(fields: "id sku"), a valid id @openfed__is(fields: "id") mapping satisfies the first key but this block still errors on the second key’s missing sku. Accumulate incomplete candidates and emit this error only if no key produced a mapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composition/src/v1/normalization/normalization-factory.ts` around lines 5090
- 5110, The error handling in the normalization factory is incorrectly reporting
incomplete keys even when alternative keys are satisfied. Instead of immediately
pushing an error when a key has explicit mappings but incomplete fields (the
condition checking explicitForThisKey.length > 0 && unmappedFields.length > 0),
you should accumulate these incomplete key candidates and only emit the error if
no key produced a valid mapping at all. Move the error reporting logic to occur
after evaluating all keys, checking if the keyFullySatisfied condition was never
true across any of the keys being processed, rather than erroring immediately
upon encountering the first incomplete key.

Comment on lines +172 to +186
export type QueryCacheDirectiveNode = {
readonly arguments: ReadonlyArray<QueryCacheArgumentNode>;
readonly kind: Kind.DIRECTIVE;
readonly name: NameNode;
readonly loc?: Location;
};

export type QueryCacheArgumentNode = {
readonly kind: Kind.ARGUMENT;
readonly name: NameNode;
// maxAge is Int; includeHeaders/shadowMode are Boolean.
// validateDirectives() guarantees each argument's value matches its declared type.
readonly value: IntValueNode | BooleanValueNode;
readonly loc?: Location;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's verify the file exists and check the exact content at lines 172-186
cat -n composition/src/v1/normalization/types/types.ts | sed -n '170,190p'

Repository: wundergraph/cosmo

Length of output: 869


🏁 Script executed:

# Check if this file has other type definitions to understand the pattern
grep -n "export type\|export interface" composition/src/v1/normalization/types/types.ts | head -20

Repository: wundergraph/cosmo

Length of output: 1067


🏁 Script executed:

# Check usage of these two types in the codebase
rg "QueryCacheDirectiveNode|QueryCacheArgumentNode" --type ts --type tsx -A 2 -B 2

Repository: wundergraph/cosmo

Length of output: 89


🏁 Script executed:

# Search for usage of QueryCacheDirectiveNode and QueryCacheArgumentNode
rg "QueryCacheDirectiveNode|QueryCacheArgumentNode" --type ts -A 2 -B 2

Repository: wundergraph/cosmo

Length of output: 1986


🏁 Script executed:

# Check the overall pattern in this file: count types vs interfaces
echo "=== Type aliases in file ===" && \
grep -c "^export type" composition/src/v1/normalization/types/types.ts && \
echo "=== Interfaces in file ===" && \
grep -c "^export interface" composition/src/v1/normalization/types/types.ts

Repository: wundergraph/cosmo

Length of output: 122


🏁 Script executed:

# Check if these types use union types or discriminated unions (which require `type`)
sed -n '172,186p' composition/src/v1/normalization/types/types.ts | grep -E "\\||&"

Repository: wundergraph/cosmo

Length of output: 112


🏁 Script executed:

# Check broader context: are there similar directive node types that use interface?
rg "DirectiveNode|ArgumentNode" composition/src/v1/normalization/types/types.ts -A 5 | head -40

Repository: wundergraph/cosmo

Length of output: 943


Consider using interface instead of type for these object shapes.

QueryCacheDirectiveNode and QueryCacheArgumentNode are object-shaped types and should use interface declarations per TypeScript conventions for this repository (**/*.{ts,tsx} files prefer interfaces over type aliases for object shapes). The union type in the value property of QueryCacheArgumentNode does not prevent using an interface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composition/src/v1/normalization/types/types.ts` around lines 172 - 186, The
type declarations QueryCacheDirectiveNode and QueryCacheArgumentNode are
declared using `type` aliases but should be declared using `interface` instead,
as per the repository's TypeScript conventions for object-shaped types. Convert
both `export type QueryCacheDirectiveNode` and `export type
QueryCacheArgumentNode` declarations to use the `interface` keyword, keeping all
the property definitions exactly as they are, including the union type in the
value property of QueryCacheArgumentNode.

Source: Coding guidelines

Comment thread composition/src/v1/warnings/params.ts
Comment on lines +264 to +268
message:
`Field "${fieldCoords}" has @openfed__queryCache and returns entity "${entityType}",` +
` but "${entityType}" has no @openfed__entityCache directive. Add @openfed__entityCache(maxAge: ...)` +
` to "${entityType}" to enable caching, or remove @openfed__queryCache from "${fieldCoords}".` +
' The @openfed__queryCache config for this field was not extracted.',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and get the context around lines 264-268
cd composition && head -300 src/v1/warnings/warnings.ts | tail -100

Repository: wundergraph/cosmo

Length of output: 3588


Use a single template literal for the warning message text.

The message property uses string concatenation with + operators across multiple lines; this should be expressed as a single template literal per project guidelines for **/*.{ts,tsx} files.

Current code (lines 264-268)
    message:
      `Field "${fieldCoords}" has `@openfed__queryCache` and returns entity "${entityType}",` +
      ` but "${entityType}" has no `@openfed__entityCache` directive. Add `@openfed__entityCache`(maxAge: ...)` +
      ` to "${entityType}" to enable caching, or remove `@openfed__queryCache` from "${fieldCoords}".` +
      ' The `@openfed__queryCache` config for this field was not extracted.',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@composition/src/v1/warnings/warnings.ts` around lines 264 - 268, The message
property in the warnings section uses string concatenation with + operators
across multiple lines instead of a single template literal. Refactor this to use
a single template literal that spans the entire message, removing all the +
operators and combining all the text portions (from the fieldCoords field
through the final sentence about config extraction) into one continuous template
literal block.

Source: Coding guidelines

SkArchon added 8 commits June 19, 2026 20:50
…e-directive

# Conflicts:
#	composition/src/v1/normalization/normalization-factory.ts
#	connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go
#	connect/src/wg/cosmo/node/v1/node_pb.ts
#	router/gen/proto/wg/cosmo/node/v1/node.pb.go
#	shared/src/router-config/builder.ts
…e-directive

Reconcile queryCache's cache-extraction (old dispatcher structure) with requestScoped's
refactored inline structure (from cachePopulate review fixes):
- Adopt requestScoped's inline 1-arg extractCacheInvalidateConfig/extractCachePopulateConfig
  (called during the parent walk); trim processRootFieldCacheDirectives() to only dispatch
  @openfed__queryCache extraction + @openfed__is placement validation, re-add its normalize() call
- extractQueryCacheConfig writes via getOrInitializeEntityCaching (required-field type)
- Restore queryCache's richer maxAgeNotPositiveIntegerErrorMessage({directiveName, value}) and
  update entityCache/cachePopulate call sites + tests to the object form
- Dedup imports (ENTITY_CACHE/OPENFED_ENTITY_CACHE/EntityCacheConfiguration/EntityCacheDirectiveNode)
- Regenerate node.pb.go (max_age_seconds non-optional); rename test helper to createSubgraphWithDefaultName
…e-directive

# Conflicts:
#	composition/src/router-configuration/types.ts
#	composition/src/v1/normalization/normalization-factory.ts
#	composition/src/v1/warnings/warnings.ts
#	connect-go/gen/proto/wg/cosmo/node/v1/node.pb.go
#	connect/src/wg/cosmo/node/v1/node_pb.ts
#	proto/wg/cosmo/node/v1/node.proto
#	router/gen/proto/wg/cosmo/node/v1/node.pb.go
#	shared/src/router-config/builder.ts
…s 4/5)

Reorders the stack so @openfed__queryCache/@openfed__is sits directly on
main as the 4th PR. The @openfed__requestScoped feature is removed here and
re-applied on top in the requestScoped branch (now 5/5). The requestScoped
assertions in shared/test/entity-caching.test.ts move to that PR.

Proto regenerated (request_scoped_configurations field 4 dropped; query_cache
remains field 5).
@SkArchon SkArchon merged commit e96a801 into milinda/requestScoped-directive Jun 23, 2026
@SkArchon SkArchon deleted the milinda/queryCache-directive branch June 23, 2026 06:58
@SkArchon SkArchon restored the milinda/queryCache-directive branch June 23, 2026 07:01
@SkArchon

Copy link
Copy Markdown
Contributor Author

⚠️ This PR was not actually merged into main. It was inadvertently marked as merged by GitHub during a stack reorder: the milinda/requestScoped-directive branch came to contain this PR's head commit while this PR's base still pointed at milinda/requestScoped-directive, so GitHub treated it as merged (and auto-deleted the head branch). No code from this PR reached main.

It has been replaced by #2997 (identical change, now based on main as 4/5). The reorder is complete: #2997 = queryCache/@is (4/5) → main, and #2985 = requestScoped (5/5) → milinda/queryCache-directive.

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.

2 participants