Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e4fb5ac
feat(router): support costs on arguments of directives
ysmolski Apr 10, 2026
fd01c8e
change graphqls
ysmolski Apr 10, 2026
81e9b79
add an e2e test
ysmolski Apr 21, 2026
873dda8
bump the eng
ysmolski Apr 21, 2026
3a3baff
update docs
ysmolski Apr 21, 2026
f8e406a
Merge branch 'main' of github.com:wundergraph/cosmo into yury/eng-870…
ysmolski Apr 21, 2026
f08b68a
update generate
ysmolski Apr 21, 2026
b1a3307
bump index global
ysmolski Apr 21, 2026
9a9f6ca
generate demo
ysmolski Apr 21, 2026
1c35f65
fix a comment
ysmolski Apr 21, 2026
95d0f61
avoid em dash
ysmolski Apr 21, 2026
b3c5708
fix the test with updated config
ysmolski Apr 21, 2026
f4c56a9
fix the generated resolvers
ysmolski Apr 21, 2026
5dbe102
fix checks
ysmolski Apr 21, 2026
368e8b1
use alias
ysmolski Apr 23, 2026
f18390d
fix names
ysmolski Apr 23, 2026
6cb40a0
clarify with comments
ysmolski Apr 24, 2026
f9857fc
handle repeatable directives
ysmolski Apr 24, 2026
aa05c59
refactor IFs a little
ysmolski Apr 24, 2026
beec49c
small fixes
ysmolski Apr 24, 2026
0763033
bump enginge to 2.0.0
ysmolski Apr 27, 2026
662ef84
Merge branch 'main' of github.com:wundergraph/cosmo into yury/eng-870…
ysmolski Apr 27, 2026
f5929be
update globals
ysmolski Apr 27, 2026
c16ed7b
rename variables and argument of a directive
ysmolski Apr 29, 2026
f54f071
Merge branch 'main' of github.com:wundergraph/cosmo into yury/eng-870…
ysmolski Apr 29, 2026
a76d9c5
update global
ysmolski Apr 29, 2026
ab94e78
verify that nullified argument of directive does not change cost
ysmolski Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions composition-go/index.global.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions composition/src/router-configuration/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export type Costs = {

export type FieldWeightConfiguration = {
argumentWeights: Map<ArgumentName, number>;
directiveArgumentWeights: Map<DirectiveArgumentCoords, number>;
fieldName: FieldName;
typeName: TypeName;
weight?: number;
Expand Down
117 changes: 83 additions & 34 deletions composition/src/v1/normalization/normalization-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ import {
type ConfigureDescriptionData,
type EntityData,
type EntityInterfaceSubgraphData,
type DirectiveDefinitionData,
type EnumDefinitionData,
type EnumValueData,
ExtensionType,
Expand Down Expand Up @@ -378,6 +379,7 @@ import {
type FieldSetParentResult,
type HandleCostDirectiveParams,
type HandleListSizeDirectiveParams,
type RecordDirectiveWeightOnFieldParams,
type HandleOverrideDirectiveParams,
type HandleRequiresScopesDirectiveParams,
type HandleSemanticNonNullDirectiveParams,
Expand Down Expand Up @@ -732,7 +734,10 @@ export class NormalizationFactory {
if (isAuthenticated) {
this.handleAuthenticatedDirective(data, parentTypeName);
}
if (isSemanticNonNull && isField) {
if (!isField) {
return errorMessages;
}
if (isSemanticNonNull) {
// The default argument for levels is [0], so a non-null wrapper is invalid.
if (isTypeRequired(data.type)) {
errorMessages.push(
Expand All @@ -745,11 +750,14 @@ export class NormalizationFactory {
data.nullLevelsBySubgraphName.set(this.subgraphName, new Set<number>([0]));
}
}
if (isListSize && isField && !isTypeNodeListType(data.type)) {
if (isListSize && !isTypeNodeListType(data.type)) {
errorMessages.push(
listSizeFieldMustReturnListOrUseSizedFieldsErrorMessage(directiveCoords, printTypeNode(data.type)),
);
}
if (!isCost && !isListSize) {
this.recordDirectiveWeightOnField({ data: data as FieldData, definitionData, directiveName, directiveNode });
}
return errorMessages;
}
const definedArgumentNames = new Set<string>();
Expand Down Expand Up @@ -812,8 +820,12 @@ export class NormalizationFactory {
}
if (isCost) {
this.handleCostDirective({ data, directiveCoords, directiveNode, errorMessages });
} else if (isListSize && isField) {
this.handleListSizeDirective({ data, directiveCoords, directiveNode, errorMessages });
} else if (isField) {
if (isListSize) {
this.handleListSizeDirective({ data, directiveCoords, directiveNode, errorMessages });
} else {
this.recordDirectiveWeightOnField({ data: data as FieldData, definitionData, directiveName, directiveNode });
}
}
if (duplicateArgumentNames.size > 0) {
errorMessages.push(duplicateDirectiveArgumentDefinitionsErrorMessage([...duplicateArgumentNames]));
Expand Down Expand Up @@ -2472,6 +2484,20 @@ export class NormalizationFactory {
data.nullLevelsBySubgraphName.set(this.subgraphName, levels);
}

getOrCreateFieldWeight(typeName: TypeName, fieldName: FieldName): FieldWeightConfiguration {
Comment thread
Aenimus marked this conversation as resolved.
const fieldCoords = `${typeName}.${fieldName}`;
return getValueOrDefault(
this.costs.fieldWeights,
fieldCoords,
(): FieldWeightConfiguration => ({
typeName,
fieldName,
argumentWeights: new Map(),
directiveArgumentWeights: new Map(),
}),
);
}

handleCostDirective({ data, directiveCoords, directiveNode, errorMessages }: HandleCostDirectiveParams) {
const weightArg = directiveNode.arguments?.find((arg) => arg.name.value === WEIGHT);
if (!weightArg || weightArg.value.kind !== Kind.INT) {
Expand All @@ -2495,16 +2521,7 @@ export class NormalizationFactory {
errorMessages.push(costOnInterfaceFieldErrorMessage(directiveCoords));
break;
}
const fieldCoords = `${typeName}.${data.name}`;
const fieldWeight = getValueOrDefault(
this.costs.fieldWeights,
fieldCoords,
(): FieldWeightConfiguration => ({
typeName,
fieldName: data.name,
argumentWeights: new Map(),
}),
);
const fieldWeight = this.getOrCreateFieldWeight(typeName, data.name);
fieldWeight.weight = weightValue;
break;
}
Expand All @@ -2522,36 +2539,68 @@ export class NormalizationFactory {
errorMessages.push(costOnInterfaceFieldErrorMessage(directiveCoords));
break;
}
const parentFieldCoords = `${typeName}.${ivData.fieldName}`;
const fieldWeight = getValueOrDefault(
this.costs.fieldWeights,
parentFieldCoords,
(): FieldWeightConfiguration => ({
typeName,
fieldName: ivData.fieldName!,
argumentWeights: new Map(),
}),
);
const fieldWeight = this.getOrCreateFieldWeight(typeName, ivData.fieldName!);
fieldWeight.argumentWeights.set(ivData.name, weightValue);
} else {
const typeName = ivData.renamedParentTypeName || ivData.originalParentTypeName;
const fieldCoords = `${typeName}.${ivData.name}`;
const fieldWeight = getValueOrDefault(
this.costs.fieldWeights,
fieldCoords,
(): FieldWeightConfiguration => ({
typeName,
fieldName: ivData.name,
argumentWeights: new Map(),
}),
);
const fieldWeight = this.getOrCreateFieldWeight(typeName, ivData.name);
fieldWeight.weight = weightValue;
}
break;
}
}
}

recordDirectiveWeightOnField({
data,
definitionData,
directiveName,
directiveNode,
}: RecordDirectiveWeightOnFieldParams) {
// This method walks every argument defined on the directive and records a directive weight
// on the field only when all these conditions hold:
// 1. The argument of the directive has a cost weight assigned
// 2. The argument is non-null
// 3. The parent type is not an interface type.
const typeName = data.renamedParentTypeName || data.originalParentTypeName;
const parentTypeData = this.parentDefinitionDataByTypeName.get(typeName);
// Directive argument weights should only be recorded for concrete type fields.
if (!parentTypeData || parentTypeData.kind === Kind.INTERFACE_TYPE_DEFINITION) {
return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Determine which arguments are non-null on this directive usage.
// Record the DirectiveArgument coords if its argument has an explicit non-null value or
// if it has a default value and was not explicitly set to null.
const suppliedArgNodeByName = new Map<string, ConstValueNode>();
for (const arg of directiveNode.arguments ?? []) {
suppliedArgNodeByName.set(arg.name.value, arg.value);
}

for (const [argName, argData] of definitionData.argumentTypeNodeByName) {
const coords = `${directiveName}.${argName}`;
const argWeight = this.costs.directiveArgumentWeights.get(coords);
// Bail if the argName argument does not have cost attached to it.
if (argWeight === undefined) {
continue;
}
// Check if this argument is non-null at the usage site:
const argNode = suppliedArgNodeByName.get(argName);
if (argNode) {
if (argNode.kind === Kind.NULL) {
continue;
}
} else if (!argData.defaultValue || argData.defaultValue.kind === Kind.NULL) {
continue;
}
const fieldWeight = this.getOrCreateFieldWeight(typeName, data.name);
// Accumulate across directive usages so that repeatable directives on the same
// field are charged once per usage.
const existingWeight = fieldWeight.directiveArgumentWeights.get(coords) ?? 0;
fieldWeight.directiveArgumentWeights.set(coords, existingWeight + argWeight);
}
}

handleListSizeDirective({ data, directiveCoords, directiveNode, errorMessages }: HandleListSizeDirectiveParams) {
const args = directiveNode.arguments;
if (!args) {
Expand Down
9 changes: 8 additions & 1 deletion composition/src/v1/normalization/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '../../schema-building/types';
import { type ConstDirectiveNode, type DocumentNode, type InputValueDefinitionNode, type ValueNode } from 'graphql';
import { type RequiredFieldConfiguration } from '../../router-configuration/types';
import { type DirectiveArgumentCoords, type SubgraphName } from '../../types/types';
import { type DirectiveArgumentCoords, type DirectiveName, type SubgraphName } from '../../types/types';

export type KeyFieldSetData = {
documentNode: DocumentNode;
Expand Down Expand Up @@ -82,6 +82,13 @@ export type HandleListSizeDirectiveParams = {
errorMessages: Array<string>;
};

export type RecordDirectiveWeightOnFieldParams = {
data: FieldData;
definitionData: DirectiveDefinitionData;
directiveName: DirectiveName;
directiveNode: ConstDirectiveNode;
};

export type AddInputValueDataByNodeParams = {
inputValueDataByName: Map<string, InputValueData>;
isArgument: boolean;
Expand Down
Loading
Loading