@wundergraph/composition 0.59.0 → 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 (36) 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 +8 -1
  5. package/dist/errors/errors.js +32 -0
  6. package/dist/errors/errors.js.map +1 -1
  7. package/dist/errors/types/params.d.ts +4 -0
  8. package/dist/router-configuration/types.d.ts +26 -0
  9. package/dist/router-configuration/utils.d.ts +2 -1
  10. package/dist/router-configuration/utils.js +11 -0
  11. package/dist/router-configuration/utils.js.map +1 -1
  12. package/dist/schema-building/utils.js +9 -10
  13. package/dist/schema-building/utils.js.map +1 -1
  14. package/dist/tsconfig.tsbuildinfo +1 -1
  15. package/dist/utils/composition-version.js +1 -1
  16. package/dist/utils/string-constants.d.ts +8 -0
  17. package/dist/utils/string-constants.js +12 -4
  18. package/dist/utils/string-constants.js.map +1 -1
  19. package/dist/v1/constants/constants.js +3 -0
  20. package/dist/v1/constants/constants.js.map +1 -1
  21. package/dist/v1/constants/directive-definitions.d.ts +3 -0
  22. package/dist/v1/constants/directive-definitions.js +60 -1
  23. package/dist/v1/constants/directive-definitions.js.map +1 -1
  24. package/dist/v1/constants/type-nodes.d.ts +3 -1
  25. package/dist/v1/constants/type-nodes.js +6 -1
  26. package/dist/v1/constants/type-nodes.js.map +1 -1
  27. package/dist/v1/federation/federation-factory.d.ts +2 -2
  28. package/dist/v1/federation/federation-factory.js +62 -55
  29. package/dist/v1/federation/federation-factory.js.map +1 -1
  30. package/dist/v1/normalization/normalization-factory.d.ts +7 -2
  31. package/dist/v1/normalization/normalization-factory.js +199 -4
  32. package/dist/v1/normalization/normalization-factory.js.map +1 -1
  33. package/dist/v1/normalization/types/types.d.ts +73 -1
  34. package/dist/v1/normalization/utils.js +9 -1
  35. package/dist/v1/normalization/utils.js.map +1 -1
  36. 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
  }
@@ -2956,6 +2963,179 @@ class NormalizationFactory {
2956
2963
  }
2957
2964
  }
2958
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
+ }
2959
3139
  addFieldNamesToConfigurationData(fieldDataByFieldName, configurationData) {
2960
3140
  const externalFieldNames = new Set();
2961
3141
  for (const [fieldName, fieldData] of fieldDataByFieldName) {
@@ -3013,6 +3193,18 @@ class NormalizationFactory {
3013
3193
  }
3014
3194
  definitions.push(...dependencies);
3015
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
+ }
3016
3208
  normalize(document) {
3017
3209
  // Collect any renamed root types
3018
3210
  (0, walkers_1.upsertDirectiveSchemaAndEntityDefinitions)(this, document);
@@ -3119,7 +3311,10 @@ class NormalizationFactory {
3119
3311
  }
3120
3312
  const externalInterfaceFieldNames = [];
3121
3313
  for (const [fieldName, fieldData] of parentData.fieldDataByName) {
3122
- 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) {
3123
3318
  externalInterfaceFieldNames.push(fieldName);
3124
3319
  }
3125
3320
  // Arguments can only be fully validated once all parents types are known