Class: ElasticGraph::SchemaDefinition::SchemaElements::ScalarType
- Inherits:
-
Struct
- Object
- Struct
- ElasticGraph::SchemaDefinition::SchemaElements::ScalarType
- Includes:
- Mixins::CanBeGraphQLOnly, Mixins::HasDerivedGraphQLTypeCustomizations, Mixins::HasDirectives, Mixins::HasDocumentation, Mixins::HasTypeInfo, Mixins::VerifiesGraphQLName
- Defined in:
- elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb
Overview
Defines a GraphQL scalar type. ElasticGraph itself uses this to define a few
common scalar types (e.g. Date and DateTime), but it is also available to you to use to define your own custom scalar types.
Constant Summary
Constants included from Mixins::HasTypeInfo
Mixins::HasTypeInfo::CUSTOMIZABLE_DATASTORE_PARAMS
Instance Attribute Summary collapse
-
#grouping_missing_value_placeholder_overridden ⇒ Object
Returns the value of attribute grouping_missing_value_placeholder_overridden.
-
#schema_def_state ⇒ State
readonly
Schema definition state.
Attributes included from Mixins::HasDocumentation
Instance Method Summary collapse
-
#coerce_with(adapter_name, defined_at:) ⇒ void
Specifies the scalar coercion adapter that should be used for this scalar type.
-
#grouping_missing_value_placeholder(placeholder) ⇒ void
Specifies a placeholder value to use for missing values when grouping by this scalar type.
-
#mapping(**options) ⇒ void
Defines the Elasticsearch/OpenSearch field mapping type and mapping parameters for a field or type.
-
#name ⇒ String
Name of the scalar type.
-
#prepare_for_indexing_with(preparer_name, defined_at:) ⇒ void
Specifies an indexing preparer that should be used for this scalar type.
-
#to_sdl ⇒ String
The GraphQL SDL form of this scalar.
Methods included from Mixins::HasTypeInfo
#json_schema, #json_schema_options, #mapping_options
Methods included from Mixins::HasDerivedGraphQLTypeCustomizations
#customize_derived_type_fields, #customize_derived_types
Methods included from Mixins::HasDirectives
#directive, #directives, #directives_sdl
Methods included from Mixins::HasDocumentation
#append_to_documentation, #derived_documentation, #documentation, #formatted_documentation
Methods included from Mixins::CanBeGraphQLOnly
Methods included from Mixins::VerifiesGraphQLName
Instance Attribute Details
#grouping_missing_value_placeholder_overridden ⇒ Object
Returns the value of attribute grouping_missing_value_placeholder_overridden
44 45 46 |
# File 'elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb', line 44 def grouping_missing_value_placeholder_overridden @grouping_missing_value_placeholder_overridden end |
#schema_def_state ⇒ State (readonly)
Returns schema definition state.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 |
# File 'elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb', line 44 class ScalarType < Struct.new( :schema_def_state, :type_ref, :grouping_missing_value_placeholder_overridden, :mapping_type, :runtime_metadata, :aggregated_values_customizations, :filter_input_customizations ) # `Struct.new` provides the following methods: # @dynamic type_ref, runtime_metadata prepend Mixins::VerifiesGraphQLName include Mixins::CanBeGraphQLOnly include Mixins::HasDocumentation include Mixins::HasDirectives include Mixins::HasDerivedGraphQLTypeCustomizations include Mixins::HasReadableToSAndInspect.new { |t| t.name } # `HasTypeInfo` provides the following methods: # @dynamic mapping_options, json_schema_options include Mixins::HasTypeInfo # @dynamic graphql_only? # @private def initialize(schema_def_state, name) super(schema_def_state, schema_def_state.type_ref(name).to_final_form, false) # Default the runtime metadata before yielding, so it can be overridden as needed. self. = SchemaArtifacts::RuntimeMetadata::ScalarType.new( coercion_adapter_ref: SchemaArtifacts::RuntimeMetadata::ScalarType::DEFAULT_COERCION_ADAPTER_REF, indexing_preparer_ref: SchemaArtifacts::RuntimeMetadata::ScalarType::DEFAULT_INDEXING_PREPARER_REF, grouping_missing_value_placeholder: nil ) yield self missing = [ ("`mapping`" if .empty?), ("`json_schema`" if .empty?) ].compact if missing.any? raise Errors::SchemaError, "Scalar types require `mapping` and `json_schema` to be configured, but `#{name}` lacks #{missing.join(" and ")}." end if (placeholder = inferred_grouping_missing_value_placeholder) self. = .with(grouping_missing_value_placeholder: placeholder) end end # @return [String] name of the scalar type def name type_ref.name end # (see Mixins::HasTypeInfo#mapping) def mapping(**) self.mapping_type = .fetch(:type) do raise Errors::SchemaError, "Must specify a mapping `type:` on custom scalars but was missing on the `#{name}` type." end super end # Specifies the scalar coercion adapter that should be used for this scalar type. The scalar coercion adapter is responsible # for validating and coercing scalar input values, and converting scalar return values to a form suitable for JSON serialization. # # @note For examples of scalar coercion adapters, see `ElasticGraph::GraphQL::ScalarCoercionAdapters`. # @note If the `defined_at` require path requires any directories be put on the Ruby `$LOAD_PATH`, you are responsible for doing # that before booting {ElasticGraph::GraphQL}. # # @param adapter_name [String] fully qualified Ruby class name of the adapter # @param defined_at [String] the `require` path of the adapter # @return [void] # # @example Register a coercion adapter # ElasticGraph.define_schema do |schema| # schema.scalar_type "PhoneNumber" do |t| # t.mapping type: "keyword" # t.json_schema type: "string", pattern: "^\\+[1-9][0-9]{1,14}$" # t.coerce_with "CoercionAdapters::PhoneNumber", defined_at: "./coercion_adapters/phone_number" # end # end def coerce_with(adapter_name, defined_at:) self. = .with(coercion_adapter_ref: { "name" => adapter_name, "require_path" => defined_at }).tap(&:load_coercion_adapter) # verify the adapter is valid. end # Specifies an indexing preparer that should be used for this scalar type. The indexing preparer is responsible for preparing # scalar values before indexing them, performing any desired formatting or normalization. # # @note For examples of scalar coercion adapters, see `ElasticGraph::Indexer::IndexingPreparers`. # @note If the `defined_at` require path requires any directories be put on the Ruby `$LOAD_PATH`, you are responsible for doing # that before booting {ElasticGraph::GraphQL}. # # @param preparer_name [String] fully qualified Ruby class name of the indexing preparer # @param defined_at [String] the `require` path of the preparer # @return [void] # # @example Register an indexing preparer # ElasticGraph.define_schema do |schema| # schema.scalar_type "PhoneNumber" do |t| # t.mapping type: "keyword" # t.json_schema type: "string", pattern: "^\\+[1-9][0-9]{1,14}$" # # t.prepare_for_indexing_with "IndexingPreparers::PhoneNumber", # defined_at: "./indexing_preparers/phone_number" # end # end def prepare_for_indexing_with(preparer_name, defined_at:) self. = .with(indexing_preparer_ref: { "name" => preparer_name, "require_path" => defined_at }).tap(&:load_indexing_preparer) # verify the preparer is valid. end # Specifies a placeholder value to use for missing values when grouping by this scalar type. # This optimization allows ElasticGraph to use a single terms aggregation instead of separate # terms and missing aggregations, reducing the exponential explosion of subaggregations when # grouping by multiple fields. # # @param placeholder [String, Numeric] the placeholder value to use for missing/null values # @return [void] # # @example Define a grouping missing value placeholder # ElasticGraph.define_schema do |schema| # schema.scalar_type "BigInt" do |t| # t.mapping type: "long" # t.json_schema type: "integer", minimum: -(2**53) + 1, maximum: (2**53) - 1 # t.grouping_missing_value_placeholder "NaN" # end # end def grouping_missing_value_placeholder(placeholder) unless placeholder.nil? || placeholder.is_a?(String) || placeholder.is_a?(Numeric) raise Errors::SchemaError, "grouping_missing_value_placeholder must be a String or Numeric value, but got #{placeholder.class}: #{placeholder.inspect}" end self.grouping_missing_value_placeholder_overridden = true self. = .with(grouping_missing_value_placeholder: placeholder) end # @return [String] the GraphQL SDL form of this scalar def to_sdl "#{formatted_documentation}scalar #{name} #{directives_sdl}" end # Registers a block which will be used to customize the derived `*AggregatedValues` object type. # # @private def customize_aggregated_values_type(&block) self.aggregated_values_customizations = block end # Registers a block which will be used to customize the derived `*FilterInput` object type. # # @private def customize_filter_input_type(&block) self.filter_input_customizations = block end # @private def aggregated_values_type if aggregated_values_customizations type_ref.as_aggregated_values else schema_def_state.type_ref("NonNumeric").as_aggregated_values end end # @private def to_indexing_field_type Indexing::FieldType::Scalar.new(scalar_type: self) end # @private def derived_graphql_types return [] if graphql_only? pagination_types = if schema_def_state.paginated_collection_element_types.include?(name) schema_def_state.factory.build_relay_pagination_types(name, include_total_edge_count: true) else [] # : ::Array[ObjectType] end (to_input_filters + pagination_types).tap do |derived_types| if (aggregated_values_type = to_aggregated_values_type) derived_types << aggregated_values_type end end end # @private def indexed? false end private EQUAL_TO_ANY_OF_DOC = <<~EOS Matches records where the field value is equal to any of the provided values. This works just like an IN operator in SQL. When `null` is passed, matches all documents. When an empty list is passed, this part of the filter matches no documents. When `null` is passed in the list, this part of the filter matches records where the field value is `null`. EOS GT_DOC = <<~EOS Matches records where the field value is greater than (>) the provided value. When `null` is passed, matches all documents. EOS GTE_DOC = <<~EOS Matches records where the field value is greater than or equal to (>=) the provided value. When `null` is passed, matches all documents. EOS LT_DOC = <<~EOS Matches records where the field value is less than (<) the provided value. When `null` is passed, matches all documents. EOS LTE_DOC = <<~EOS Matches records where the field value is less than or equal to (<=) the provided value. When `null` is passed, matches all documents. EOS def to_input_filters # Note: all fields on inputs should be nullable, to support parameterized queries where # the parameters are allowed to be set to `null`. We also now support nulls within lists. # For floats, we may want to remove the `equal_to_any_of` operator at some point. # In many languages. checking exact equality with floats is problematic. # For example, in IRB: # # 2.7.1 :003 > 0.3 == (0.1 + 0.2) # => false # # However, it's not yet clear if that issue will come up with GraphQL, because # float values are serialized on the wire as JSON, using an exact decimal # string representation. So for now we are keeping `equal_to_any_of`. schema_def_state.factory.build_standard_filter_input_types_for_index_leaf_type(name) do |t| # Normally, we use a nullable type for `equal_to_any_of`, to allow a filter expression like this: # # filter: {optional_field: {equal_to_any_of: [null]}} # # That filter expression matches documents where `optional_field == null`. However, # we cannot support this: # # filter: {tags: {any_satisfy: {equal_to_any_of: [null]}}} # # We can't support that because we implement filtering on `null` using an `exists` query: # https://www.elastic.co/guide/en/elasticsearch/reference/8.10/query-dsl-exists-query.html # # ...but that works based on the field existing (or not), and does not let us filter on the # presence or absence of `null` within a list. # # So, here we make the field non-null if we're in an `any_satisfy` context (as indicated by # the type ending with `ListElementFilterInput`). equal_to_any_of_type = t.type_ref.list_element_filter_input? ? "[#{name}!]" : "[#{name}]" t.field schema_def_state.schema_elements.equal_to_any_of, equal_to_any_of_type do |f| f.documentation EQUAL_TO_ANY_OF_DOC end if mapping_type_efficiently_comparable? t.field schema_def_state.schema_elements.gt, name do |f| f.documentation GT_DOC end t.field schema_def_state.schema_elements.gte, name do |f| f.documentation GTE_DOC end t.field schema_def_state.schema_elements.lt, name do |f| f.documentation LT_DOC end t.field schema_def_state.schema_elements.lte, name do |f| f.documentation LTE_DOC end end filter_input_customizations&.call(t) end end def to_aggregated_values_type return nil unless (customization_block = aggregated_values_customizations) schema_def_state.factory.new_aggregated_values_type_for_index_leaf_type(name, &customization_block) end def inferred_grouping_missing_value_placeholder return nil if grouping_missing_value_placeholder_overridden || mapping_type.nil? if STRING_TYPES.include?(mapping_type) MISSING_STRING_PLACEHOLDER elsif FLOAT_TYPES.include?(mapping_type) MISSING_NUMERIC_PLACEHOLDER elsif mapping_type == "long" # It is only safe to use NaN for a long when the long's range is safe to coerce to a float # without loss of precision. This is because using NaN as the missing value will cause # the datastore to coerce the other bucket keys to float. # JSON schema min/max only constrains newly indexed values, not existing data that may fall outside the range before the constraints were added. # This is an edge case where the long range may exceed safe float precision. # In this case, users can set grouping_missing_value_placeholder to nil. if ([:minimum] || LONG_STRING_MIN) >= JSON_SAFE_LONG_MIN && ([:maximum] || LONG_STRING_MAX) <= JSON_SAFE_LONG_MAX inferred_numeric_placeholder_for_integer_type end elsif mapping_type == "unsigned_long" # Similar to the checks above for long except we only need to check the max # (since the min is zero even if not specified) if ([:maximum] || LONG_STRING_MAX) <= JSON_SAFE_LONG_MAX inferred_numeric_placeholder_for_integer_type end elsif INTEGER_TYPES.include?(mapping_type) # All other integer types can safely be coerced to float without loss of precision inferred_numeric_placeholder_for_integer_type end end def inferred_numeric_placeholder_for_integer_type # Using NaN as the missing value placeholder causes the datastore to coerce all bucket keys to float. # If using the default coercion adapter (which is a no-op), the values won't be coerced back to integers, # causing a type change in the returned values. Only use NaN if a custom coercion adapter is configured. if .coercion_adapter_ref == SchemaArtifacts::RuntimeMetadata::ScalarType::DEFAULT_COERCION_ADAPTER_REF nil else MISSING_NUMERIC_PLACEHOLDER end end # https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html # https://www.elastic.co/guide/en/elasticsearch/reference/7.13/number.html#number FLOAT_TYPES = %w[double float half_float scaled_float].to_set INTEGER_TYPES = %w[long integer short byte unsigned_long].to_set NUMERIC_TYPES = FLOAT_TYPES | INTEGER_TYPES # https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/keyword # https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/text-type-family # https://docs.opensearch.org/latest/mappings/supported-field-types/index/#string-based-field-types STRING_TYPES = %w[keyword constant_keyword wildcard text match_only_text pattern_text semantic_text].to_set DATE_TYPES = %w[date date_nanos].to_set # The Elasticsearch/OpenSearch docs do not exhaustively give a list of types on which range queries are efficient, # but the docs are clear that it is efficient on numeric and date types, and is inefficient on string # types: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html COMPARABLE_TYPES = NUMERIC_TYPES | DATE_TYPES def mapping_type_efficiently_comparable? COMPARABLE_TYPES.include?(mapping_type) end end |
Instance Method Details
#coerce_with(adapter_name, defined_at:) ⇒ void
For examples of scalar coercion adapters, see ElasticGraph::GraphQL::ScalarCoercionAdapters.
If the defined_at require path requires any directories be put on the Ruby $LOAD_PATH, you are responsible for doing
that before booting GraphQL.
This method returns an undefined value.
Specifies the scalar coercion adapter that should be used for this scalar type. The scalar coercion adapter is responsible for validating and coercing scalar input values, and converting scalar return values to a form suitable for JSON serialization.
128 129 130 131 132 133 |
# File 'elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb', line 128 def coerce_with(adapter_name, defined_at:) self. = .with(coercion_adapter_ref: { "name" => adapter_name, "require_path" => defined_at }).tap(&:load_coercion_adapter) # verify the adapter is valid. end |
#grouping_missing_value_placeholder(placeholder) ⇒ void
This method returns an undefined value.
Specifies a placeholder value to use for missing values when grouping by this scalar type. This optimization allows ElasticGraph to use a single terms aggregation instead of separate terms and missing aggregations, reducing the exponential explosion of subaggregations when grouping by multiple fields.
179 180 181 182 183 184 185 186 |
# File 'elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb', line 179 def grouping_missing_value_placeholder(placeholder) unless placeholder.nil? || placeholder.is_a?(String) || placeholder.is_a?(Numeric) raise Errors::SchemaError, "grouping_missing_value_placeholder must be a String or Numeric value, but got #{placeholder.class}: #{placeholder.inspect}" end self.grouping_missing_value_placeholder_overridden = true self. = .with(grouping_missing_value_placeholder: placeholder) end |
#mapping(**options) ⇒ void
This method returns an undefined value.
Defines the Elasticsearch/OpenSearch field mapping type
and mapping parameters for a field or type.
The options passed here will be included in the generated datastore_config.yaml artifact that ElasticGraph uses to configure
Elasticsearch/OpenSearch.
Can be called multiple times; each time, the options will be merged into the existing options.
This is required on a ElasticGraph::SchemaDefinition::SchemaElements::ScalarType; without it, ElasticGraph would have no way to know how the datatype should be indexed in the datastore.
On a Field, this can be used to customize how a field is indexed. For example, String fields are normally
indexed as keywords; to instead index a String
field for full text search, you’d need to configure mapping type: "text".
On a ObjectType, this can be used to use a specific Elasticsearch/OpenSearch data type for something that is
modeled as an object in GraphQL. For example, we use it for the GeoLocation type so they get indexed in Elasticsearch using the
geo_point type.
101 102 103 104 105 106 107 |
# File 'elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb', line 101 def mapping(**) self.mapping_type = .fetch(:type) do raise Errors::SchemaError, "Must specify a mapping `type:` on custom scalars but was missing on the `#{name}` type." end super end |
#name ⇒ String
Returns name of the scalar type.
96 97 98 |
# File 'elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb', line 96 def name type_ref.name end |
#prepare_for_indexing_with(preparer_name, defined_at:) ⇒ void
For examples of scalar coercion adapters, see ElasticGraph::Indexer::IndexingPreparers.
If the defined_at require path requires any directories be put on the Ruby $LOAD_PATH, you are responsible for doing
that before booting GraphQL.
This method returns an undefined value.
Specifies an indexing preparer that should be used for this scalar type. The indexing preparer is responsible for preparing scalar values before indexing them, performing any desired formatting or normalization.
156 157 158 159 160 161 |
# File 'elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb', line 156 def prepare_for_indexing_with(preparer_name, defined_at:) self. = .with(indexing_preparer_ref: { "name" => preparer_name, "require_path" => defined_at }).tap(&:load_indexing_preparer) # verify the preparer is valid. end |
#to_sdl ⇒ String
Returns the GraphQL SDL form of this scalar.
189 190 191 |
# File 'elasticgraph-schema_definition/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb', line 189 def to_sdl "#{formatted_documentation}scalar #{name} #{directives_sdl}" end |