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,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