Skip to content
Draft
Show file tree
Hide file tree
Changes from 91 commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
69f5af6
feat(composition): @openfed__entityCache directive
SkArchon Jun 19, 2026
1d693ce
feat(composition): @openfed__cacheInvalidate directive
SkArchon Jun 19, 2026
b52c0a2
feat(composition): @openfed__cachePopulate directive
SkArchon Jun 19, 2026
faca30a
feat(composition): @openfed__requestScoped directive
SkArchon Jun 19, 2026
4c90da9
feat(composition): @openfed__queryCache and @openfed__is directives
SkArchon Jun 19, 2026
4cbbaf7
fix(composition): use OPENFED_-prefixed directive name constants and …
SkArchon Jun 19, 2026
9910b6d
fix(composition): OPENFED_ constants, final directive tests, createSu…
SkArchon Jun 19, 2026
75a5812
fix(composition): OPENFED_ constants, final directive tests, createSu…
SkArchon Jun 19, 2026
ea8c9b7
fix(composition): OPENFED_ constants, final directive tests, createSu…
SkArchon Jun 19, 2026
9d525ba
fix(composition): prettier formatting
SkArchon Jun 19, 2026
7edc194
Merge milinda/entityCache-directive into milinda/cacheInvalidate-dire…
SkArchon Jun 19, 2026
548f32f
Merge milinda/cachePopulate-directive into milinda/requestScoped-dire…
SkArchon Jun 19, 2026
f26b7b6
Merge milinda/cacheInvalidate-directive into milinda/cachePopulate-di…
SkArchon Jun 19, 2026
66242f7
Merge milinda/requestScoped-directive into milinda/queryCache-directive
SkArchon Jun 19, 2026
85ed11a
fix(shared): prettier formatting
SkArchon Jun 19, 2026
3e3dee3
fix(shared): prettier formatting
SkArchon Jun 19, 2026
49f93f1
Merge milinda/cachePopulate-directive into milinda/requestScoped-dire…
SkArchon Jun 19, 2026
3bc62aa
Merge milinda/entityCache-directive into milinda/cacheInvalidate-dire…
SkArchon Jun 19, 2026
dc7e324
Merge milinda/cacheInvalidate-directive into milinda/cachePopulate-di…
SkArchon Jun 19, 2026
ee1ea98
Merge milinda/requestScoped-directive into milinda/queryCache-directive
SkArchon Jun 19, 2026
7be55f0
fix: review comments
SkArchon Jun 19, 2026
ecc8047
fix: review comments
SkArchon Jun 19, 2026
bf76389
fix: review comments
SkArchon Jun 19, 2026
34122a1
Merge branch 'milinda/entityCache-directive' into milinda/cacheInvali…
SkArchon Jun 19, 2026
bcbc740
Merge branch 'milinda/cacheInvalidate-directive' into milinda/cachePo…
SkArchon Jun 19, 2026
09ebecc
Merge branch 'milinda/cachePopulate-directive' into milinda/requestSc…
SkArchon Jun 19, 2026
ea22596
Merge branch 'milinda/requestScoped-directive' into milinda/queryCach…
SkArchon Jun 19, 2026
fe0c991
fix: review comments
SkArchon Jun 19, 2026
32f2f15
fix: review comments
SkArchon Jun 19, 2026
12f3887
fix: review comments
SkArchon Jun 19, 2026
928fee5
fix: review comments
SkArchon Jun 19, 2026
15e6865
fix: review comments
SkArchon Jun 19, 2026
49654b4
Merge branch 'milinda/entityCache-directive' into milinda/cacheInvali…
SkArchon Jun 19, 2026
bb3e8da
fix: review comments
SkArchon Jun 19, 2026
8f00a10
fix: review comments
SkArchon Jun 19, 2026
075bdfd
fix: review comments
SkArchon Jun 19, 2026
f133458
fix: review comments
SkArchon Jun 19, 2026
03d8bcb
Merge branch 'milinda/entityCache-directive' into milinda/cacheInvali…
SkArchon Jun 19, 2026
86deba3
fix: review comments
SkArchon Jun 19, 2026
b4b8735
fix: review comments
SkArchon Jun 20, 2026
ceb9df3
fix: review comments
SkArchon Jun 20, 2026
b92c1c3
fix: review comments
SkArchon Jun 20, 2026
ecb7cc8
fix: review comments
SkArchon Jun 20, 2026
268a744
fix: review comments
SkArchon Jun 20, 2026
ff28d82
fix: review comments
SkArchon Jun 21, 2026
4f510c7
fix: review comments
SkArchon Jun 21, 2026
abecdca
Merge branch 'milinda/entityCache-directive' into milinda/cacheInvali…
SkArchon Jun 21, 2026
85daf6c
fix: review comments
SkArchon Jun 21, 2026
9c083e1
fix: review comments
SkArchon Jun 21, 2026
bbaba62
fix: review comments
SkArchon Jun 21, 2026
de95de0
fix: review comments
SkArchon Jun 21, 2026
8ae9643
Merge branch 'milinda/cacheInvalidate-directive' into milinda/cachePo…
SkArchon Jun 22, 2026
5d6ba2a
fix: review comments
SkArchon Jun 22, 2026
574a260
fix: review comments
SkArchon Jun 22, 2026
206c769
fix: review comments
SkArchon Jun 22, 2026
f21a2a2
Merge branch 'main' into milinda/entityCache-directive
SkArchon Jun 22, 2026
9da7e1b
Merge branch 'milinda/entityCache-directive' into milinda/cacheInvali…
SkArchon Jun 22, 2026
3ad1f49
fix: move to upsertObject
SkArchon Jun 22, 2026
c6fb320
Merge branch 'milinda/entityCache-directive' into milinda/cacheInvali…
SkArchon Jun 22, 2026
53e7a3d
fix: cleanup post merge
SkArchon Jun 22, 2026
2332597
fix: review comments
SkArchon Jun 22, 2026
1aeb2d7
Merge branch 'main' into milinda/cacheInvalidate-directive
SkArchon Jun 22, 2026
16d2a87
fix: review comments
SkArchon Jun 22, 2026
d9a9a13
chore: handle repeated directive
Aenimus Jun 22, 2026
12907be
fix: review comments
SkArchon Jun 22, 2026
345968c
fix: review comments
SkArchon Jun 22, 2026
44cab48
Merge branch 'milinda/cacheInvalidate-directive' into milinda/cachePo…
SkArchon Jun 22, 2026
af8c707
fix: merge changes
SkArchon Jun 22, 2026
18319d6
Merge remote-tracking branch 'origin/main' into milinda/cachePopulate…
SkArchon Jun 22, 2026
da93894
fix: review comments
SkArchon Jun 22, 2026
ccd61b7
fix: review comments
SkArchon Jun 22, 2026
e48fef3
Merge branch 'milinda/cachePopulate-directive' into milinda/requestSc…
SkArchon Jun 22, 2026
9058b72
Merge branch 'milinda/requestScoped-directive' into milinda/queryCach…
SkArchon Jun 22, 2026
a46e874
fix: review comments
SkArchon Jun 22, 2026
dc74e32
fix: review comments
SkArchon Jun 22, 2026
d2aa192
fix: review comments
SkArchon Jun 22, 2026
89af114
fix: review comments
SkArchon Jun 22, 2026
7cc280b
fix: review comments
SkArchon Jun 22, 2026
93b5cbc
fix: review comments
SkArchon Jun 22, 2026
cd52465
Merge branch 'milinda/requestScoped-directive' into milinda/queryCach…
SkArchon Jun 22, 2026
e137a0f
fix: review comments
SkArchon Jun 22, 2026
ba1f054
fix: review comments
SkArchon Jun 22, 2026
43e7743
chore: refactor
Aenimus Jun 22, 2026
64414b8
chore: remove unnecessary comment
Aenimus Jun 22, 2026
acd81a5
chore: respond to PR feedback
Aenimus Jun 22, 2026
fe4e903
Merge branch 'main' into milinda/cachePopulate-directive
Aenimus Jun 22, 2026
64613c4
Merge remote-tracking branch 'origin/milinda/cachePopulate-directive'…
SkArchon Jun 22, 2026
840f24d
Merge remote-tracking branch 'origin/main' into milinda/requestScoped…
SkArchon Jun 22, 2026
b69cce2
Merge branch 'milinda/requestScoped-directive' into milinda/queryCach…
SkArchon Jun 23, 2026
fc06034
fix: updates
SkArchon Jun 23, 2026
efa4cdf
chore: remove requestScoped from queryCache branch (queryCache become…
SkArchon Jun 23, 2026
b4effbd
refactor: revert maxAgeNotPositiveIntegerErrorMessage to positional v…
SkArchon Jun 23, 2026
30c81e0
fix: refactoring
SkArchon Jun 23, 2026
1fbcf23
chore: commit progress state
Aenimus Jun 25, 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
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,11 @@ import {
OPENFED_CACHE_POPULATE,
OPENFED_ENTITY_CACHE,
INCLUDE_HEADERS,
OPENFED_IS,
MAX_AGE,
NEGATIVE_CACHE_TTL,
PARTIAL_CACHE_LOAD,
OPENFED_QUERY_CACHE,
SHADOW_MODE,
} from '../utils/string-constants';
import {
Expand Down Expand Up @@ -123,6 +125,8 @@ import {
OPENFED_CACHE_INVALIDATE_DEFINITION,
OPENFED_CACHE_POPULATE_DEFINITION,
OPENFED_ENTITY_CACHE_DEFINITION,
OPENFED_IS_DEFINITION,
OPENFED_QUERY_CACHE_DEFINITION,
SPECIFIED_BY_DEFINITION,
SUBSCRIPTION_FILTER_DEFINITION,
TAG_DEFINITION,
Expand Down Expand Up @@ -1045,3 +1049,60 @@ export const CACHE_POPULATE_DEFINITION_DATA = newDirectiveDefinitionData({
node: OPENFED_CACHE_POPULATE_DEFINITION,
optionalArgumentNames: new Set<ArgumentName>([MAX_AGE]),
});

export const QUERY_CACHE_DEFINITION_DATA = newDirectiveDefinitionData({
argumentDataByName: new Map<ArgumentName, DirectiveArgumentData>([
[
MAX_AGE,
newDirectiveArgumentData({
directive: `@${OPENFED_QUERY_CACHE}`,
name: MAX_AGE,
namedTypeKind: Kind.SCALAR_TYPE_DEFINITION,
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_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