@wundergraph/composition 0.58.3 → 0.62.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/directive-definition-data/directive-definition-data.d.ts +3 -0
  2. package/dist/directive-definition-data/directive-definition-data.js +82 -1
  3. package/dist/directive-definition-data/directive-definition-data.js.map +1 -1
  4. package/dist/errors/errors.d.ts +26 -12
  5. package/dist/errors/errors.js +96 -29
  6. package/dist/errors/errors.js.map +1 -1
  7. package/dist/errors/types/params.d.ts +4 -0
  8. package/dist/federation/types/results.d.ts +6 -0
  9. package/dist/federation/types/results.js +3 -0
  10. package/dist/federation/types/results.js.map +1 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/router-configuration/types.d.ts +26 -0
  15. package/dist/router-configuration/utils.d.ts +2 -1
  16. package/dist/router-configuration/utils.js +11 -0
  17. package/dist/router-configuration/utils.js.map +1 -1
  18. package/dist/schema-building/utils.js +9 -10
  19. package/dist/schema-building/utils.js.map +1 -1
  20. package/dist/tsconfig.tsbuildinfo +1 -1
  21. package/dist/utils/composition-version.js +1 -1
  22. package/dist/utils/string-constants.d.ts +8 -0
  23. package/dist/utils/string-constants.js +12 -4
  24. package/dist/utils/string-constants.js.map +1 -1
  25. package/dist/v1/constants/constants.js +3 -0
  26. package/dist/v1/constants/constants.js.map +1 -1
  27. package/dist/v1/constants/directive-definitions.d.ts +3 -0
  28. package/dist/v1/constants/directive-definitions.js +60 -1
  29. package/dist/v1/constants/directive-definitions.js.map +1 -1
  30. package/dist/v1/constants/type-nodes.d.ts +3 -1
  31. package/dist/v1/constants/type-nodes.js +6 -1
  32. package/dist/v1/constants/type-nodes.js.map +1 -1
  33. package/dist/v1/federation/federation-factory.d.ts +9 -4
  34. package/dist/v1/federation/federation-factory.js +220 -98
  35. package/dist/v1/federation/federation-factory.js.map +1 -1
  36. package/dist/v1/federation/types/params.d.ts +15 -1
  37. package/dist/v1/normalization/normalization-factory.d.ts +7 -2
  38. package/dist/v1/normalization/normalization-factory.js +293 -15
  39. package/dist/v1/normalization/normalization-factory.js.map +1 -1
  40. package/dist/v1/normalization/types/types.d.ts +73 -1
  41. package/dist/v1/normalization/utils.js +9 -1
  42. package/dist/v1/normalization/utils.js.map +1 -1
  43. package/package.json +2 -2
