elasticgraph-schema_definition 0.18.0.0

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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +7 -0
  4. data/elasticgraph-schema_definition.gemspec +26 -0
  5. data/lib/elastic_graph/schema_definition/api.rb +359 -0
  6. data/lib/elastic_graph/schema_definition/factory.rb +506 -0
  7. data/lib/elastic_graph/schema_definition/indexing/derived_fields/append_only_set.rb +79 -0
  8. data/lib/elastic_graph/schema_definition/indexing/derived_fields/field_initializer_support.rb +59 -0
  9. data/lib/elastic_graph/schema_definition/indexing/derived_fields/immutable_value.rb +99 -0
  10. data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +62 -0
  11. data/lib/elastic_graph/schema_definition/indexing/derived_indexed_type.rb +346 -0
  12. data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +74 -0
  13. data/lib/elastic_graph/schema_definition/indexing/field.rb +181 -0
  14. data/lib/elastic_graph/schema_definition/indexing/field_reference.rb +51 -0
  15. data/lib/elastic_graph/schema_definition/indexing/field_type/enum.rb +65 -0
  16. data/lib/elastic_graph/schema_definition/indexing/field_type/object.rb +113 -0
  17. data/lib/elastic_graph/schema_definition/indexing/field_type/scalar.rb +51 -0
  18. data/lib/elastic_graph/schema_definition/indexing/field_type/union.rb +70 -0
  19. data/lib/elastic_graph/schema_definition/indexing/index.rb +318 -0
  20. data/lib/elastic_graph/schema_definition/indexing/json_schema_field_metadata.rb +34 -0
  21. data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +234 -0
  22. data/lib/elastic_graph/schema_definition/indexing/list_counts_mapping.rb +53 -0
  23. data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +96 -0
  24. data/lib/elastic_graph/schema_definition/indexing/rollover_config.rb +25 -0
  25. data/lib/elastic_graph/schema_definition/indexing/update_target_factory.rb +54 -0
  26. data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +195 -0
  27. data/lib/elastic_graph/schema_definition/json_schema_pruner.rb +61 -0
  28. data/lib/elastic_graph/schema_definition/mixins/can_be_graphql_only.rb +31 -0
  29. data/lib/elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations.rb +119 -0
  30. data/lib/elastic_graph/schema_definition/mixins/has_directives.rb +65 -0
  31. data/lib/elastic_graph/schema_definition/mixins/has_documentation.rb +74 -0
  32. data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +281 -0
  33. data/lib/elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect.rb +46 -0
  34. data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +116 -0
  35. data/lib/elastic_graph/schema_definition/mixins/has_type_info.rb +181 -0
  36. data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +122 -0
  37. data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +47 -0
  38. data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +267 -0
  39. data/lib/elastic_graph/schema_definition/mixins/verifies_graphql_name.rb +38 -0
  40. data/lib/elastic_graph/schema_definition/rake_tasks.rb +190 -0
  41. data/lib/elastic_graph/schema_definition/results.rb +404 -0
  42. data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +482 -0
  43. data/lib/elastic_graph/schema_definition/schema_elements/argument.rb +56 -0
  44. data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +1541 -0
  45. data/lib/elastic_graph/schema_definition/schema_elements/deprecated_element.rb +21 -0
  46. data/lib/elastic_graph/schema_definition/schema_elements/directive.rb +40 -0
  47. data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +189 -0
  48. data/lib/elastic_graph/schema_definition/schema_elements/enum_value.rb +73 -0
  49. data/lib/elastic_graph/schema_definition/schema_elements/enum_value_namer.rb +89 -0
  50. data/lib/elastic_graph/schema_definition/schema_elements/enums_for_indexed_types.rb +82 -0
  51. data/lib/elastic_graph/schema_definition/schema_elements/field.rb +1085 -0
  52. data/lib/elastic_graph/schema_definition/schema_elements/field_path.rb +112 -0
  53. data/lib/elastic_graph/schema_definition/schema_elements/field_source.rb +16 -0
  54. data/lib/elastic_graph/schema_definition/schema_elements/graphql_sdl_enumerator.rb +113 -0
  55. data/lib/elastic_graph/schema_definition/schema_elements/input_field.rb +31 -0
  56. data/lib/elastic_graph/schema_definition/schema_elements/input_type.rb +60 -0
  57. data/lib/elastic_graph/schema_definition/schema_elements/interface_type.rb +72 -0
  58. data/lib/elastic_graph/schema_definition/schema_elements/list_counts_state.rb +40 -0
  59. data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +53 -0
  60. data/lib/elastic_graph/schema_definition/schema_elements/relationship.rb +218 -0
  61. data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +310 -0
  62. data/lib/elastic_graph/schema_definition/schema_elements/sort_order_enum_value.rb +36 -0
  63. data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +66 -0
  64. data/lib/elastic_graph/schema_definition/schema_elements/type_namer.rb +237 -0
  65. data/lib/elastic_graph/schema_definition/schema_elements/type_reference.rb +353 -0
  66. data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +579 -0
  67. data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +157 -0
  68. data/lib/elastic_graph/schema_definition/scripting/file_system_repository.rb +77 -0
  69. data/lib/elastic_graph/schema_definition/scripting/script.rb +48 -0
  70. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_day_of_week.painless +24 -0
  71. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_time_of_day.painless +41 -0
  72. data/lib/elastic_graph/schema_definition/scripting/scripts/filter/by_time_of_day.painless +22 -0
  73. data/lib/elastic_graph/schema_definition/scripting/scripts/update/index_data.painless +93 -0
  74. data/lib/elastic_graph/schema_definition/state.rb +212 -0
  75. data/lib/elastic_graph/schema_definition/test_support.rb +113 -0
  76. metadata +513 -0
