Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e2c39a1
feat: support dot-path in slicingArguments in composition
ysmolski Apr 28, 2026
9ecb480
update global
ysmolski Apr 28, 2026
b607596
use full value of slicing argument in the error
ysmolski Apr 28, 2026
8f05833
refine error messages
ysmolski Apr 28, 2026
6bf922d
fix lint
ysmolski Apr 28, 2026
42adc32
verify router support
ysmolski May 6, 2026
13ab1bc
Merge branch 'main' of github.com:wundergraph/cosmo into yury/eng-933…
ysmolski May 7, 2026
a990735
update generated
ysmolski May 7, 2026
453d312
remove stale file
ysmolski May 7, 2026
b0fc41f
fix names
ysmolski May 7, 2026
6c37569
fix comment
ysmolski May 7, 2026
773474d
fix 1 comment
ysmolski May 7, 2026
8729bc1
allow intermediate default values with undefined leaf
ysmolski May 7, 2026
bb1d725
Merge branch 'main' of github.com:wundergraph/cosmo into yury/eng-933…
ysmolski May 7, 2026
68eeba8
bump engine
ysmolski May 7, 2026
ddffbe8
update lock file
ysmolski May 7, 2026
71a37df
update docs
ysmolski May 7, 2026
201d78e
Merge branch 'main' of github.com:wundergraph/cosmo into yury/eng-933…
ysmolski May 11, 2026
c98200f
update goldies
ysmolski May 11, 2026
6814d00
bump engine to 2.3.0
ysmolski May 15, 2026
5c41c35
add edge cases
ysmolski May 18, 2026
f8f2fd4
Merge branch 'main' of github.com:wundergraph/cosmo into yury/eng-933…
ysmolski May 18, 2026
aabb5c5
fix names
ysmolski May 18, 2026
cc81248
update test for tested queries
ysmolski May 18, 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
35 changes: 35 additions & 0 deletions composition/src/errors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1797,6 +1797,41 @@ export function listSizeSlicingArgumentNotIntErrorMessage(
);
}

export function listSizeSlicingArgumentMalformedPathErrorMessage(
directiveCoords: DirectiveArgumentCoords,
path: string,
): string {
return (
` The "slicingArguments" value "${path}" on "${directiveCoords}" is not a valid path.` +
` A path must be a non-empty argument name, optionally followed by ".<inputField>" segments,` +
` with no empty segments and no leading or trailing dots.`
);
}

export function listSizeSlicingArgumentSegmentNotFoundErrorMessage(
directiveCoords: DirectiveArgumentCoords,
path: string,
segment: string,
parentTypeName: TypeName,
): string {
return (
` The "slicingArguments" path "${path}" on "${directiveCoords}" references "${segment}",` +
` which is not a defined field on Input Object type "${parentTypeName}".`
);
}

export function listSizeSlicingArgumentSegmentNotInputObjectErrorMessage(
directiveCoords: DirectiveArgumentCoords,
path: string,
segment: string,
typeName: TypeName,
): string {
return (
` The "slicingArguments" path "${path}" on "${directiveCoords}" references "${segment}",` +
` whose type "${typeName}" is not an Input Object.`
);
}

