Skip to content

Commit b70a55b

Browse files
authored
feat(router): support costs on arguments of directives (#2781)
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! }
1 parent 403bd79 commit b70a55b

22 files changed

Lines changed: 710 additions & 325 deletions

File tree

composition-go/index.global.js

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

composition/src/router-configuration/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export type Costs = {
109109

110110
export type FieldWeightConfiguration = {
111111
argumentWeights: Map<ArgumentName, number>;
112+
directiveArgumentWeights: Map<DirectiveArgumentCoords, number>;
112113
fieldName: FieldName;
113114
typeName: TypeName;
114115
weight?: number;

composition/src/v1/normalization/normalization-factory.ts

Lines changed: 83 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ import {
212212
type ConfigureDescriptionData,
213213
type EntityData,
214214
type EntityInterfaceSubgraphData,
215+
type DirectiveDefinitionData,
215216
type EnumDefinitionData,
216217
type EnumValueData,
217218
ExtensionType,
@@ -378,6 +379,7 @@ import {
378379
type FieldSetParentResult,
379380
type HandleCostDirectiveParams,
380381
type HandleListSizeDirectiveParams,
382+
type RecordDirectiveWeightOnFieldParams,
381383
type HandleOverrideDirectiveParams,
382384
type HandleRequiresScopesDirectiveParams,
383385
type HandleSemanticNonNullDirectiveParams,
@@ -732,7 +734,10 @@ export class NormalizationFactory {
732734
if (isAuthenticated) {
733735
this.handleAuthenticatedDirective(data, parentTypeName);
734736
}
735-
if (isSemanticNonNull && isField) {
737+
if (!isField) {
738+
return errorMessages;
739+
}
740+
if (isSemanticNonNull) {
736741
// The default argument for levels is [0], so a non-null wrapper is invalid.
737742
if (isTypeRequired(data.type)) {
738743
errorMessages.push(
@@ -745,11 +750,14 @@ export class NormalizationFactory {
745750
data.nullLevelsBySubgraphName.set(this.subgraphName, new Set<number>([0]));
746751
}
747752
}
748-
if (isListSize && isField && !isTypeNodeListType(data.type)) {
753+
if (isListSize && !isTypeNodeListType(data.type)) {
749754
errorMessages.push(
750755
listSizeFieldMustReturnListOrUseSizedFieldsErrorMessage(directiveCoords, printTypeNode(data.type)),
751756
);
752757
}
758+
if (!isCost && !isListSize) {
759+
this.recordDirectiveWeightOnField({ data: data as FieldData, definitionData, directiveName, directiveNode });
760+
}
753761
return errorMessages;
754762
}
755763
const definedArgumentNames = new Set<string>();
@@ -812,8 +820,12 @@ export class NormalizationFactory {
812820
}
813821
if (isCost) {
814822
this.handleCostDirective({ data, directiveCoords, directiveNode, errorMessages });
815-
} else if (isListSize && isField) {
816-
this.handleListSizeDirective({ data, directiveCoords, directiveNode, errorMessages });
823+
} else if (isField) {
824+
if (isListSize) {
825+
this.handleListSizeDirective({ data, directiveCoords, directiveNode, errorMessages });
826+
} else {
827+
this.recordDirectiveWeightOnField({ data: data as FieldData, definitionData, directiveName, directiveNode });
828+
}
817829
}
818830
if (duplicateArgumentNames.size > 0) {
819831
errorMessages.push(duplicateDirectiveArgumentDefinitionsErrorMessage([...duplicateArgumentNames]));
@@ -2472,6 +2484,20 @@ export class NormalizationFactory {
24722484
data.nullLevelsBySubgraphName.set(this.subgraphName, levels);
24732485
}
24742486

2487+
getOrCreateFieldWeight(typeName: TypeName, fieldName: FieldName): FieldWeightConfiguration {
2488+
const fieldCoords = `${typeName}.${fieldName}`;
2489+
return getValueOrDefault(
2490+
this.costs.fieldWeights,
2491+
fieldCoords,
2492+
(): FieldWeightConfiguration => ({
2493+
typeName,
2494+
fieldName,
2495+
argumentWeights: new Map(),
2496+
directiveArgumentWeights: new Map(),
2497+
}),
2498+
);
2499+
}
2500+
24752501
handleCostDirective({ data, directiveCoords, directiveNode, errorMessages }: HandleCostDirectiveParams) {
24762502
const weightArg = directiveNode.arguments?.find((arg) => arg.name.value === WEIGHT);
24772503
if (!weightArg || weightArg.value.kind !== Kind.INT) {
@@ -2495,16 +2521,7 @@ export class NormalizationFactory {
24952521
errorMessages.push(costOnInterfaceFieldErrorMessage(directiveCoords));
24962522
break;
24972523
}
2498-
const fieldCoords = `${typeName}.${data.name}`;
2499-
const fieldWeight = getValueOrDefault(
2500-
this.costs.fieldWeights,
2501-
fieldCoords,
2502-
(): FieldWeightConfiguration => ({
2503-
typeName,
2504-
fieldName: data.name,
2505-
argumentWeights: new Map(),
2506-
}),
2507-
);
2524+
const fieldWeight = this.getOrCreateFieldWeight(typeName, data.name);
25082525
fieldWeight.weight = weightValue;
25092526
break;
25102527
}
@@ -2522,36 +2539,68 @@ export class NormalizationFactory {
25222539
errorMessages.push(costOnInterfaceFieldErrorMessage(directiveCoords));
25232540
break;
25242541
}
2525-
const parentFieldCoords = `${typeName}.${ivData.fieldName}`;
2526-
const fieldWeight = getValueOrDefault(
2527-
this.costs.fieldWeights,
2528-
parentFieldCoords,
2529-
(): FieldWeightConfiguration => ({
2530-
typeName,
2531-
fieldName: ivData.fieldName!,
2532-
argumentWeights: new Map(),
2533-
}),
2534-
);
2542+
const fieldWeight = this.getOrCreateFieldWeight(typeName, ivData.fieldName!);
25352543
fieldWeight.argumentWeights.set(ivData.name, weightValue);
25362544
} else {
25372545
const typeName = ivData.renamedParentTypeName || ivData.originalParentTypeName;
2538-
const fieldCoords = `${typeName}.${ivData.name}`;
2539-
const fieldWeight = getValueOrDefault(
2540-
this.costs.fieldWeights,
2541-
fieldCoords,
2542-
(): FieldWeightConfiguration => ({
2543-
typeName,
2544-
fieldName: ivData.name,
2545-
argumentWeights: new Map(),
2546-
}),
2547-
);
2546+
const fieldWeight = this.getOrCreateFieldWeight(typeName, ivData.name);
25482547
fieldWeight.weight = weightValue;
25492548
}
25502549
break;
25512550
}
25522551
}
25532552
}
25542553

2554+
recordDirectiveWeightOnField({
2555+
data,
2556+
definitionData,
2557+
directiveName,
2558+
directiveNode,
2559+
}: RecordDirectiveWeightOnFieldParams) {
2560+
// This method walks every argument defined on the directive and records a directive weight
2561+
// on the field only when all these conditions hold:
2562+
// 1. The argument of the directive has a cost weight assigned
2563+
// 2. The argument is non-null
2564+
// 3. The parent type is not an interface type.
2565+
const typeName = data.renamedParentTypeName || data.originalParentTypeName;
2566+
const parentTypeData = this.parentDefinitionDataByTypeName.get(typeName);
2567+
// Directive argument weights should only be recorded for concrete type fields.
2568+
if (!parentTypeData || parentTypeData.kind === Kind.INTERFACE_TYPE_DEFINITION) {
2569+
return;
2570+
}
2571+
2572+
// Determine which arguments are non-null on this directive usage.
2573+
// Record the DirectiveArgument coords if its argument has an explicit non-null value or
2574+
// if it has a default value and was not explicitly set to null.
2575+
const suppliedArgNodeByName = new Map<string, ConstValueNode>();
2576+
for (const arg of directiveNode.arguments ?? []) {
2577+
suppliedArgNodeByName.set(arg.name.value, arg.value);
2578+
}
2579+
2580+
for (const [argName, argData] of definitionData.argumentTypeNodeByName) {
2581+
const coords = `${directiveName}.${argName}`;
2582+
const argWeight = this.costs.directiveArgumentWeights.get(coords);
2583+
// Bail if the argName argument does not have cost attached to it.
2584+
if (argWeight === undefined) {
2585+
continue;
2586+
}
2587+
// Check if this argument is non-null at the usage site:
2588+
const argNode = suppliedArgNodeByName.get(argName);
2589+
if (argNode) {
2590+
if (argNode.kind === Kind.NULL) {
2591+
continue;
2592+
}
2593+
} else if (!argData.defaultValue || argData.defaultValue.kind === Kind.NULL) {
2594+
continue;
2595+
}
2596+
const fieldWeight = this.getOrCreateFieldWeight(typeName, data.name);
2597+
// Accumulate across directive usages so that repeatable directives on the same
2598+
// field are charged once per usage.
2599+
const existingWeight = fieldWeight.directiveArgumentWeights.get(coords) ?? 0;
2600+
fieldWeight.directiveArgumentWeights.set(coords, existingWeight + argWeight);
2601+
}
2602+
}
2603+
25552604
handleListSizeDirective({ data, directiveCoords, directiveNode, errorMessages }: HandleListSizeDirectiveParams) {
25562605
const args = directiveNode.arguments;
25572606
if (!args) {

composition/src/v1/normalization/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '../../schema-building/types';
1111
import { type ConstDirectiveNode, type DocumentNode, type InputValueDefinitionNode, type ValueNode } from 'graphql';
1212
import { type RequiredFieldConfiguration } from '../../router-configuration/types';
13-
import { type DirectiveArgumentCoords, type SubgraphName } from '../../types/types';
13+
import { type DirectiveArgumentCoords, type DirectiveName, type SubgraphName } from '../../types/types';
1414

1515
export type KeyFieldSetData = {
1616
documentNode: DocumentNode;
@@ -82,6 +82,13 @@ export type HandleListSizeDirectiveParams = {
8282
errorMessages: Array<string>;
8383
};
8484

85+
export type RecordDirectiveWeightOnFieldParams = {
86+
data: FieldData;
87+
definitionData: DirectiveDefinitionData;
88+
directiveName: DirectiveName;
89+
directiveNode: ConstDirectiveNode;
90+
};
91+
8592
export type AddInputValueDataByNodeParams = {
8693
inputValueDataByName: Map<string, InputValueData>;
8794
isArgument: boolean;

0 commit comments

Comments
 (0)