Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
43 changes: 39 additions & 4 deletions composition/src/errors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1812,22 +1812,57 @@ export function oneOfRequiredFieldsError({ requiredFieldNames, typeName }: OneOf

export function listSizeInvalidSlicingArgumentErrorMessage(
directiveCoords: DirectiveArgumentCoords,
argumentName: ArgumentName,
path: ArgumentName,
): string {
return ` The "slicingArguments" value "${argumentName}" on "${directiveCoords}" does not reference a defined argument on this field.`;
return ` The "slicingArguments" value "${path}" on "${directiveCoords}" does not reference a defined argument on this field.`;
}

export function listSizeSlicingArgumentNotIntErrorMessage(
directiveCoords: DirectiveArgumentCoords,
argumentName: ArgumentName,
path: ArgumentName,
actualType: TypeName,
): string {
return (
` The "slicingArguments" value "${argumentName}" on "${directiveCoords}" references an argument of type` +
` The "slicingArguments" value "${path}" on "${directiveCoords}" references an argument of type` +
` "${actualType}", but slicing arguments must be of type "Int" or "Int!".`
);
}

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
130 changes: 119 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,12 @@ export class NormalizationFactory {
sizedFields: [],
requireOneSlicingArgument: true, // per IBM cost spec
};
/**
* For each accepted slicing argument, capture the full resolved chain:
* (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 +2593,86 @@ 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.
continue;
Comment thread
ysmolski marked this conversation as 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.
* `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.
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.
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 +2738,44 @@ 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.
* When assumedSize is set together with slicingArguments, the slicing argument must not have
* a default leaf value in any element of the slicingArgument's chain.
* That will 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 along the chain only matters if it actually supplies a value for the leaf:
* walk the remaining tail through the default's AST and reject only when the walk resolves.
* E.g. `a: SearchInput = { b: {} }` is allowed, and the leaf stays undefined.
*/
for (const slicingArgPath of listSizeConfig.slicingArguments) {
const chain = slicingArgChainByPath.get(slicingArgPath);
if (!chain) {
continue;
}
const segments = slicingArgPath.split(LITERAL_PERIOD);
// Outer loop: which default are we starting from?
for (let i = 0; i < chain.length; i++) {
let value: ConstValueNode | undefined = chain[i].defaultValue;
// Inner loop: try to find a defined leaf in the chain:
for (let j = i + 1; j < segments.length; j++) {
if (!value || value.kind !== Kind.OBJECT) {
value = undefined;
break;
}
const field = value.fields.find((f) => f.name.value === segments[j]);
value = field?.value;
}
if (value !== undefined) {
errorMessages.push(listSizeAssumedSizeSlicingArgDefaultErrorMessage(directiveCoords, slicingArgPath));
break;
}
}
}
}
Expand Down
Loading
Loading