Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,11 @@ import {
OPENFED_CACHE_POPULATE,
OPENFED_ENTITY_CACHE,
INCLUDE_HEADERS,
OPENFED_IS,
MAX_AGE,
NEGATIVE_CACHE_TTL,
PARTIAL_CACHE_LOAD,
OPENFED_REQUEST_SCOPED,
OPENFED_QUERY_CACHE,
SHADOW_MODE,
} from '../utils/string-constants';
import {
Expand Down Expand Up @@ -124,7 +125,8 @@ import {
OPENFED_CACHE_INVALIDATE_DEFINITION,
OPENFED_CACHE_POPULATE_DEFINITION,
OPENFED_ENTITY_CACHE_DEFINITION,
OPENFED_REQUEST_SCOPED_DEFINITION,
OPENFED_IS_DEFINITION,
OPENFED_QUERY_CACHE_DEFINITION,
SPECIFIED_BY_DEFINITION,
SUBSCRIPTION_FILTER_DEFINITION,
TAG_DEFINITION,
Expand Down Expand Up @@ -1047,20 +1049,60 @@ export const CACHE_POPULATE_DEFINITION_DATA = newDirectiveDefinitionData({
node: OPENFED_CACHE_POPULATE_DEFINITION,
optionalArgumentNames: new Set<ArgumentName>([MAX_AGE]),
});
export const REQUEST_SCOPED_DEFINITION_DATA = newDirectiveDefinitionData({

export const QUERY_CACHE_DEFINITION_DATA = newDirectiveDefinitionData({
argumentDataByName: new Map<ArgumentName, DirectiveArgumentData>([
[
KEY,
MAX_AGE,
newDirectiveArgumentData({
directive: `@${OPENFED_REQUEST_SCOPED}`,
name: KEY,
directive: `@${OPENFED_QUERY_CACHE}`,
name: MAX_AGE,
namedTypeKind: Kind.SCALAR_TYPE_DEFINITION,
typeNode: REQUIRED_STRING_TYPE_NODE,
typeNode: REQUIRED_INT_TYPE_NODE,
}),
],
[
INCLUDE_HEADERS,
newDirectiveArgumentData({
directive: `@${OPENFED_QUERY_CACHE}`,
defaultValue: { kind: Kind.BOOLEAN, value: false },
name: INCLUDE_HEADERS,
namedTypeKind: Kind.SCALAR_TYPE_DEFINITION,
typeNode: stringToNamedTypeNode(BOOLEAN_SCALAR),
}),
],
[
SHADOW_MODE,
newDirectiveArgumentData({
directive: `@${OPENFED_QUERY_CACHE}`,
defaultValue: { kind: Kind.BOOLEAN, value: false },
name: SHADOW_MODE,
namedTypeKind: Kind.SCALAR_TYPE_DEFINITION,
typeNode: stringToNamedTypeNode(BOOLEAN_SCALAR),
}),
],
]),
locations: new Set<DirectiveLocation>([FIELD_DEFINITION_UPPER]),
name: OPENFED_REQUEST_SCOPED,
node: OPENFED_REQUEST_SCOPED_DEFINITION,
requiredArgumentNames: new Set<ArgumentName>([KEY]),
name: OPENFED_QUERY_CACHE,
node: OPENFED_QUERY_CACHE_DEFINITION,
optionalArgumentNames: new Set<ArgumentName>([INCLUDE_HEADERS, SHADOW_MODE]),
requiredArgumentNames: new Set<ArgumentName>([MAX_AGE]),
});

export const IS_DEFINITION_DATA = newDirectiveDefinitionData({
argumentDataByName: new Map<ArgumentName, DirectiveArgumentData>([
[
FIELDS,
newDirectiveArgumentData({
directive: `@${OPENFED_IS}`,
name: FIELDS,
namedTypeKind: Kind.SCALAR_TYPE_DEFINITION,
typeNode: REQUIRED_STRING_TYPE_NODE,
}),
],
]),
locations: new Set<DirectiveLocation>([ARGUMENT_DEFINITION_UPPER]),
name: OPENFED_IS,
node: OPENFED_IS_DEFINITION,
requiredArgumentNames: new Set<ArgumentName>([FIELDS]),
});
249 changes: 247 additions & 2 deletions composition/src/errors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type InvalidRepeatedDirectiveErrorParams,
type InvalidSubValueFieldLinkDirectiveImportErrorParams,
type invalidVersionLinkDirectiveUrlErrorParams,
type MaxAgeNotPositiveIntegerErrorParams,
type NonExternalConditionalFieldErrorParams,
type OneOfRequiredFieldsErrorParams,
type SemanticNonNullLevelsIndexOutOfBoundsErrorParams,
Expand Down Expand Up @@ -2056,14 +2057,31 @@ export function entityCacheWithoutKeyErrorMessage(typeName: TypeName): string {
return `Object "${typeName}" does not define a "@key" directive.`;
}

export function maxAgeNotPositiveIntegerErrorMessage(value: number | string | null): string {
return `The argument "maxAge" must be provided a positive integer; received "${value}".`;
export function maxAgeNotPositiveIntegerErrorMessage({
directiveName,
value,
}: MaxAgeNotPositiveIntegerErrorParams): string {
return `The directive "@${directiveName}" argument "maxAge" must be provided a positive integer; received "${value}".`;
}

export function negativeCacheTTLNotNonNegativeIntegerErrorMessage(value: number): string {
return `The argument "negativeCacheTTL" must be provided zero or a positive integer; received "${value}".`;
}

export function queryCacheOnNonQueryFieldErrorMessage(fieldCoords: string): string {
return (
`@openfed__queryCache must only be defined on fields of the root query type; found on "${fieldCoords}".` +
` Use @openfed__cachePopulate or @openfed__cacheInvalidate on mutation or subscription fields.`
);
}

export function queryCacheOnNonEntityReturnTypeErrorMessage(fieldCoords: string, returnType: string): string {
return (
`Field "${fieldCoords}" has @openfed__queryCache but returns non-entity type "${returnType}".` +
` @openfed__queryCache requires the return type to be an entity with @key.`
);
}

export function invalidMutationOrSubscriptionFieldCoordsErrorMessage(fieldCoords: string): string {
return `Field coordinates "${fieldCoords}" are not a Mutation or Subscription root field.`;
}
Expand All @@ -2081,3 +2099,230 @@ export function invalidMutuallyExclusiveCacheDirectivesError(fieldCoords: string
` and "@openfed__cachePopulate".`,
);
}

export function isWithoutQueryCacheErrorMessage(argumentName: string, fieldCoords: string): string {
return `@openfed__is on argument "${argumentName}" of field "${fieldCoords}" has no effect without @openfed__queryCache.`;
}

export function isReferencesUnknownKeyFieldErrorMessage(
isField: string,
argumentName: string,
fieldCoords: string,
entityType: string,
): string {
return (
`@openfed__is(fields: "${isField}") on argument "${argumentName}" of field "${fieldCoords}"` +
` references unknown @key field "${isField}" on type "${entityType}".`
);
}

export function duplicateKeyFieldMappingErrorMessage(fieldCoords: string, keyField: string): string {
return `Multiple arguments on field "${fieldCoords}" map to @key field "${keyField}".`;
}

export function explicitTypeMismatchErrorMessage(
argumentName: string,
fieldCoords: string,
argumentType: string,
isField: string,
entityType: string,
keyFieldType: string,
): string {
return `Argument "${argumentName}" on field "${fieldCoords}" has type "${argumentType}" but @openfed__is(fields: "${isField}") targets @key field "${isField}" of type "${keyFieldType}" on entity "${entityType}".`;
}

export function nonKeyFieldSpecErrorMessage(
argumentName: string,
fieldCoords: string,
isField: string,
entityType: string,
): string {
return `Argument "${argumentName}" on field "${fieldCoords}" uses @openfed__is(fields: "${isField}"), but "${isField}" is not a @key field on entity "${entityType}". @openfed__is can only target fields that are part of a @key.`;
}

export function listArgumentToScalarKeySpecErrorMessage(
argumentName: string,
fieldCoords: string,
argumentType: string,
isField: string,
entityType: string,
keyFieldType: string,
): string {
return (
`Argument "${argumentName}" on field "${fieldCoords}" has type "${argumentType}" but @openfed__is(fields: "${isField}") targets @key field "${isField}" of type "${keyFieldType}" on entity "${entityType}".` +
' List arguments can only map to scalar key fields when the field returns a list of entities, or to list key fields when the key field itself is a list type.'
);
}

export function scalarArgumentToListKeySpecErrorMessage(
argumentName: string,
fieldCoords: string,
argumentType: string,
isField: string,
entityType: string,
keyFieldType: string,
): string {
return (
`Argument "${argumentName}" on field "${fieldCoords}" has type "${argumentType}" but @openfed__is(fields: "${isField}") targets @key field "${isField}" of type "${keyFieldType}" on entity "${entityType}".` +
' A scalar argument cannot map to a list key field.'
);
}

export function explicitIncompleteCompositeKeyErrorMessage(
fieldCoords: string,
argumentName: string,
mappedField: string,
entityType: string,
compositeKey: string,
missingField: string,
): string {
return `Field "${fieldCoords}" has argument "${argumentName}" with @openfed__is mapping to @key field "${mappedField}" on entity "${entityType}", but composite @key "${compositeKey}" is incomplete because no argument maps to required key field "${missingField}".`;
}

export function explicitSingularAdditionalNonKeyArgumentErrorMessage(
fieldCoords: string,
argumentName: string,
keyField: string,
entityType: string,
extraArgument: string,
): string {
return `Field "${fieldCoords}" has argument "${argumentName}" with @openfed__is mapping to @key field "${keyField}" on entity "${entityType}", but also has additional argument "${extraArgument}" which is not mapped to a key field. All arguments must be key arguments — additional arguments may filter the response, making the cache key incomplete.`;
}

export function explicitCompositeAdditionalNonKeyArgumentErrorMessage(
fieldCoords: string,
firstArgument: string,
secondArgument: string,
compositeKey: string,
entityType: string,
extraArgument: string,
): string {
return `Field "${fieldCoords}" has arguments "${firstArgument}" and "${secondArgument}" with @openfed__is mappings covering composite @key "${compositeKey}" on entity "${entityType}", but also has additional argument "${extraArgument}" which is not mapped to a key field. All arguments must be key arguments — additional arguments may filter the response, making the cache key incomplete.`;
}

export function batchListValuedKeyRequiresNestedListsErrorMessage(
fieldCoords: string,
isField: string,
entityType: string,
actualType: string,
): string {
return `Field "${fieldCoords}" returns a list of entities, so cache lookup is a batch lookup and requires one key value per entity. Because @openfed__is(fields: "${isField}") targets list-valued @key field "${isField}" on entity "${entityType}", the argument must provide a list of tag lists (e.g., "[[String!]!]!"), not ${actualType}.`;
}

export function explicitBatchAdditionalNonKeyArgumentErrorMessage(
fieldCoords: string,
argumentName: string,
keyField: string,
entityType: string,
extraArgument: string,
): string {
return `Field "${fieldCoords}" returns a list of entities, so cache lookup is a batch lookup and requires a single key input that determines the returned entities. Argument "${argumentName}" uses @openfed__is to map to @key field "${keyField}" on entity "${entityType}", but additional argument "${extraArgument}" is not mapped to a key field and may filter the response, so the batch key would be incomplete.`;
}

export function explicitScalarArgumentsCannotEstablishBatchMappingErrorMessage(
fieldCoords: string,
entityType: string,
): string {
return `Field "${fieldCoords}" returns a list of entities, so cache lookup is a batch lookup and requires one key value per entity. Scalar arguments with @openfed__is mapping to @key fields on entity "${entityType}" cannot provide a batch of keys, so they cannot establish cache key mappings for this field. Use list arguments for batch cache lookups.`;
}

export function multipleListArgumentsBatchFactoryMessage(fieldCoords: string, entityType: string): string {
return (
`Field "${fieldCoords}" has multiple list arguments mapping to @key fields on entity "${entityType}".` +
' Batch cache lookups require a single list argument.' +
' For composite keys, use a single list of input objects instead.'
);
}

export function inputObjectCompositeTypeMismatchErrorMessage(
argumentName: string,
fieldCoords: string,
keyFields: string,
entityType: string,
inputType: string,
inputFieldName: string,
inputFieldType: string,
entityFieldPath: string,
entityFieldType: string,
): string {
return (
`Argument "${argumentName}" on field "${fieldCoords}" uses @openfed__is(fields: "${keyFields}") mapping to composite @key on entity "${entityType}",` +
` but input type "${inputType}" field "${inputFieldName}" has type "${inputFieldType}"` +
` which does not match key field "${entityFieldPath}" of type "${entityFieldType}".`
);
}

export function inputObjectCompositeMissingFieldErrorMessage(
argumentName: string,
fieldCoords: string,
keyFields: string,
entityType: string,
inputType: string,
missingFieldName: string,
): string {
return (
`Argument "${argumentName}" on field "${fieldCoords}" uses @openfed__is(fields: "${keyFields}") mapping to composite @key on entity "${entityType}",` +
` but input type "${inputType}" is missing required key field "${missingFieldName}".`
);
}

export function nestedKeyRequiresInputObjectErrorMessage(
argumentName: string,
fieldCoords: string,
keyFields: string,
entityType: string,
inputTypeName: string,
entityKeyPath: string,
): string {
return (
`Argument "${argumentName}" on field "${fieldCoords}" maps to nested @key "${keyFields}" on entity "${entityType}",` +
` but the input field at key path "${entityKeyPath}" has type "${inputTypeName}",` +
` which is not an input object type and therefore cannot provide the nested key selection.`
);
}

export function nestedInputObjectTypeMismatchErrorMessage(
argumentName: string,
fieldCoords: string,
keyFields: string,
entityType: string,
inputType: string,
inputFieldName: string,
inputFieldType: string,
entityFieldPath: string,
entityFieldType: string,
): string {
return (
`Argument "${argumentName}" on field "${fieldCoords}" maps to nested @key "${keyFields}" on entity "${entityType}",` +
` but input type "${inputType}" field "${inputFieldName}" has type "${inputFieldType}"` +
` which does not match key field "${entityFieldPath}" of type "${entityFieldType}".`
);
}

export function nestedInputObjectMissingFieldErrorMessage(
argumentName: string,
fieldCoords: string,
keyFields: string,
entityType: string,
inputType: string,
missingFieldName: string,
): string {
return (
`Argument "${argumentName}" on field "${fieldCoords}" maps to nested @key "${keyFields}" on entity "${entityType}",` +
` but input type "${inputType}" is missing required key field "${missingFieldName}".`
);
}

export function nonInputArgumentCannotTargetCompositeKeyErrorMessage(
argumentName: string,
fieldCoords: string,
keyFields: string,
entityType: string,
argumentType: string,
): string {
return (
`Argument "${argumentName}" on field "${fieldCoords}" uses @openfed__is(fields: "${keyFields}") targeting composite @key on entity "${entityType}",` +
` but argument type "${argumentType}" does not provide nested fields for each key field.` +
' Use separate arguments or an input object that matches the composite key shape.'
);
}
5 changes: 5 additions & 0 deletions composition/src/errors/types/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,8 @@ export type InvalidEntityReturnTypeErrorParams = {
fieldCoords: string;
returnTypeName: string;
};

export type MaxAgeNotPositiveIntegerErrorParams = {
directiveName: DirectiveName;
value: number | string | null;
};
Loading
Loading