@@ -0,0 +1,181 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "elastic_graph/json_schema/meta_schema_validator"
10
+
11
+ module ElasticGraph
12
+ module SchemaDefinition
13
+ module Mixins
14
+ # Mixin used to specify non-GraphQL type info (datastore index and JSON schema type info).
15
+ # Exists as a mixin so we can apply the same consistent API to every place we need to use this.
16
+ # Currently it's used in 3 places:
17
+ #
18
+ # - {SchemaElements::ScalarType}: allows specification of how scalars are represented in JSON schema and the index.
19
+ # - {SchemaElements::TypeWithSubfields}: allows customization of how an object type is represented in JSON schema and the index.
20
+ # - {SchemaElements::Field}: allows customization of a specific field over the field type's standard JSON schema and the index mapping.
21
+ module HasTypeInfo
22
+ # @return [Hash<Symbol, Object>] datastore mapping options
23
+ def mapping_options
24
+ @mapping_options ||= {}
25
+ end
26
+
27
+ # @return [Hash<Symbol, Object>] JSON schema options
28
+ def json_schema_options
29
+ @json_schema_options ||= {}
30
+ end
31
+
32
+ # Set of mapping parameters that it makes sense to allow customization of, based on
33
+ # [the Elasticsearch docs](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/mapping-params.html).
34
+ CUSTOMIZABLE_DATASTORE_PARAMS = Set[
35
+ :analyzer,
36
+ :eager_global_ordinals,
37
+ :enabled,
38
+ :fields,
39
+ :format,
40
+ :index,
41
+ :meta, # not actually in the doc above. Added to support some `index_configurator` tests on 7.9+.
42
+ :norms,
43
+ :null_value,
44
+ :search_analyzer,
45
+ :type,
46
+ ]
47
+
48
+ # Defines the Elasticsearch/OpenSearch [field mapping type](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-types.html)
49
+ # and [mapping parameters](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-params.html) for a field or type.
50
+ # The options passed here will be included in the generated `datastore_config.yaml` artifact that ElasticGraph uses to configure
51
+ # Elasticsearch/OpenSearch.
52
+ #
53
+ # Can be called multiple times; each time, the options will be merged into the existing options.
54
+ #
55
+ # This is required on a {SchemaElements::ScalarType}; without it, ElasticGraph would have no way to know how the datatype should be
56
+ # indexed in the datastore.
57
+ #
58
+ # On a {SchemaElements::Field}, this can be used to customize how a field is indexed. For example, `String` fields are normally
59
+ # indexed as [keywords](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/keyword.html); to instead index a `String`
60
+ # field for full text search, you’d need to configure `mapping type: "text"`.
61
+ #
62
+ # On a {SchemaElements::ObjectType}, this can be used to use a specific Elasticsearch/OpenSearch data type for something that is
63
+ # modeled as an object in GraphQL. For example, we use it for the `GeoLocation` type so they get indexed in Elasticsearch using the
64
+ # [geo_point type](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/geo-point.html).
65
+ #
66
+ # @param options [Hash<Symbol, Object>] mapping options--must be limited to {CUSTOMIZABLE_DATASTORE_PARAMS}
67
+ # @return [void]
68
+ #
69
+ # @example Define the mapping of a custom scalar type
70
+ # ElasticGraph.define_schema do |schema|
71
+ # schema.scalar_type "URL" do |t|
72
+ # t.mapping type: "keyword"
73
+ # t.json_schema type: "string", format: "uri"
74
+ # end
75
+ # end
76
+ #
77
+ # @example Customize the mapping of a field
78
+ # ElasticGraph.define_schema do |schema|
79
+ # schema.object_type "Card" do |t|
80
+ # t.field "id", "ID!"
81
+ #
82
+ # t.field "cardholderName", "String" do |f|
83
+ # # index this field for full text search
84
+ # f.mapping type: "text"
85
+ # end
86
+ #
87
+ # t.field "expYear", "Int" do |f|
88
+ # # Use a smaller numeric type to save space in the datastore
89
+ # f.mapping type: "short"
90
+ # f.json_schema minimum: 2000, maximum: 2099
91
+ # end
92
+ #
93
+ # t.field "expMonth", "Int" do |f|
94
+ # # Use a smaller numeric type to save space in the datastore
95
+ # f.mapping type: "byte"
96
+ # f.json_schema minimum: 1, maximum: 12
97
+ # end
98
+ #
99
+ # t.index "cards"
100
+ # end
101
+ # end
102
+ def mapping(**options)
103
+ param_diff = (options.keys.to_set - CUSTOMIZABLE_DATASTORE_PARAMS).to_a
104
+
105
+ unless param_diff.empty?
106
+ raise SchemaError, "Some configured mapping overrides are unsupported: #{param_diff.inspect}"
107
+ end
108
+
109
+ mapping_options.update(options)
110
+ end
111
+
112
+ # Defines the [JSON schema](https://json-schema.org/understanding-json-schema/) validations for this field or type. Validations
113
+ # defined here will be included in the generated `json_schemas.yaml` artifact, which is used by the ElasticGraph indexer to
114
+ # validate events before indexing their data in the datastore. In addition, the publisher may use `json_schemas.yaml` for code
115
+ # generation and to apply validation before publishing an event to ElasticGraph.
116
+ #
117
+ # Can be called multiple times; each time, the options will be merged into the existing options.
118
+ #
119
+ # This is _required_ on a {SchemaElements::ScalarType} (since we don’t know how a custom scalar type should be represented in
120
+ # JSON!). On a {SchemaElements::Field}, this is optional, but can be used to make the JSON schema validation stricter then it
121
+ # would otherwise be. For example, you could use `json_schema maxLength: 30` on a `String` field to limit the length.
122
+ #
123
+ # You can use any of the JSON schema validation keywords here. In addition, `nullable: false` is supported to configure the
124
+ # generated JSON schema to disallow `null` values for the field. Note that if you define a field with a non-nullable GraphQL type
125
+ # (e.g. `Int!`), the JSON schema will automatically disallow nulls. However, as explained in the
126
+ # {SchemaElements::TypeWithSubfields#field} documentation, we generally recommend against defining non-nullable GraphQL fields.
127
+ # `json_schema nullable: false` will disallow `null` values from being indexed, while still keeping the field nullable in the
128
+ # GraphQL schema. If you think you might want to make a field non-nullable in the GraphQL schema some day, it’s a good idea to use
129
+ # `json_schema nullable: false` now to ensure every indexed record has a non-null value for the field.
130
+ #
131
+ # @note We recommend using JSON schema validations in a limited fashion. Validations that are appropriate to apply when data is
132
+ # entering the system-of-record are often not appropriate on a secondary index like ElasticGraph. Events that violate a JSON
133
+ # schema validation will fail to index (typically they will be sent to the dead letter queue and page an oncall engineer). If an
134
+ # ElasticGraph instance is meant to contain all the data of some source system, you probably don’t want it applying stricter
135
+ # validations than the source system itself has. We recommend limiting your JSON schema validations to situations where
136
+ # violations would prevent ElasticGraph from operating correctly.
137
+ #
138
+ # @param options [Hash<Symbol, Object>] JSON schema options
139
+ # @return [void]
140
+ #
141
+ # @example Define the JSON schema validations of a custom scalar type
142
+ # ElasticGraph.define_schema do |schema|
143
+ # schema.scalar_type "URL" do |t|
144
+ # t.mapping type: "keyword"
145
+ #
146
+ # # JSON schema has a built-in URI format validator:
147
+ # # https://json-schema.org/understanding-json-schema/reference/string.html#resource-identifiers
148
+ # t.json_schema type: "string", format: "uri"
149
+ # end
150
+ # end
151
+ #
152
+ # @example Define additional validations on a field
153
+ # ElasticGraph.define_schema do |schema|
154
+ # schema.object_type "Card" do |t|
155
+ # t.field "id", "ID!"
156
+ #
157
+ # t.field "expYear", "Int" do |f|
158
+ # # Use JSON schema to ensure the publisher is sending us 4 digit years, not 2 digit years.
159
+ # f.json_schema minimum: 2000, maximum: 2099
160
+ # end
161
+ #
162
+ # t.field "expMonth", "Int" do |f|
163
+ # f.json_schema minimum: 1, maximum: 12
164
+ # end
165
+ #
166
+ # t.index "cards"
167
+ # end
168
+ # end
169
+ def json_schema(**options)
170
+ validatable_json_schema = Support::HashUtil.stringify_keys(options)
171
+
172
+ if (error_msg = JSONSchema.strict_meta_schema_validator.validate_with_error_message(validatable_json_schema))
173
+ raise SchemaError, "Invalid JSON schema options set on #{self}:\n\n#{error_msg}"
174
+ end
175
+
176
+ json_schema_options.update(options)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,122 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "elastic_graph/error"
10
+
11
+ module ElasticGraph
12
+ module SchemaDefinition
13
+ module Mixins
14
+ # Mixin for types that can implement interfaces ({SchemaElements::ObjectType} and {SchemaElements::InterfaceType}).
15
+ module ImplementsInterfaces
16
+ # Declares that the current type implements the specified interface, making the current type a subtype of the interface. The
17
+ # current type must define all of the fields of the named interface, with the exact same field types.
18
+ #
19
+ # @param interface_names [Array<String>] names of interface types implemented by this type
20
+ # @return [void]
21
+ #
22
+ # @example Implement an interface
23
+ # ElasticGraph.define_schema do |schema|
24
+ # schema.interface_type "Athlete" do |t|
25
+ # t.field "name", "String"
26
+ # t.field "team", "String"
27
+ # end
28
+ #
29
+ # schema.object_type "BaseballPlayer" do |t|
30
+ # t.implements "Athlete"
31
+ # t.field "name", "String"
32
+ # t.field "team", "String"
33
+ # t.field "battingAvg", "Float"
34
+ # end
35
+ #
36
+ # schema.object_type "BasketballPlayer" do |t|
37
+ # t.implements "Athlete"
38
+ # t.field "name", "String"
39
+ # t.field "team", "String"
40
+ # t.field "pointsPerGame", "Float"
41
+ # end
42
+ # end
43
+ def implements(*interface_names)
44
+ interface_refs = interface_names.map do |interface_name|
45
+ schema_def_state.type_ref(interface_name).to_final_form.tap do |interface_ref|
46
+ schema_def_state.implementations_by_interface_ref[interface_ref] << self
47
+ end
48
+ end
49
+
50
+ implemented_interfaces.concat(interface_refs)
51
+ end
52
+
53
+ # @return [Array<SchemaElements::TypeReference>] list of type references for the interface types implemented by this type
54
+ def implemented_interfaces
55
+ @implemented_interfaces ||= []
56
+ end
57
+
58
+ # Called after the schema definition is complete, before dumping artifacts. Here we validate
59
+ # the correctness of interface implementations. We defer it until this time to not require the
60
+ # interface and fields to be defined before the `implements` call.
61
+ #
62
+ # Note that the GraphQL gem on its own supports a form of "interface inheritance": if declaring
63
+ # that an object type implements an interface, and the object type is missing one or more of the
64
+ # interface fields, the GraphQL gem dynamically adds the missing interface fields to the object
65
+ # type (at least, that's the result I noted when dumping the GraphQL SDL after trying that!).
66
+ # However, we cannot allow that, because our schema definition is used to generate non-GrapQL
67
+ # artifacts (e.g. the JSON schema and the index mapping), and all the artifacts must agree
68
+ # on the fields. Therefore, we use this method to verify that the object type fully implements
69
+ # the specified interfaces.
70
+ #
71
+ # @return [void]
72
+ # @private
73
+ def verify_graphql_correctness!
74
+ schema_error_messages = implemented_interfaces.filter_map do |interface_ref|
75
+ interface = interface_ref.resolved
76
+
77
+ case interface
78
+ when SchemaElements::InterfaceType
79
+ differences = (_ = interface).interface_fields_by_name.values.filter_map do |interface_field|
80
+ my_field_sdl = graphql_fields_by_name[interface_field.name]&.to_sdl(type_structure_only: true)
81
+ interface_field_sdl = interface_field.to_sdl(type_structure_only: true)
82
+
83
+ if my_field_sdl.nil?
84
+ "missing `#{interface_field.name}`"
85
+ elsif my_field_sdl != interface_field_sdl
86
+ "`#{interface_field_sdl.strip}` vs `#{my_field_sdl.strip}`"
87
+ end
88
+ end
89
+
90
+ unless differences.empty?
91
+ "Type `#{name}` does not correctly implement interface `#{interface_ref}` " \
92
+ "due to field differences: #{differences.join("; ")}."
93
+ end
94
+ when nil
95
+ "Type `#{name}` cannot implement `#{interface_ref}` because `#{interface_ref}` is not defined."
96
+ else
97
+ "Type `#{name}` cannot implement `#{interface_ref}` because `#{interface_ref}` is not an interface."
98
+ end
99
+ end
100
+
101
+ unless schema_error_messages.empty?
102
+ raise SchemaError, schema_error_messages.join("\n\n")
103
+ end
104
+ end
105
+
106
+ # @yield [SchemaElements::Argument] an argument
107
+ # @yieldreturn [Boolean] whether or not to include the argument in the generated GraphQL SDL
108
+ # @return [String] SDL string of the type
109
+ def to_sdl(&field_arg_selector)
110
+ name_section =
111
+ if implemented_interfaces.empty?
112
+ name
113
+ else
114
+ "#{name} implements #{implemented_interfaces.join(" & ")}"
115
+ end
116
+
117
+ generate_sdl(name_section: name_section, &field_arg_selector)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,47 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "elastic_graph/support/graphql_formatter"
10
+
11
+ module ElasticGraph
12
+ module SchemaDefinition
13
+ module Mixins
14
+ # A mixin designed to be included in a schema element class that supports default values.
15
+ # Designed to be `prepended` so that it can hook into `initialize`.
16
+ module SupportsDefaultValue
17
+ # @private
18
+ def initialize(...)
19
+ __skip__ = super # steep can't type this.
20
+ @default_value = NO_DEFAULT_PROVIDED
21
+ end
22
+
23
+ # Used to specify the default value for this field or argument.
24
+ #
25
+ # @param default_value [Object] default value for this field or argument
26
+ # @return [void]
27
+ def default(default_value)
28
+ @default_value = default_value
29
+ end
30
+
31
+ # Generates SDL for the default value. Suitable for inclusion in the schema elememnts `#to_sdl`.
32
+ #
33
+ # @return [String]
34
+ def default_value_sdl
35
+ return nil if @default_value == NO_DEFAULT_PROVIDED
36
+ " = #{Support::GraphQLFormatter.serialize(@default_value)}"
37
+ end
38
+
39
+ private
40
+
41
+ # A sentinel value that we can use to detect when a default has been provided.
42
+ # We can't use `nil` to detect if a default has been provided because `nil` is a valid default value!
43
+ NO_DEFAULT_PROVIDED = Module.new
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,267 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ module ElasticGraph
10
+ module SchemaDefinition
11
+ module Mixins
12
+ # Responsible for building object types for filtering and aggregation, from an existing object type.
13
+ #
14
+ # This is specifically designed to support {SchemaElements::TypeWithSubfields} (where we have the fields directly available) and
15
+ # {SchemaElements::UnionType} (where we will need to compute the list of fields by resolving the subtypes and merging their fields).
16
+ #
17
+ # @private
18
+ module SupportsFilteringAndAggregation
19
+ # Indicates if this type supports a given feature (e.g. `filterable?`).
20
+ def supports?(&feature_predicate)
21
+ # If the type uses a custom mapping type we don't know if it can support a feature, so we assume it can't.
22
+ # TODO: clean this up using an interface instead of checking mapping options.
23
+ return false if has_custom_mapping_type?
24
+
25
+ graphql_fields_by_name.values.any?(&feature_predicate)
26
+ end
27
+
28
+ # Inverse of `supports?`.
29
+ def does_not_support?(&feature_predicate)
30
+ !supports?(&feature_predicate)
31
+ end
32
+
33
+ def derived_graphql_types
34
+ return [] if graphql_only?
35
+
36
+ indexed_agg_type = to_indexed_aggregation_type
37
+ indexed_aggregation_pagination_types =
38
+ if indexed_agg_type
39
+ schema_def_state.factory.build_relay_pagination_types(indexed_agg_type.name)
40
+ else
41
+ [] # : ::Array[SchemaElements::ObjectType]
42
+ end
43
+
44
+ sub_aggregation_types = sub_aggregation_types_for_nested_field_references.flat_map do |type|
45
+ [type] + schema_def_state.factory.build_relay_pagination_types(type.name, support_pagination: false) do |t|
46
+ # Record metadata that is necessary for elasticgraph-graphql to correctly recognize and handle
47
+ # this sub-aggregation correctly.
48
+ t.runtime_metadata_overrides = {elasticgraph_category: :nested_sub_aggregation_connection}
49
+ end
50
+ end
51
+
52
+ document_pagination_types =
53
+ if indexed?
54
+ schema_def_state.factory.build_relay_pagination_types(name, include_total_edge_count: true, derived_indexed_types: (_ = self).derived_indexed_types)
55
+ elsif schema_def_state.paginated_collection_element_types.include?(name)
56
+ schema_def_state.factory.build_relay_pagination_types(name, include_total_edge_count: true)
57
+ else
58
+ [] # : ::Array[SchemaElements::ObjectType]
59
+ end
60
+
61
+ sort_order_enum_type = schema_def_state.enums_for_indexed_types.sort_order_enum_for(self)
62
+ derived_sort_order_enum_types = [sort_order_enum_type].compact + (sort_order_enum_type&.derived_graphql_types || [])
63
+
64
+ to_input_filters +
65
+ document_pagination_types +
66
+ indexed_aggregation_pagination_types +
67
+ sub_aggregation_types +
68
+ derived_sort_order_enum_types +
69
+ build_aggregation_sub_aggregations_types + [
70
+ indexed_agg_type,
71
+ to_grouped_by_type,
72
+ to_aggregated_values_type
73
+ ].compact
74
+ end
75
+
76
+ def has_custom_mapping_type?
77
+ mapping_type = mapping_options[:type]
78
+ mapping_type && mapping_type != "object"
79
+ end
80
+
81
+ private
82
+
83
+ # Converts the type to the corresponding input filter type.
84
+ def to_input_filters
85
+ return [] if does_not_support?(&:filterable?)
86
+
87
+ schema_def_state.factory.build_standard_filter_input_types_for_index_object_type(name) do |t|
88
+ graphql_fields_by_name.values.each do |field|
89
+ if field.filterable?
90
+ t.graphql_fields_by_name[field.name] = field.to_filter_field(parent_type: t)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ # Generates the `*SubAggregation` types for all of the `mapping type: "nested"` fields that reference this type.
97
+ # A different `*SubAggregation` type needs to be generated for each nested field reference, and for each parent nesting
98
+ # context of that nested field reference. This is necessary because we will support different available `sub_aggregations`
99
+ # based on the parents of a particular nested field.
100
+ #
101
+ # For example, given a `Player` object type definition and a `Team` type definition like this:
102
+ #
103
+ # schema.object_type "Team" do |t|
104
+ # t.field "id", "ID!"
105
+ # t.field "name", "String"
106
+ # t.field "players", "[Player!]!" do |f|
107
+ # f.mapping type: "nested"
108
+ # end
109
+ # t.index "teams"
110
+ # end
111
+ #
112
+ # ...we will generate a `TeamPlayerSubAggregation` type which will have a `sub_aggregations` field which can have
113
+ # `parent_team` and `seasons` fields (assuming `Player` has a `seasons` nested field...).
114
+ def sub_aggregation_types_for_nested_field_references
115
+ schema_def_state.user_defined_field_references_by_type_name.fetch(name) { [] }.select(&:nested?).flat_map do |nested_field_ref|
116
+ schema_def_state.sub_aggregation_paths_for(nested_field_ref.parent_type).map do |path|
117
+ schema_def_state.factory.new_object_type type_ref.as_sub_aggregation(parent_doc_types: path.parent_doc_types).name do |t|
118
+ t.documentation "Return type representing a bucket of `#{name}` objects for a sub-aggregation within each `#{type_ref.as_parent_aggregation(parent_doc_types: path.parent_doc_types).name}`."
119
+
120
+ t.field schema_def_state.schema_elements.count_detail, "AggregationCountDetail", graphql_only: true do |f|
121
+ f.documentation "Details of the count of `#{name}` documents in a sub-aggregation bucket."
122
+ end
123
+
124
+ if supports?(&:groupable?)
125
+ t.field schema_def_state.schema_elements.grouped_by, type_ref.as_grouped_by.name, graphql_only: true do |f|
126
+ f.documentation "Used to specify the `#{name}` fields to group by. The returned values identify each sub-aggregation bucket."
127
+ end
128
+ end
129
+
130
+ if supports?(&:aggregatable?)
131
+ t.field schema_def_state.schema_elements.aggregated_values, type_ref.as_aggregated_values.name, graphql_only: true do |f|
132
+ f.documentation "Provides computed aggregated values over all `#{name}` documents in a sub-aggregation bucket."
133
+ end
134
+ end
135
+
136
+ if graphql_fields_by_name.values.any?(&:sub_aggregatable?)
137
+ sub_aggs_name = type_ref.as_aggregation_sub_aggregations(parent_doc_types: path.parent_doc_types + [name]).name
138
+ t.field schema_def_state.schema_elements.sub_aggregations, sub_aggs_name, graphql_only: true do |f|
139
+ f.documentation "Used to perform sub-aggregations of `#{t.name}` data."
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ # Builds the `*AggregationSubAggregations` types. For example, for an indexed type named `Team` which has nested fields,
148
+ # this would generate a `TeamAggregationSubAggregations` type. This type provides access to the various sub-aggregation
149
+ # fields.
150
+ def build_aggregation_sub_aggregations_types
151
+ # The sub-aggregation types do not generate correctly for abstract types, so for now we omit sub-aggregations for abstract types.
152
+ return [] if abstract?
153
+
154
+ sub_aggregatable_fields = graphql_fields_by_name.values.select(&:sub_aggregatable?)
155
+ return [] if sub_aggregatable_fields.empty?
156
+
157
+ schema_def_state.sub_aggregation_paths_for(self).map do |path|
158
+ agg_sub_aggs_type_ref = type_ref.as_aggregation_sub_aggregations(
159
+ parent_doc_types: path.parent_doc_types,
160
+ field_path: path.field_path
161
+ )
162
+
163
+ schema_def_state.factory.new_object_type agg_sub_aggs_type_ref.name do |t|
164
+ under_field_description = "under `#{path.field_path_string}` " unless path.field_path.empty?
165
+ t.documentation "Provides access to the `#{schema_def_state.schema_elements.sub_aggregations}` #{under_field_description}within each `#{type_ref.as_parent_aggregation(parent_doc_types: path.parent_doc_types).name}`."
166
+
167
+ sub_aggregatable_fields.each do |field|
168
+ if field.nested?
169
+ unwrapped_type = field.type_for_derived_types.fully_unwrapped
170
+ field_type_name = unwrapped_type
171
+ .as_sub_aggregation(parent_doc_types: path.parent_doc_types)
172
+ .as_connection
173
+ .name
174
+
175
+ field.define_sub_aggregations_field(parent_type: t, type: field_type_name) do |f|
176
+ f.argument schema_def_state.schema_elements.filter, unwrapped_type.as_filter_input.name do |a|
177
+ a.documentation "Used to filter the `#{unwrapped_type.name}` documents included in this sub-aggregation based on the provided criteria."
178
+ end
179
+
180
+ f.argument schema_def_state.schema_elements.first, "Int" do |a|
181
+ a.documentation "Determines how many sub-aggregation buckets should be returned."
182
+ end
183
+ end
184
+ else
185
+ field_type_name = type_ref.as_aggregation_sub_aggregations(
186
+ parent_doc_types: path.parent_doc_types,
187
+ field_path: path.field_path + [field]
188
+ ).name
189
+
190
+ field.define_sub_aggregations_field(parent_type: t, type: field_type_name)
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ def to_indexed_aggregation_type
198
+ return nil unless indexed?
199
+
200
+ schema_def_state.factory.new_object_type type_ref.as_aggregation.name do |t|
201
+ t.documentation "Return type representing a bucket of `#{name}` documents for an aggregations query."
202
+
203
+ if supports?(&:groupable?)
204
+ t.field schema_def_state.schema_elements.grouped_by, type_ref.as_grouped_by.name, graphql_only: true do |f|
205
+ f.documentation "Used to specify the `#{name}` fields to group by. The returned values identify each aggregation bucket."
206
+ end
207
+ end
208
+
209
+ t.field schema_def_state.schema_elements.count, "JsonSafeLong!", graphql_only: true do |f|
210
+ f.documentation "The count of `#{name}` documents in an aggregation bucket."
211
+ end
212
+
213
+ if supports?(&:aggregatable?)
214
+ t.field schema_def_state.schema_elements.aggregated_values, type_ref.as_aggregated_values.name, graphql_only: true do |f|
215
+ f.documentation "Provides computed aggregated values over all `#{name}` documents in an aggregation bucket."
216
+ end
217
+ end
218
+
219
+ # The sub-aggregation types do not generate correctly for abstract types, so for now we omit sub-aggregations for abstract types.
220
+ if !abstract? && supports?(&:sub_aggregatable?)
221
+ t.field schema_def_state.schema_elements.sub_aggregations, type_ref.as_aggregation_sub_aggregations.name, graphql_only: true do |f|
222
+ f.documentation "Used to perform sub-aggregations of `#{t.name}` data."
223
+ end
224
+ end
225
+
226
+ # Record metadata that is necessary for elasticgraph-graphql to correctly recognize and handle
227
+ # this indexed aggregation type correctly.
228
+ t.runtime_metadata_overrides = {source_type: name, elasticgraph_category: :indexed_aggregation}
229
+ end
230
+ end
231
+
232
+ def to_grouped_by_type
233
+ # If the type uses a custom mapping type we don't know how it can be aggregated, so we assume it needs no aggregation type.
234
+ # TODO: clean this up using an interface instead of checking mapping options.
235
+ return nil if has_custom_mapping_type?
236
+
237
+ new_non_empty_object_type type_ref.as_grouped_by.name do |t|
238
+ t.documentation "Type used to specify the `#{name}` fields to group by for aggregations."
239
+
240
+ graphql_fields_by_name.values.each do |field|
241
+ field.define_grouped_by_field(t)
242
+ end
243
+ end
244
+ end
245
+
246
+ def to_aggregated_values_type
247
+ # If the type uses a custom mapping type we don't know how it can be aggregated, so we assume it needs no aggregation type.
248
+ # TODO: clean this up using an interface instead of checking mapping options.
249
+ return nil if has_custom_mapping_type?
250
+
251
+ new_non_empty_object_type type_ref.as_aggregated_values.name do |t|
252
+ t.documentation "Type used to perform aggregation computations on `#{name}` fields."
253
+
254
+ graphql_fields_by_name.values.each do |field|
255
+ field.define_aggregated_values_field(t)
256
+ end
257
+ end
258
+ end
259
+
260
+ def new_non_empty_object_type(name, &block)
261
+ type = schema_def_state.factory.new_object_type(name, &block)
262
+ type unless type.graphql_fields_by_name.empty?
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,38 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "elastic_graph/constants"
10
+
11
+ module ElasticGraph
12
+ module SchemaDefinition
13
+ module Mixins
14
+ # Used to verify the validity of the name of GraphQL schema elements.
15
+ #
16
+ # @note This mixin is designed to be used via `prepend`, so it can add a constructor override that enforces
17
+ # the GraphQL name pattern as the object is built.
18
+ module VerifiesGraphQLName
19
+ # @private
20
+ def initialize(...)
21
+ __skip__ = super(...) # __skip__ tells Steep to ignore this
22
+
23
+ VerifiesGraphQLName.verify_name!(name)
24
+ end
25
+
26
+ # Raises if the provided name is invalid.
27
+ #
28
+ # @param name [String] name of GraphQL schema element
29
+ # @return [void]
30
+ # @raise [InvalidGraphQLNameError] if the name is invalid
31
+ def self.verify_name!(name)
32
+ return if GRAPHQL_NAME_PATTERN.match?(name)
33
+ raise InvalidGraphQLNameError, "Not a valid GraphQL name: `#{name}`. #{GRAPHQL_NAME_VALIDITY_DESCRIPTION}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end