@@ -60,6 +60,11 @@ class NormalizationFactory {
60
60
  directiveDefinitionDataByName = (0, utils_2.initializeDirectiveDefinitionDatas)();
61
61
  doesParentRequireFetchReasons = false;
62
62
  edfsDirectiveReferences = new Set();
63
+ /* Cached entity configs keyed by type name, populated by extractEntityCacheDirective() from
64
+ * @openfed__entityCache. Future caching directives (@openfed__queryCache etc.) use this as a lookup
65
+ * to verify a field's return type is a cached entity.
66
+ */
67
+ entityCacheConfigByTypeName = new Map();
63
68
  errors = new Array();
64
69
  entityDataByTypeName = new Map();
65
70
  entityInterfaceDataByTypeName = new Map();
@@ -888,13 +893,11 @@ class NormalizationFactory {
888
893
  this.updateCompositeOutputDataByNode(node, parentData, extensionType);
889
894
  if (!directivesByName.has(string_constants_1.INTERFACE_OBJECT)) {
890
895
  this.addConcreteTypeNamesForImplementedInterfaces(parentData.implementedInterfaceTypeNames, typeName);
896
+ this.extractEntityCacheDirective(parentData);
891
897
  }
892
898
  return;
893
899
  }
894
900
  const implementedInterfaceTypeNames = this.extractImplementedInterfaceTypeNames(node, new Set());
895
- if (!directivesByName.has(string_constants_1.INTERFACE_OBJECT)) {
896
- this.addConcreteTypeNamesForImplementedInterfaces(implementedInterfaceTypeNames, typeName);
897
- }
898
901
  const newParentData = {
899
902
  configureDescriptionDataBySubgraphName: new Map(),
900
903
  directivesByName: directivesByName,
@@ -913,6 +916,10 @@ class NormalizationFactory {
913
916
  subgraphNames: new Set([this.subgraphName]),
914
917
  description: (0, utils_1.formatDescription)('description' in node ? node.description : undefined),
915
918
  };
919
+ if (!directivesByName.has(string_constants_1.INTERFACE_OBJECT)) {
920
+ this.addConcreteTypeNamesForImplementedInterfaces(implementedInterfaceTypeNames, typeName);
921
+ this.extractEntityCacheDirective(newParentData);
922
+ }
916
923
  this.extractConfigureDescriptionsData(newParentData);
917
924
  this.parentDefinitionDataByTypeName.set(typeName, newParentData);
918
925
  }
@@ -1801,6 +1808,12 @@ class NormalizationFactory {
1801
1808
  sizedFields: [],
1802
1809
  requireOneSlicingArgument: true, // per IBM cost spec
1803
1810
  };
1811
+ /**
1812
+ * For each accepted slicing argument, capture the full resolved chain:
1813
+ * (argument + each intermediate input field + leaf).
1814
+ * Later we check the assumedSize against the default value of every element of the chain.
1815
+ */
1816
+ const slicingArgChainByPath = new Map();
1804
1817
  for (const argumentNode of args) {
1805
1818
  const argumentName = argumentNode.name.value;
1806
1819
  // type validation handled upstream
@@ -1830,18 +1843,61 @@ class NormalizationFactory {
1830
1843
  if (valueNode.kind !== graphql_1.Kind.STRING) {
1831
1844
  continue;
1832
1845
  }
1833
- const slicingArgName = valueNode.value;
1834
- const argData = data.argumentDataByName.get(slicingArgName);
1846
+ // slicingArgPath could be just an argument "inputA" or a dot-path "inputA.page.first".
1847
+ const slicingArgPath = valueNode.value;
1848
+ const segments = slicingArgPath.split(string_constants_1.LITERAL_PERIOD);
1849
+ // Reject empty segments (e.g. "", "a.", ".b", "a..b").
1850
+ if (segments.length === 0 || segments.some((s) => s.length === 0)) {
1851
+ errorMessages.push((0, errors_1.listSizeSlicingArgumentMalformedPathErrorMessage)(directiveCoords, slicingArgPath));
1852
+ continue;
1853
+ }
1854
+ // First segment should be valid argument name:
1855
+ const firstSegment = segments[0];
1856
+ const argData = data.argumentDataByName.get(firstSegment);
1835
1857
  if (!argData) {
1836
- errorMessages.push((0, errors_1.listSizeInvalidSlicingArgumentErrorMessage)(directiveCoords, slicingArgName));
1858
+ errorMessages.push((0, errors_1.listSizeInvalidSlicingArgumentErrorMessage)(directiveCoords, slicingArgPath));
1859
+ continue;
1860
+ }
1861
+ /**
1862
+ * Walk the rest of the path in the slicingArgument.
1863
+ * `current` tracks the input value reached so far:
1864
+ * the argument itself for a flat path, or the nested input field for each step of a
1865
+ * dot-path. After the loop it is the leaf, which must be Int/Int!.
1866
+ */
1867
+ let current = argData;
1868
+ const chain = [argData];
1869
+ let isPathInvalid = false;
1870
+ for (let i = 1; i < segments.length; i++) {
1871
+ // Non-leaf step: `current` must unwrap to an Input Object. Lists are rejected here.
1872
+ const unwrapped = current.type.kind === graphql_1.Kind.NON_NULL_TYPE ? current.type.type : current.type;
1873
+ const parentTypeData = this.parentDefinitionDataByTypeName.get(current.namedTypeName);
1874
+ if (unwrapped.kind === graphql_1.Kind.LIST_TYPE ||
1875
+ !parentTypeData ||
1876
+ parentTypeData.kind !== graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION) {
1877
+ errorMessages.push((0, errors_1.listSizeSlicingArgumentSegmentNotInputObjectErrorMessage)(directiveCoords, slicingArgPath, current.name, (0, merge_1.printTypeNode)(current.type)));
1878
+ isPathInvalid = true;
1879
+ break;
1880
+ }
1881
+ const inputValue = parentTypeData.inputValueDataByName.get(segments[i]);
1882
+ if (!inputValue) {
1883
+ errorMessages.push((0, errors_1.listSizeSlicingArgumentSegmentNotFoundErrorMessage)(directiveCoords, slicingArgPath, segments[i], current.namedTypeName));
1884
+ isPathInvalid = true;
1885
+ break;
1886
+ }
1887
+ current = inputValue;
1888
+ chain.push(inputValue);
1889
+ }
1890
+ if (isPathInvalid) {
1837
1891
  continue;
1838
1892
  }
1839
- const unwrappedType = argData.type.kind === graphql_1.Kind.NON_NULL_TYPE ? argData.type.type : argData.type;
1840
- if (unwrappedType.kind === graphql_1.Kind.LIST_TYPE || argData.namedTypeName !== string_constants_1.INT_SCALAR) {
1841
- errorMessages.push((0, errors_1.listSizeSlicingArgumentNotIntErrorMessage)(directiveCoords, slicingArgName, (0, merge_1.printTypeNode)(argData.type)));
1893
+ // Leaf check: the final value must be Int or Int!
1894
+ const unwrappedType = current.type.kind === graphql_1.Kind.NON_NULL_TYPE ? current.type.type : current.type;
1895
+ if (unwrappedType.kind === graphql_1.Kind.LIST_TYPE || current.namedTypeName !== string_constants_1.INT_SCALAR) {
1896
+ errorMessages.push((0, errors_1.listSizeSlicingArgumentNotIntErrorMessage)(directiveCoords, slicingArgPath, (0, merge_1.printTypeNode)(current.type)));
1842
1897
  continue;
1843
1898
  }
1844
- listSizeConfig.slicingArguments.push(slicingArgName);
1899
+ listSizeConfig.slicingArguments.push(slicingArgPath);
1900
+ slicingArgChainByPath.set(slicingArgPath, chain);
1845
1901
  }
1846
1902
  break;
1847
1903
  }
@@ -1897,10 +1953,44 @@ class NormalizationFactory {
1897
1953
  errorMessages.push((0, errors_1.listSizeAssumedSizeWithRequiredSlicingArgumentErrorMessage)(directiveCoords));
1898
1954
  }
1899
1955
  else {
1900
- for (const slicingArgName of listSizeConfig.slicingArguments) {
1901
- const argData = data.argumentDataByName.get(slicingArgName);
1902
- if (argData?.defaultValue) {
1903
- errorMessages.push((0, errors_1.listSizeAssumedSizeSlicingArgDefaultErrorMessage)(directiveCoords, slicingArgName));
1956
+ /**
1957
+ * When assumedSize is set together with slicingArguments, the slicing argument must not have
1958
+ * a default leaf value in any element of the slicingArgument's chain.
1959
+ * That will be checked for all slicing Arguments.
1960
+ * For example, if the query is defined like this:
1961
+ * search(a: SearchInput): [Book]
1962
+ * @listSize(assumedSize: 50, slicingArguments: ["a.b.c"], requireOneSlicingArgument: false)
1963
+ * any of those defaults are forbidden:
1964
+ * input SearchInput { b: PaginationInput = { c: 10 } }
1965
+ * input PaginationInput { c: Int = 10 }
1966
+ * Notably, this query definition is also forbidden with the @listSize above:
1967
+ * search(a: SearchInput = { b: { c: 10 } }): [Book]
1968
+ * A default along the chain only matters if it actually supplies a value for the leaf:
1969
+ * walk the remaining tail through the default's AST and reject only when the walk resolves.
1970
+ * E.g. `a: SearchInput = { b: {} }` is allowed, and the leaf stays undefined.
1971
+ */
1972
+ for (const slicingArgPath of listSizeConfig.slicingArguments) {
1973
+ const chain = slicingArgChainByPath.get(slicingArgPath);
1974
+ if (!chain) {
1975
+ continue;
1976
+ }
1977
+ const segments = slicingArgPath.split(string_constants_1.LITERAL_PERIOD);
1978
+ // Outer loop: which default are we starting from?
1979
+ for (let i = 0; i < chain.length; i++) {
1980
+ let value = chain[i].defaultValue;
1981
+ // Inner loop: try to find a defined leaf in the chain:
1982
+ for (let j = i + 1; j < segments.length; j++) {
1983
+ if (!value || value.kind !== graphql_1.Kind.OBJECT) {
1984
+ value = undefined;
1985
+ break;
1986
+ }
1987
+ const field = value.fields.find((f) => f.name.value === segments[j]);
1988
+ value = field?.value;
1989
+ }
1990
+ if (value !== undefined) {
1991
+ errorMessages.push((0, errors_1.listSizeAssumedSizeSlicingArgDefaultErrorMessage)(directiveCoords, slicingArgPath));
1992
+ break;
1993
+ }
1904
1994
  }
1905
1995
  }
1906
1996
  }
@@ -2873,6 +2963,179 @@ class NormalizationFactory {
2873
2963
  }
2874
2964
  }
2875
2965
  }
2966
+ extractEntityCacheDirective({ directivesByName, name: typeName }) {
2967
+ const entityCacheDirectives = directivesByName.get(string_constants_1.OPENFED_ENTITY_CACHE);
2968
+ if (!entityCacheDirectives || entityCacheDirectives.length === 0) {
2969
+ return;
2970
+ }
2971
+ if (!this.keyFieldSetDatasByTypeName.has(typeName)) {
2972
+ this.errors.push((0, errors_1.invalidDirectiveError)(string_constants_1.OPENFED_ENTITY_CACHE, typeName, string_constants_1.FIRST_ORDINAL, [
2973
+ (0, errors_1.entityCacheWithoutKeyErrorMessage)(typeName),
2974
+ ]));
2975
+ return;
2976
+ }
2977
+ /* validateDirectives() (run earlier in normalize()) has already guaranteed each argument's type —
2978
+ * Int for maxAge/negativeCacheTTL, Boolean for the flags — so the generic ConstDirectiveNode is
2979
+ * narrowed once to the precise typed node, mirroring RequestScopedDirectiveNode/ComposeDirectiveNode.
2980
+ * Optional arguments may be absent (definition defaults are not materialized onto the usage AST),
2981
+ * so the config starts at the directive's documented defaults and each present argument overrides it.
2982
+ */
2983
+ const directive = entityCacheDirectives[0];
2984
+ if (this.entityCacheConfigByTypeName.has(typeName)) {
2985
+ // The error would be caught in directive validation as an invalid repeated directive use.
2986
+ return;
2987
+ }
2988
+ const config = {
2989
+ typeName,
2990
+ maxAgeSeconds: 0,
2991
+ notFoundCacheTtlSeconds: 0,
2992
+ includeHeaders: false,
2993
+ partialCacheLoad: false,
2994
+ shadowMode: false,
2995
+ };
2996
+ for (const arg of directive.arguments) {
2997
+ if (!arg) {
2998
+ continue;
2999
+ }
3000
+ const { name, value } = arg;
3001
+ switch (value.kind) {
3002
+ case graphql_1.Kind.INT: {
3003
+ switch (name.value) {
3004
+ case string_constants_1.MAX_AGE: {
3005
+ config.maxAgeSeconds = parseInt(value.value, 10);
3006
+ break;
3007
+ }
3008
+ case string_constants_1.NEGATIVE_CACHE_TTL: {
3009
+ config.notFoundCacheTtlSeconds = parseInt(value.value, 10);
3010
+ break;
3011
+ }
3012
+ }
3013
+ break;
3014
+ }
3015
+ default: {
3016
+ switch (name.value) {
3017
+ case string_constants_1.INCLUDE_HEADERS: {
3018
+ config.includeHeaders = value.value;
3019
+ break;
3020
+ }
3021
+ case string_constants_1.PARTIAL_CACHE_LOAD: {
3022
+ config.partialCacheLoad = value.value;
3023
+ break;
3024
+ }
3025
+ case string_constants_1.SHADOW_MODE: {
3026
+ config.shadowMode = value.value;
3027
+ break;
3028
+ }
3029
+ }
3030
+ break;
3031
+ }
3032
+ }
3033
+ }
3034
+ const entityCacheErrors = [];
3035
+ if (config.maxAgeSeconds <= 0) {
3036
+ entityCacheErrors.push((0, errors_1.invalidDirectiveError)(string_constants_1.OPENFED_ENTITY_CACHE, typeName, string_constants_1.FIRST_ORDINAL, [
3037
+ (0, errors_1.maxAgeNotPositiveIntegerErrorMessage)(config.maxAgeSeconds),
3038
+ ]));
3039
+ }
3040
+ if (config.notFoundCacheTtlSeconds < 0) {
3041
+ entityCacheErrors.push((0, errors_1.invalidDirectiveError)(string_constants_1.OPENFED_ENTITY_CACHE, typeName, string_constants_1.FIRST_ORDINAL, [
3042
+ (0, errors_1.negativeCacheTTLNotNonNegativeIntegerErrorMessage)(config.notFoundCacheTtlSeconds),
3043
+ ]));
3044
+ }
3045
+ if (entityCacheErrors.length > 0) {
3046
+ this.errors.push(...entityCacheErrors);
3047
+ return;
3048
+ }
3049
+ this.entityCacheConfigByTypeName.set(typeName, config);
3050
+ const configurationData = (0, utils_5.getValueOrDefault)(this.configurationDataByTypeName, typeName, () => (0, utils_6.newConfigurationData)(true, typeName));
3051
+ (0, utils_6.getOrInitializeEntityCaching)(configurationData).entityCacheConfigurations.push(config);
3052
+ }
3053
+ // Returns true if the directive is defined and false otherwise
3054
+ extractCacheInvalidateConfig(fieldData) {
3055
+ if (!fieldData.directivesByName.has(string_constants_1.OPENFED_CACHE_INVALIDATE)) {
3056
+ return false;
3057
+ }
3058
+ const operationType = this.getOperationTypeNodeForRootTypeName(fieldData.originalParentTypeName);
3059
+ const fieldCoords = `${fieldData.originalParentTypeName}.${fieldData.name}`;
3060
+ if (!operationType || operationType === graphql_1.OperationTypeNode.QUERY) {
3061
+ this.errors.push((0, errors_1.invalidDirectiveError)(string_constants_1.OPENFED_CACHE_INVALIDATE, fieldCoords, string_constants_1.FIRST_ORDINAL, [
3062
+ (0, errors_1.invalidMutationOrSubscriptionFieldCoordsErrorMessage)(fieldCoords),
3063
+ ]));
3064
+ return true;
3065
+ }
3066
+ const returnTypeName = (0, ast_1.getTypeNodeNamedTypeName)(fieldData.node.type);
3067
+ if (!this.entityCacheConfigByTypeName.has(returnTypeName)) {
3068
+ this.errors.push((0, errors_1.invalidDirectiveError)(string_constants_1.OPENFED_CACHE_INVALIDATE, fieldCoords, string_constants_1.FIRST_ORDINAL, [
3069
+ (0, errors_1.invalidEntityReturnTypeErrorMessage)({ fieldCoords, returnTypeName: returnTypeName }),
3070
+ ]));
3071
+ return true;
3072
+ }
3073
+ const configurationData = (0, utils_5.getValueOrDefault)(this.configurationDataByTypeName, fieldData.renamedParentTypeName, () => (0, utils_6.newConfigurationData)(false, fieldData.renamedParentTypeName));
3074
+ const config = {
3075
+ entityTypeName: returnTypeName,
3076
+ fieldName: fieldData.name,
3077
+ operationType: operationType,
3078
+ };
3079
+ (0, utils_6.getOrInitializeEntityCaching)(configurationData).cacheInvalidateConfigurations.push(config);
3080
+ return true;
3081
+ }
3082
+ /* Extracts @openfed__cachePopulate from Mutation/Subscription fields.
3083
+ * The return type must be a cached entity (@key & @openfed__entityCache).
3084
+ * Argument `maxAge` is optional; if absent, the router falls back to the entity's @openfed__entityCache TTL;
3085
+ * if provided, must be a positive integer.
3086
+ *
3087
+ * Returns true if the directive is defined and false otherwise
3088
+ */
3089
+ extractCachePopulateConfig(fieldData) {
3090
+ if (!fieldData.directivesByName.has(string_constants_1.OPENFED_CACHE_POPULATE)) {
3091
+ return false;
3092
+ }
3093
+ const fieldCoords = `${fieldData.originalParentTypeName}.${fieldData.name}`;
3094
+ const operationType = this.getOperationTypeNodeForRootTypeName(fieldData.originalParentTypeName);
3095
+ if (!operationType || operationType === graphql_1.OperationTypeNode.QUERY) {
3096
+ this.errors.push((0, errors_1.invalidDirectiveError)(string_constants_1.OPENFED_CACHE_POPULATE, fieldCoords, string_constants_1.FIRST_ORDINAL, [
3097
+ (0, errors_1.invalidMutationOrSubscriptionFieldCoordsErrorMessage)(fieldCoords),
3098
+ ]));
3099
+ return true;
3100
+ }
3101
+ const returnTypeName = (0, ast_1.getTypeNodeNamedTypeName)(fieldData.node.type);
3102
+ const entityCacheConfig = this.entityCacheConfigByTypeName.get(returnTypeName);
3103
+ if (!entityCacheConfig) {
3104
+ this.errors.push((0, errors_1.invalidDirectiveError)(string_constants_1.OPENFED_CACHE_POPULATE, fieldCoords, string_constants_1.FIRST_ORDINAL, [
3105
+ (0, errors_1.invalidEntityReturnTypeErrorMessage)({ fieldCoords, returnTypeName }),
3106
+ ]));
3107
+ return true;
3108
+ }
3109
+ /* validateDirectives() has already guaranteed maxAge is an Int when present, so the generic
3110
+ * ConstDirectiveNode is narrowed once to the precise typed node — mirroring the other caching
3111
+ * directives. maxAge is the only argument and is optional.
3112
+ */
3113
+ const cachePopulateDirective = fieldData.directivesByName.get(string_constants_1.OPENFED_CACHE_POPULATE)[0];
3114
+ const config = {
3115
+ entityTypeName: returnTypeName,
3116
+ fieldName: fieldData.name,
3117
+ operationType,
3118
+ maxAgeSeconds: entityCacheConfig.maxAgeSeconds, // Set fallback immediately; overridden where appropriate.
3119
+ };
3120
+ const maxAgeArgument = cachePopulateDirective.arguments[0];
3121
+ if (!maxAgeArgument) {
3122
+ const configurationData = (0, utils_5.getValueOrDefault)(this.configurationDataByTypeName, fieldData.renamedParentTypeName, () => (0, utils_6.newConfigurationData)(false, fieldData.renamedParentTypeName));
3123
+ (0, utils_6.getOrInitializeEntityCaching)(configurationData).cachePopulateConfigurations.push(config);
3124
+ return true;
3125
+ }
3126
+ config.maxAgeSeconds = parseInt(maxAgeArgument.value.value, 10);
3127
+ // This syntax handles possible NaN.
3128
+ if (isNaN(config.maxAgeSeconds) || config.maxAgeSeconds <= 0) {
3129
+ this.errors.push((0, errors_1.invalidDirectiveError)(string_constants_1.OPENFED_CACHE_POPULATE, fieldCoords, string_constants_1.FIRST_ORDINAL, [
3130
+ // If null is explicitly provided in GraphQL the value in JS is undefined.
3131
+ (0, errors_1.maxAgeNotPositiveIntegerErrorMessage)(maxAgeArgument.value.value ?? null),
3132
+ ]));
3133
+ return true;
3134
+ }
3135
+ const configurationData = (0, utils_5.getValueOrDefault)(this.configurationDataByTypeName, fieldData.renamedParentTypeName, () => (0, utils_6.newConfigurationData)(false, fieldData.renamedParentTypeName));
3136
+ (0, utils_6.getOrInitializeEntityCaching)(configurationData).cachePopulateConfigurations.push(config);
3137
+ return true;
3138
+ }
2876
3139
  addFieldNamesToConfigurationData(fieldDataByFieldName, configurationData) {
2877
3140
  const externalFieldNames = new Set();
2878
3141
  for (const [fieldName, fieldData] of fieldDataByFieldName) {
@@ -2930,6 +3193,18 @@ class NormalizationFactory {
2930
3193
  }
2931
3194
  definitions.push(...dependencies);
2932
3195
  }
3196
+ // Only Object fields should be passed to this method
3197
+ handleFieldCacheDirectives(data) {
3198
+ /* A field can't both evict (@openfed__cacheInvalidate) and write (@openfed__cachePopulate)
3199
+ * the cache for the same entity, so the two directives are mutually exclusive.
3200
+ */
3201
+ const definesInvalidate = this.extractCacheInvalidateConfig(data);
3202
+ const definesPopulate = this.extractCachePopulateConfig(data);
3203
+ if (definesInvalidate && definesPopulate) {
3204
+ const fieldCoords = `${data.originalParentTypeName}.${data.name}`;
3205
+ this.errors.push((0, errors_1.invalidMutuallyExclusiveCacheDirectivesError)(fieldCoords));
3206
+ }
3207
+ }
2933
3208
  normalize(document) {
2934
3209
  // Collect any renamed root types
2935
3210
  (0, walkers_1.upsertDirectiveSchemaAndEntityDefinitions)(this, document);
@@ -3036,7 +3311,10 @@ class NormalizationFactory {
3036
3311
  }
3037
3312
  const externalInterfaceFieldNames = [];
3038
3313
  for (const [fieldName, fieldData] of parentData.fieldDataByName) {
3039
- if (!isObject && fieldData.externalFieldDataBySubgraphName.get(this.subgraphName)?.isDefinedExternal) {
3314
+ if (isObject) {
3315
+ this.handleFieldCacheDirectives(fieldData);
3316
+ }
3317
+ else if (fieldData.externalFieldDataBySubgraphName.get(this.subgraphName)?.isDefinedExternal) {
3040
3318
  externalInterfaceFieldNames.push(fieldName);
3041
3319
  }
3042
3320
  // Arguments can only be fully validated once all parents types are known