elasticgraph-schema_definition 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,218 @@
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 "delegate"
10
+ require "elastic_graph/error"
11
+ require "elastic_graph/schema_definition/schema_elements/field"
12
+ require "elastic_graph/support/hash_util"
13
+
14
+ module ElasticGraph
15
+ module SchemaDefinition
16
+ module SchemaElements
17
+ # Wraps a {Field} to provide additional relationship-specific functionality when defining a field via
18
+ # {TypeWithSubfields#relates_to_one} or {TypeWithSubfields#relates_to_many}.
19
+ #
20
+ # @example Define relationships between two types
21
+ # ElasticGraph.define_schema do |schema|
22
+ # schema.object_type "Orchestra" do |t|
23
+ # t.field "id", "ID"
24
+ # t.relates_to_many "musicians", "Musician", via: "orchestraId", dir: :in, singular: "musician" do |r|
25
+ # # In this block, `r` is a `Relationship`.
26
+ # end
27
+ # t.index "orchestras"
28
+ # end
29
+ #
30
+ # schema.object_type "Musician" do |t|
31
+ # t.field "id", "ID"
32
+ # t.field "instrument", "String"
33
+ # t.relates_to_one "orchestra", "Orchestra", via: "orchestraId", dir: :out do |r|
34
+ # # In this block, `r` is a `Relationship`.
35
+ # end
36
+ # t.index "musicians"
37
+ # end
38
+ # end
39
+ class Relationship < DelegateClass(Field)
40
+ # @dynamic related_type
41
+
42
+ # @return [ObjectType, InterfaceType, UnionType] the type this relationship relates to
43
+ attr_reader :related_type
44
+
45
+ # @private
46
+ def initialize(field, cardinality:, related_type:, foreign_key:, direction:)
47
+ super(field)
48
+ @cardinality = cardinality
49
+ @related_type = related_type
50
+ @foreign_key = foreign_key
51
+ @direction = direction
52
+ @equivalent_field_paths_by_local_path = {}
53
+ @additional_filter = {}
54
+ end
55
+
56
+ # Adds additional filter conditions to a relationship beyond the foreign key.
57
+ #
58
+ # @param filter [Hash<Symbol, Object>, Hash<String, Object>] additional filter conditions for this relationship
59
+ # @return [void]
60
+ #
61
+ # @example Define additional filter conditions on a `relates_to_one` relationship
62
+ # ElasticGraph.define_schema do |schema|
63
+ # schema.object_type "Orchestra" do |t|
64
+ # t.field "id", "ID"
65
+ # t.relates_to_many "musicians", "Musician", via: "orchestraId", dir: :in, singular: "musician"
66
+ # t.relates_to_one "firstViolin", "Musician", via: "orchestraId", dir: :in do |r|
67
+ # r.additional_filter isFirstViolon: true
68
+ # end
69
+ #
70
+ # t.index "orchestras"
71
+ # end
72
+ #
73
+ # schema.object_type "Musician" do |t|
74
+ # t.field "id", "ID"
75
+ # t.field "instrument", "String"
76
+ # t.field "isFirstViolon", "Boolean"
77
+ # t.relates_to_one "orchestra", "Orchestra", via: "orchestraId", dir: :out
78
+ # t.index "musicians"
79
+ # end
80
+ # end
81
+ def additional_filter(filter)
82
+ stringified_filter = Support::HashUtil.stringify_keys(filter)
83
+ @additional_filter = Support::HashUtil.deep_merge(@additional_filter, stringified_filter)
84
+ end
85
+
86
+ # Indicates that `path` (a field on the related type) is the equivalent of `locally_named` on this type.
87
+ #
88
+ # Use this API to specify a local field's equivalent path on the related type. This must be used on relationships used by
89
+ # {Field#sourced_from} when the local type uses {Indexing::Index#route_with} or {Indexing::Index#rollover} so that
90
+ # ElasticGraph can determine what field from the related type to use to route the update requests to the correct index and shard.
91
+ #
92
+ # @param path [String] path to a routing or rollover field on the related type
93
+ # @param locally_named [String] path on the local type to the equivalent field
94
+ # @return [void]
95
+ #
96
+ # @example
97
+ # ElasticGraph.define_schema do |schema|
98
+ # schema.object_type "Campaign" do |t|
99
+ # t.field "id", "ID!"
100
+ # t.field "name", "String"
101
+ # t.field "createdAt", "DateTime"
102
+ #
103
+ # t.relates_to_one "launchPlan", "CampaignLaunchPlan", via: "campaignId", dir: :in do |r|
104
+ # r.equivalent_field "campaignCreatedAt", locally_named: "createdAt"
105
+ # end
106
+ #
107
+ # t.field "launchDate", "Date" do |f|
108
+ # f.sourced_from "launchPlan", "launchDate"
109
+ # end
110
+ #
111
+ # t.index "campaigns"do |i|
112
+ # i.rollover :yearly, "createdAt"
113
+ # end
114
+ # end
115
+ #
116
+ # schema.object_type "CampaignLaunchPlan" do |t|
117
+ # t.field "id", "ID"
118
+ # t.field "campaignId", "ID"
119
+ # t.field "campaignCreatedAt", "DateTime"
120
+ # t.field "launchDate", "Date"
121
+ #
122
+ # t.index "campaign_launch_plans"
123
+ # end
124
+ # end
125
+ def equivalent_field(path, locally_named: path)
126
+ if @equivalent_field_paths_by_local_path.key?(locally_named)
127
+ raise SchemaError, "`equivalent_field` has been called multiple times on `#{parent_type.name}.#{name}` with the same " \
128
+ "`locally_named` value (#{locally_named.inspect}), but each local field can have only one `equivalent_field`."
129
+ else
130
+ @equivalent_field_paths_by_local_path[locally_named] = path
131
+ end
132
+ end
133
+
134
+ # Gets the `routing_value_source` from this relationship for the given `index`, based on the configured
135
+ # routing used by `index` and the configured equivalent fields.
136
+ #
137
+ # Returns the GraphQL field name (not the `name_in_index`).
138
+ #
139
+ # @private
140
+ def routing_value_source_for_index(index)
141
+ return nil unless index.uses_custom_routing?
142
+
143
+ @equivalent_field_paths_by_local_path.fetch(index.routing_field_path.path) do |local_need|
144
+ yield local_need
145
+ end
146
+ end
147
+
148
+ # Gets the `rollover_timestamp_value_source` from this relationship for the given `index`, based on the
149
+ # configured equivalent fields and the rollover configuration used by `index`.
150
+ #
151
+ # Returns the GraphQL field name (not the `name_in_index`).
152
+ #
153
+ # @private
154
+ def rollover_timestamp_value_source_for_index(index)
155
+ return nil unless (rollover_config = index.rollover_config)
156
+
157
+ @equivalent_field_paths_by_local_path.fetch(rollover_config.timestamp_field_path.path) do |local_need|
158
+ yield local_need
159
+ end
160
+ end
161
+
162
+ # @private
163
+ def validate_equivalent_fields(field_path_resolver)
164
+ resolved_related_type = (_ = related_type.as_object_type) # : indexableType
165
+
166
+ @equivalent_field_paths_by_local_path.flat_map do |local_path_string, related_type_path_string|
167
+ errors = [] # : ::Array[::String]
168
+
169
+ local_path = resolve_and_validate_field_path(parent_type, local_path_string, field_path_resolver) do |error|
170
+ errors << error
171
+ end
172
+
173
+ related_type_path = resolve_and_validate_field_path(resolved_related_type, related_type_path_string, field_path_resolver) do |error|
174
+ errors << error
175
+ end
176
+
177
+ if local_path && related_type_path && local_path.type.unwrap_non_null != related_type_path.type.unwrap_non_null
178
+ errors << "Field `#{related_type_path.full_description}` is defined as an equivalent of " \
179
+ "`#{local_path.full_description}` via an `equivalent_field` definition on `#{parent_type.name}.#{name}`, " \
180
+ "but their types do not agree. To continue, change one or the other so that they agree."
181
+ end
182
+
183
+ errors
184
+ end
185
+ end
186
+
187
+ # @private
188
+ def many?
189
+ @cardinality == :many
190
+ end
191
+
192
+ # @private
193
+ def runtime_metadata
194
+ field_path_resolver = SchemaElements::FieldPath::Resolver.new
195
+ resolved_related_type = (_ = related_type.unwrap_list.as_object_type) # : indexableType
196
+ foreign_key_nested_paths = field_path_resolver.determine_nested_paths(resolved_related_type, @foreign_key)
197
+ foreign_key_nested_paths ||= [] # : ::Array[::String]
198
+ SchemaArtifacts::RuntimeMetadata::Relation.new(foreign_key: @foreign_key, direction: @direction, additional_filter: @additional_filter, foreign_key_nested_paths: foreign_key_nested_paths)
199
+ end
200
+
201
+ private
202
+
203
+ def resolve_and_validate_field_path(type, field_path_string, field_path_resolver)
204
+ field_path = field_path_resolver.resolve_public_path(type, field_path_string) do |parent_field|
205
+ !parent_field.type.list?
206
+ end
207
+
208
+ if field_path.nil?
209
+ yield "Field `#{type.name}.#{field_path_string}` (referenced from an `equivalent_field` defined on " \
210
+ "`#{parent_type.name}.#{name}`) does not exist. Either define it or correct the `equivalent_field` definition."
211
+ end
212
+
213
+ field_path
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,310 @@
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/schema_artifacts/runtime_metadata/scalar_type"
10
+ require "elastic_graph/schema_definition/indexing/field_type/scalar"
11
+ require "elastic_graph/schema_definition/mixins/can_be_graphql_only"
12
+ require "elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations"
13
+ require "elastic_graph/schema_definition/mixins/has_directives"
14
+ require "elastic_graph/schema_definition/mixins/has_documentation"
15
+ require "elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect"
16
+ require "elastic_graph/schema_definition/mixins/has_type_info"
17
+ require "elastic_graph/schema_definition/mixins/verifies_graphql_name"
18
+
19
+ module ElasticGraph
20
+ module SchemaDefinition
21
+ module SchemaElements
22
+ # {include:API#scalar_type}
23
+ #
24
+ # @example Define a scalar type
25
+ # ElasticGraph.define_schema do |schema|
26
+ # schema.scalar_type "URL" do |t|
27
+ # t.mapping type: "keyword"
28
+ # t.json_schema type: "string", format: "uri"
29
+ # end
30
+ # end
31
+ #
32
+ # @!attribute [r] schema_def_state
33
+ # @return [State] schema definition state
34
+ # @!attribute [rw] type_ref
35
+ # @private
36
+ # @!attribute [rw] mapping_type
37
+ # @private
38
+ # @!attribute [rw] runtime_metadata
39
+ # @private
40
+ # @!attribute [rw] aggregated_values_customizations
41
+ # @private
42
+ class ScalarType < Struct.new(:schema_def_state, :type_ref, :mapping_type, :runtime_metadata, :aggregated_values_customizations)
43
+ # `Struct.new` provides the following methods:
44
+ # @dynamic type_ref, runtime_metadata
45
+ prepend Mixins::VerifiesGraphQLName
46
+ include Mixins::CanBeGraphQLOnly
47
+ include Mixins::HasDocumentation
48
+ include Mixins::HasDirectives
49
+ include Mixins::HasDerivedGraphQLTypeCustomizations
50
+ include Mixins::HasReadableToSAndInspect.new { |t| t.name }
51
+
52
+ # `HasTypeInfo` provides the following methods:
53
+ # @dynamic mapping_options, json_schema_options
54
+ include Mixins::HasTypeInfo
55
+
56
+ # @dynamic graphql_only?
57
+
58
+ # @private
59
+ def initialize(schema_def_state, name)
60
+ super(schema_def_state, schema_def_state.type_ref(name).to_final_form)
61
+
62
+ # Default the runtime metadata before yielding, so it can be overridden as needed.
63
+ self.runtime_metadata = SchemaArtifacts::RuntimeMetadata::ScalarType.new(
64
+ coercion_adapter_ref: SchemaArtifacts::RuntimeMetadata::ScalarType::DEFAULT_COERCION_ADAPTER_REF,
65
+ indexing_preparer_ref: SchemaArtifacts::RuntimeMetadata::ScalarType::DEFAULT_INDEXING_PREPARER_REF
66
+ )
67
+
68
+ yield self
69
+
70
+ missing = [
71
+ ("`mapping`" if mapping_options.empty?),
72
+ ("`json_schema`" if json_schema_options.empty?)
73
+ ].compact
74
+
75
+ if missing.any?
76
+ raise SchemaError, "Scalar types require `mapping` and `json_schema` to be configured, but `#{name}` lacks #{missing.join(" and ")}."
77
+ end
78
+ end
79
+
80
+ # @return [String] name of the scalar type
81
+ def name
82
+ type_ref.name
83
+ end
84
+
85
+ # (see Mixins::HasTypeInfo#mapping)
86
+ def mapping(**options)
87
+ self.mapping_type = options.fetch(:type) do
88
+ raise SchemaError, "Must specify a mapping `type:` on custom scalars but was missing on the `#{name}` type."
89
+ end
90
+
91
+ super
92
+ end
93
+
94
+ # Specifies the scalar coercion adapter that should be used for this scalar type. The scalar coercion adapter is responsible
95
+ # for validating and coercing scalar input values, and converting scalar return values to a form suitable for JSON serialization.
96
+ #
97
+ # @note For examples of scalar coercion adapters, see `ElasticGraph::GraphQL::ScalarCoercionAdapters`.
98
+ # @note If the `defined_at` require path requires any directories be put on the Ruby `$LOAD_PATH`, you are responsible for doing
99
+ # that before booting {ElasticGraph::GraphQL}.
100
+ #
101
+ # @param adapter_name [String] fully qualified Ruby class name of the adapter
102
+ # @param defined_at [String] the `require` path of the adapter
103
+ # @return [void]
104
+ #
105
+ # @example Register a coercion adapter
106
+ # ElasticGraph.define_schema do |schema|
107
+ # schema.scalar_type "PhoneNumber" do |t|
108
+ # t.mapping type: "keyword"
109
+ # t.json_schema type: "string", pattern: "^\\+[1-9][0-9]{1,14}$"
110
+ # t.coerce_with "CoercionAdapters::PhoneNumber", defined_at: "./coercion_adapters/phone_number"
111
+ # end
112
+ # end
113
+ def coerce_with(adapter_name, defined_at:)
114
+ self.runtime_metadata = runtime_metadata.with(coercion_adapter_ref: {
115
+ "extension_name" => adapter_name,
116
+ "require_path" => defined_at
117
+ }).tap(&:load_coercion_adapter) # verify the adapter is valid.
118
+ end
119
+
120
+ # Specifies an indexing preparer that should be used for this scalar type. The indexing preparer is responsible for preparing
121
+ # scalar values before indexing them, performing any desired formatting or normalization.
122
+ #
123
+ # @note For examples of scalar coercion adapters, see `ElasticGraph::Indexer::IndexingPreparers`.
124
+ # @note If the `defined_at` require path requires any directories be put on the Ruby `$LOAD_PATH`, you are responsible for doing
125
+ # that before booting {ElasticGraph::GraphQL}.
126
+ #
127
+ # @param preparer_name [String] fully qualified Ruby class name of the indexing preparer
128
+ # @param defined_at [String] the `require` path of the preparer
129
+ # @return [void]
130
+ #
131
+ # @example Register an indexing preparer
132
+ # ElasticGraph.define_schema do |schema|
133
+ # schema.scalar_type "PhoneNumber" do |t|
134
+ # t.mapping type: "keyword"
135
+ # t.json_schema type: "string", pattern: "^\\+[1-9][0-9]{1,14}$"
136
+ #
137
+ # t.prepare_for_indexing_with "IndexingPreparers::PhoneNumber",
138
+ # defined_at: "./indexing_preparers/phone_number"
139
+ # end
140
+ # end
141
+ def prepare_for_indexing_with(preparer_name, defined_at:)
142
+ self.runtime_metadata = runtime_metadata.with(indexing_preparer_ref: {
143
+ "extension_name" => preparer_name,
144
+ "require_path" => defined_at
145
+ }).tap(&:load_indexing_preparer) # verify the preparer is valid.
146
+ end
147
+
148
+ # @return [String] the GraphQL SDL form of this scalar
149
+ def to_sdl
150
+ "#{formatted_documentation}scalar #{name} #{directives_sdl}"
151
+ end
152
+
153
+ # Registers a block which will be used to customize the derived `*AggregatedValues` object type.
154
+ #
155
+ # @private
156
+ def customize_aggregated_values_type(&block)
157
+ self.aggregated_values_customizations = block
158
+ end
159
+
160
+ # @private
161
+ def aggregated_values_type
162
+ if aggregated_values_customizations
163
+ type_ref.as_aggregated_values
164
+ else
165
+ schema_def_state.type_ref("NonNumeric").as_aggregated_values
166
+ end
167
+ end
168
+
169
+ # @private
170
+ def to_indexing_field_type
171
+ Indexing::FieldType::Scalar.new(scalar_type: self)
172
+ end
173
+
174
+ # @private
175
+ def derived_graphql_types
176
+ return [] if graphql_only?
177
+
178
+ pagination_types =
179
+ if schema_def_state.paginated_collection_element_types.include?(name)
180
+ schema_def_state.factory.build_relay_pagination_types(name, include_total_edge_count: true)
181
+ else
182
+ [] # : ::Array[ObjectType]
183
+ end
184
+
185
+ (to_input_filters + pagination_types).tap do |derived_types|
186
+ if (aggregated_values_type = to_aggregated_values_type)
187
+ derived_types << aggregated_values_type
188
+ end
189
+ end
190
+ end
191
+
192
+ # @private
193
+ def indexed?
194
+ false
195
+ end
196
+
197
+ private
198
+
199
+ EQUAL_TO_ANY_OF_DOC = <<~EOS
200
+ Matches records where the field value is equal to any of the provided values.
201
+ This works just like an IN operator in SQL.
202
+
203
+ Will be ignored when `null` is passed. When an empty list is passed, will cause this
204
+ part of the filter to match no documents. When `null` is passed in the list, will
205
+ match records where the field value is `null`.
206
+ EOS
207
+
208
+ GT_DOC = <<~EOS
209
+ Matches records where the field value is greater than (>) the provided value.
210
+
211
+ Will be ignored when `null` is passed.
212
+ EOS
213
+
214
+ GTE_DOC = <<~EOS
215
+ Matches records where the field value is greater than or equal to (>=) the provided value.
216
+
217
+ Will be ignored when `null` is passed.
218
+ EOS
219
+
220
+ LT_DOC = <<~EOS
221
+ Matches records where the field value is less than (<) the provided value.
222
+
223
+ Will be ignored when `null` is passed.
224
+ EOS
225
+
226
+ LTE_DOC = <<~EOS
227
+ Matches records where the field value is less than or equal to (<=) the provided value.
228
+
229
+ Will be ignored when `null` is passed.
230
+ EOS
231
+
232
+ def to_input_filters
233
+ # Note: all fields on inputs should be nullable, to support parameterized queries where
234
+ # the parameters are allowed to be set to `null`. We also now support nulls within lists.
235
+
236
+ # For floats, we may want to remove the `equal_to_any_of` operator at some point.
237
+ # In many languages. checking exact equality with floats is problematic.
238
+ # For example, in IRB:
239
+ #
240
+ # 2.7.1 :003 > 0.3 == (0.1 + 0.2)
241
+ # => false
242
+ #
243
+ # However, it's not yet clear if that issue will come up with GraphQL, because
244
+ # float values are serialized on the wire as JSON, using an exact decimal
245
+ # string representation. So for now we are keeping `equal_to_any_of`.
246
+ schema_def_state.factory.build_standard_filter_input_types_for_index_leaf_type(name) do |t|
247
+ # Normally, we use a nullable type for `equal_to_any_of`, to allow a filter expression like this:
248
+ #
249
+ # filter: {optional_field: {equal_to_any_of: [null]}}
250
+ #
251
+ # That filter expression matches documents where `optional_field == null`. However,
252
+ # we cannot support this:
253
+ #
254
+ # filter: {tags: {any_satisfy: {equal_to_any_of: [null]}}}
255
+ #
256
+ # We can't support that because we implement filtering on `null` using an `exists` query:
257
+ # https://www.elastic.co/guide/en/elasticsearch/reference/8.10/query-dsl-exists-query.html
258
+ #
259
+ # ...but that works based on the field existing (or not), and does not let us filter on the
260
+ # presence or absence of `null` within a list.
261
+ #
262
+ # So, here we make the field non-null if we're in an `any_satisfy` context (as indicated by
263
+ # the type ending with `ListElementFilterInput`).
264
+ equal_to_any_of_type = t.type_ref.list_element_filter_input? ? "[#{name}!]" : "[#{name}]"
265
+
266
+ t.field schema_def_state.schema_elements.equal_to_any_of, equal_to_any_of_type do |f|
267
+ f.documentation EQUAL_TO_ANY_OF_DOC
268
+ end
269
+
270
+ if mapping_type_efficiently_comparable?
271
+ t.field schema_def_state.schema_elements.gt, name do |f|
272
+ f.documentation GT_DOC
273
+ end
274
+
275
+ t.field schema_def_state.schema_elements.gte, name do |f|
276
+ f.documentation GTE_DOC
277
+ end
278
+
279
+ t.field schema_def_state.schema_elements.lt, name do |f|
280
+ f.documentation LT_DOC
281
+ end
282
+
283
+ t.field schema_def_state.schema_elements.lte, name do |f|
284
+ f.documentation LTE_DOC
285
+ end
286
+ end
287
+ end
288
+ end
289
+
290
+ def to_aggregated_values_type
291
+ return nil unless (customization_block = aggregated_values_customizations)
292
+ schema_def_state.factory.new_aggregated_values_type_for_index_leaf_type(name, &customization_block)
293
+ end
294
+
295
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
296
+ # https://www.elastic.co/guide/en/elasticsearch/reference/7.13/number.html#number
297
+ NUMERIC_TYPES = %w[long integer short byte double float half_float scaled_float unsigned_long].to_set
298
+ DATE_TYPES = %w[date date_nanos].to_set
299
+ # The Elasticsearch/OpenSearch docs do not exhaustively give a list of types on which range queries are efficient,
300
+ # but the docs are clear that it is efficient on numeric and date types, and is inefficient on string
301
+ # types: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html
302
+ COMPARABLE_TYPES = NUMERIC_TYPES | DATE_TYPES
303
+
304
+ def mapping_type_efficiently_comparable?
305
+ COMPARABLE_TYPES.include?(mapping_type)
306
+ end
307
+ end
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,36 @@
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 "delegate"
10
+ require "elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect"
11
+ require "elastic_graph/schema_definition/schema_elements/enum_value"
12
+
13
+ module ElasticGraph
14
+ module SchemaDefinition
15
+ module SchemaElements
16
+ # Simple wrapper around an {EnumValue} so that we can expose the `sort_order_field_path` to {Field} customization callbacks.
17
+ class SortOrderEnumValue < DelegateClass(EnumValue)
18
+ include Mixins::HasReadableToSAndInspect.new { |v| v.name }
19
+
20
+ # @dynamic sort_order_field_path
21
+
22
+ # @return [Array<Field>] path to the field from the root of the indexed {ObjectType}
23
+ attr_reader :sort_order_field_path
24
+
25
+ # @private
26
+ def initialize(enum_value, sort_order_field_path)
27
+ # We've told steep that SortOrderEnumValue is subclass of EnumValue
28
+ # but here are supering to the `DelegateClass`'s initialize, not `EnumValue`'s,
29
+ # so we have to use `__skip__`
30
+ __skip__ = super(enum_value)
31
+ @sort_order_field_path = sort_order_field_path
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,66 @@
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 SchemaElements
12
+ # Abstraction responsible for identifying paths to sub-aggregations, and, on that basis, determining
13
+ # what the type names should be.
14
+ #
15
+ # @private
16
+ SubAggregationPath = ::Data.define(
17
+ # List of index document types within which the target type exists. This contains the set of parent
18
+ # index document types--that is, types which are indexed or are themselves used as a `nested` field
19
+ # on a parent of it. Parent objects which are not "index documents" (e.g. directly at an index level
20
+ # or a nested field level) are omitted; we omit them because we don't offer sub-aggregations for such
21
+ # a field, and the set of sub-aggregations we are going to offer is the basis for generating separate
22
+ # `*SubAggregation` types.
23
+ :parent_doc_types,
24
+ # List of fields forming a path from the last parent doc type.
25
+ :field_path
26
+ ) do
27
+ # @implements SubAggregationPath
28
+
29
+ # Determines the set of sub aggregation paths for the given type.
30
+ def self.paths_for(type, schema_def_state:)
31
+ root_paths = type.indexed? ? [SubAggregationPath.new([type.name], [])] : [] # : ::Array[SubAggregationPath]
32
+
33
+ non_relation_field_refs = schema_def_state
34
+ .user_defined_field_references_by_type_name.fetch(type.name) { [] }
35
+ # Relationship fields are the only case where types can reference each other in circular fashion.
36
+ # If we don't reject that case here, we can get stuck in infinite recursion.
37
+ .reject(&:relationship)
38
+
39
+ root_paths + non_relation_field_refs.flat_map do |field_ref|
40
+ # Here we call `schema_def_state.sub_aggregation_paths_for` rather than directly
41
+ # recursing to give schema_def_state a chance to cache the results.
42
+ parent_paths = schema_def_state.sub_aggregation_paths_for(field_ref.parent_type)
43
+
44
+ if field_ref.nested?
45
+ parent_paths.map { |path| path.plus_parent(field_ref.type_for_derived_types.fully_unwrapped.name) }
46
+ else
47
+ parent_paths.map { |path| path.plus_field(field_ref) }
48
+ end
49
+ end
50
+ end
51
+
52
+ def plus_parent(parent)
53
+ with(parent_doc_types: parent_doc_types + [parent], field_path: [])
54
+ end
55
+
56
+ def plus_field(field)
57
+ with(field_path: field_path + [field])
58
+ end
59
+
60
+ def field_path_string
61
+ field_path.map(&:name).join(".")
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end