export function listSizeSizedFieldNotFoundErrorMessage(
directiveCoords: DirectiveArgumentCoords,
fieldName: FieldName,
Expand Down
108 changes: 97 additions & 11 deletions composition/src/v1/normalization/normalization-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,10 @@ import {
listSizeSizedFieldNotListErrorMessage,
listSizeSizedFieldsInvalidReturnTypeErrorMessage,
listSizeSizedFieldsOnListsErrorMessage,
listSizeSlicingArgumentMalformedPathErrorMessage,
listSizeSlicingArgumentNotIntErrorMessage,
listSizeSlicingArgumentSegmentNotFoundErrorMessage,
listSizeSlicingArgumentSegmentNotInputObjectErrorMessage,
multipleNamedTypeDefinitionError,
noBaseScalarDefinitionError,
noDefinedEnumValuesError,
Expand Down Expand Up @@ -292,6 +295,7 @@ import {
LEVELS,
LIST_SIZE,
LITERAL_AT,
LITERAL_PERIOD,
MUTATION,
NON_NULLABLE_BOOLEAN,
NON_NULLABLE_EDFS_PUBLISH_EVENT_RESULT,
Expand Down Expand Up @@ -2554,6 +2558,10 @@ export class NormalizationFactory {
sizedFields: [],
requireOneSlicingArgument: true, // per IBM cost spec
};
// For each accepted slicing argument, capture the full resolved chain:
Comment thread
Aenimus marked this conversation as resolved.
Resolved
// (argument + each intermediate input field + leaf).
// Later we check the assumedSize against the default value of every element of the chain.
const slicingArgChainByPath = new Map<string, InputValueData[]>();

for (const argumentNode of args) {
const argumentName = argumentNode.name.value;
Expand Down Expand Up @@ -2583,22 +2591,84 @@ export class NormalizationFactory {
continue;
}

const slicingArgName = (valueNode as StringValueNode).value;
const argData = data.argumentDataByName.get(slicingArgName);
// slicingArgPath could be just an argument "inputA" or a dot-path "inputA.page.first".
const slicingArgPath = (valueNode as StringValueNode).value;
const segments = slicingArgPath.split(LITERAL_PERIOD);

// Reject empty segments (e.g. "", "a.", ".b", "a..b").
if (segments.length === 0 || segments.some((s) => s.length === 0)) {
errorMessages.push(listSizeSlicingArgumentMalformedPathErrorMessage(directiveCoords, slicingArgPath));
continue;
}

// First segment should be valid argument name:
const firstSegment = segments[0];
const argData = data.argumentDataByName.get(firstSegment);
if (!argData) {
errorMessages.push(listSizeInvalidSlicingArgumentErrorMessage(directiveCoords, slicingArgName));
errorMessages.push(listSizeInvalidSlicingArgumentErrorMessage(directiveCoords, slicingArgPath));
Comment thread
ysmolski marked this conversation as resolved.
Resolved
continue;
Comment thread
ysmolski marked this conversation as resolved.
Resolved
}

const unwrappedType = argData.type.kind === Kind.NON_NULL_TYPE ? argData.type.type : argData.type;
if (unwrappedType.kind === Kind.LIST_TYPE || argData.namedTypeName !== INT_SCALAR) {
// Walk the rest of the path in the slicingArgument.
Comment thread
ysmolski marked this conversation as resolved.
Resolved
// `current` tracks the input value reached so far:
// the argument itself for a flat path, or the nested input field for each step of a
// dot-path. After the loop it is the leaf, which must be Int/Int!.
let current: InputValueData = argData;
const chain: Array<InputValueData> = [argData];
let isPathInvalid = false;

for (let i = 1; i < segments.length; i++) {
// Non-leaf step: `current` must unwrap to an Input Object. Lists are rejected here.
const unwrapped = current.type.kind === Kind.NON_NULL_TYPE ? current.type.type : current.type;
Comment thread
Aenimus marked this conversation as resolved.
Resolved
const parentTypeData = this.parentDefinitionDataByTypeName.get(current.namedTypeName);
if (
unwrapped.kind === Kind.LIST_TYPE ||
!parentTypeData ||
parentTypeData.kind !== Kind.INPUT_OBJECT_TYPE_DEFINITION
) {
errorMessages.push(
listSizeSlicingArgumentSegmentNotInputObjectErrorMessage(
directiveCoords,
slicingArgPath,
current.name,
printTypeNode(current.type),
),
);
isPathInvalid = true;
Comment thread
ysmolski marked this conversation as resolved.
Resolved
break;
}
const inputValue = parentTypeData.inputValueDataByName.get(segments[i]);
if (!inputValue) {
errorMessages.push(
listSizeSlicingArgumentSegmentNotFoundErrorMessage(
directiveCoords,
slicingArgPath,
segments[i],
current.namedTypeName,
),
);
isPathInvalid = true;
break;
}
current = inputValue;
chain.push(inputValue);
}

if (isPathInvalid) {
continue;
}

// Leaf check: the final value must be Int or Int!
const unwrappedType = current.type.kind === Kind.NON_NULL_TYPE ? current.type.type : current.type;
if (unwrappedType.kind === Kind.LIST_TYPE || current.namedTypeName !== INT_SCALAR) {
errorMessages.push(
listSizeSlicingArgumentNotIntErrorMessage(directiveCoords, slicingArgName, printTypeNode(argData.type)),
listSizeSlicingArgumentNotIntErrorMessage(directiveCoords, slicingArgPath, printTypeNode(current.type)),
);
continue;
}

listSizeConfig.slicingArguments.push(slicingArgName);
listSizeConfig.slicingArguments.push(slicingArgPath);
slicingArgChainByPath.set(slicingArgPath, chain);
}
break;
}
Expand Down Expand Up @@ -2664,10 +2734,26 @@ export class NormalizationFactory {
if (listSizeConfig.requireOneSlicingArgument) {
errorMessages.push(listSizeAssumedSizeWithRequiredSlicingArgumentErrorMessage(directiveCoords));
} else {
for (const slicingArgName of listSizeConfig.slicingArguments) {
const argData = data.argumentDataByName.get(slicingArgName);
if (argData?.defaultValue) {
errorMessages.push(listSizeAssumedSizeSlicingArgDefaultErrorMessage(directiveCoords, slicingArgName));
/**
Comment thread
Aenimus marked this conversation as resolved.
Resolved
* When assumedSize is set together with slicingArguments, the slicing argument must not have
* a default in any element of the slicingArgument's chain. That should be checked for all slicing Arguments.
* For example, if the query is defined like this:
* search(a: SearchInput): [Book]
* @listSize(assumedSize: 50, slicingArguments: ["a.b.c"], requireOneSlicingArgument: false)
* any of those defaults are forbidden:
* input SearchInput { b: PaginationInput = { c: 10 } }
* input PaginationInput { c: Int = 10 }
* Notably, this query definition is also forbidden with the @listSize above:
* search(a: SearchInput = { b: { c: 10 } }): [Book]
* A default anywhere along the chain (argument, any intermediate input field, the leaf)
* means the slicing value can resolve via that default rather than assumedSize.
* which violates the IBM cost spec rule.
*/
for (const slicingArgPath of listSizeConfig.slicingArguments) {
const chain = slicingArgChainByPath.get(slicingArgPath);
if (chain?.some((node) => node.defaultValue)) {
// At least one node in the chain has a defaultValue set.
errorMessages.push(listSizeAssumedSizeSlicingArgDefaultErrorMessage(directiveCoords, slicingArgPath));
Comment thread
ysmolski marked this conversation as resolved.
Resolved
}
}
}
Expand Down
Loading
Loading