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,1541 @@
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
+ require "elastic_graph/graphql/scalar_coercion_adapters/valid_time_zones"
11
+ require "elastic_graph/schema_artifacts/runtime_metadata/enum"
12
+
13
+ module ElasticGraph
14
+ module SchemaDefinition
15
+ module SchemaElements
16
+ # Defines all built-in GraphQL types provided by ElasticGraph.
17
+ #
18
+ # ## Scalar Types
19
+ #
20
+ # ### Standard GraphQL Scalars
21
+ #
22
+ # These are defined by the [GraphQL spec](https://spec.graphql.org/October2021/#sec-Scalars.Built-in-Scalars).
23
+ #
24
+ # Boolean
25
+ # : Represents `true` or `false` values.
26
+ #
27
+ # Float
28
+ # : Represents signed double-precision fractional values as specified by
29
+ # [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).
30
+ #
31
+ # ID
32
+ # : Represents a unique identifier that is Base64 obfuscated. It is often used to
33
+ # refetch an object or as key for a cache. The ID type appears in a JSON response as a
34
+ # String; however, it is not intended to be human-readable. When expected as an input
35
+ # type, any string (such as `"VXNlci0xMA=="`) or integer (such as `4`) input value will
36
+ # be accepted as an ID.
37
+ #
38
+ # Int
39
+ # : Represents non-fractional signed whole numeric values. Int can represent values between
40
+ # -(2^31) and 2^31 - 1.
41
+ #
42
+ # String
43
+ # : Represents textual data as UTF-8 character sequences. This type is most often used by
44
+ # GraphQL to represent free-form human-readable text.
45
+ #
46
+ # ### Additional ElasticGraph Scalars
47
+ #
48
+ # ElasticGraph defines these additional scalar types.
49
+ #
50
+ # Cursor
51
+ # : An opaque string value representing a specific location in a paginated connection type.
52
+ # Returned cursors can be passed back in the next query via the `before` or `after`
53
+ # arguments to continue paginating from that point.
54
+ #
55
+ # Date
56
+ # : A date, represented as an [ISO 8601 date string](https://en.wikipedia.org/wiki/ISO_8601).
57
+ #
58
+ # DateTime
59
+ # : A timestamp, represented as an [ISO 8601 time string](https://en.wikipedia.org/wiki/ISO_8601).
60
+ #
61
+ # JsonSafeLong
62
+ # : A numeric type for large integer values that can serialize safely as JSON. While JSON
63
+ # itself has no hard limit on the size of integers, the RFC-7159 spec mentions that
64
+ # values outside of the range -9,007,199,254,740,991 (-(2^53) + 1) to 9,007,199,254,740,991
65
+ # (2^53 - 1) may not be interopable with all JSON implementations. As it turns out, the
66
+ # number implementation used by JavaScript has this issue. When you parse a JSON string that
67
+ # contains a numeric value like `4693522397653681111`, the parsed result will contain a
68
+ # rounded value like `4693522397653681000`. While this is entirely a client-side problem,
69
+ # we want to preserve maximum compatibility with common client languages. Given the ubiquity
70
+ # of GraphiQL as a GraphQL client, we want to avoid this problem. Our solution is to support
71
+ # two separate types:
72
+ #
73
+ # - This type (`JsonSafeLong`) is serialized as a number, but limits values to the safely
74
+ # serializable range.
75
+ # - The `LongString` type supports long values that use all 64 bits, but serializes as a
76
+ # string rather than a number, avoiding the JavaScript compatibility problems. For more
77
+ # background, see the [JavaScript `Number.MAX_SAFE_INTEGER`
78
+ # docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER).
79
+ #
80
+ # LocalTime
81
+ # : A local time such as `"23:59:33"` or `"07:20:47.454"` without a time zone or offset,
82
+ # formatted based on the [partial-time portion of
83
+ # RFC3339](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6).
84
+ #
85
+ # LongString
86
+ # : A numeric type for large integer values in the inclusive range -2^63 (-9,223,372,036,854,775,808)
87
+ # to (2^63 - 1) (9,223,372,036,854,775,807). Note that `LongString` values are serialized as strings
88
+ # within JSON, to avoid interopability problems with JavaScript. If you want a large integer type
89
+ # that serializes within JSON as a number, use `JsonSafeLong`.
90
+ #
91
+ # TimeZone
92
+ # : An [IANA time zone identifier](https://www.iana.org/time-zones), such as `America/Los_Angeles`
93
+ # or `UTC`. For a full list of valid identifiers, see the
94
+ # [wikipedia article](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List).
95
+ #
96
+ # Untyped
97
+ # : A custom scalar type that allows any type of data, including:
98
+ #
99
+ # - strings
100
+ # - numbers
101
+ # - objects and arrays (nested as deeply as you like)
102
+ # - booleans
103
+ #
104
+ # Note: fields of this type are effectively untyped. We recommend it only be used for parts
105
+ # of your schema that can't be statically typed.
106
+ #
107
+ # ## Enum Types
108
+ #
109
+ # ElasticGraph defines these enum types. Most of these are intended for usage as an _input_
110
+ # argument, but they could be used as a return type in your schema if they meet your needs.
111
+ #
112
+ # DateGroupingGranularity
113
+ # : Enumerates the supported granularities of a `Date`.
114
+ #
115
+ # DateGroupingTruncationUnit
116
+ # : Enumerates the supported truncation units of a `Date`.
117
+ #
118
+ # DateTimeGroupingGranularity
119
+ # : Enumerates the supported granularities of a `DateTime`.
120
+ #
121
+ # DateTimeGroupingTruncationUnit
122
+ # : Enumerates the supported truncation units of a `DateTime`.
123
+ #
124
+ # DateTimeUnit
125
+ # : Enumeration of `DateTime` units.
126
+ #
127
+ # DateUnit
128
+ # : Enumeration of `Date` units.
129
+ #
130
+ # DayOfWeek
131
+ # : Indicates the specific day of the week.
132
+ #
133
+ # DistanceUnit
134
+ # : Enumerates the supported distance units.
135
+ #
136
+ # LocalTimeGroupingTruncationUnit
137
+ # : Enumerates the supported truncation units of a `LocalTime`.
138
+ #
139
+ # LocalTimeUnit
140
+ # : Enumeration of `LocalTime` units.
141
+ #
142
+ # MatchesQueryAllowedEditsPerTerm
143
+ # : Enumeration of allowed values for the `matchesQuery: {allowedEditsPerTerm: ...}` filter option.
144
+ #
145
+ # ## Object Types
146
+ #
147
+ # ElasticGraph defines these object types.
148
+ #
149
+ # AggregationCountDetail
150
+ # : Provides detail about an aggregation `count`.
151
+ #
152
+ # GeoLocation
153
+ # : Geographic coordinates representing a location on the Earth's surface.
154
+ #
155
+ # PageInfo
156
+ # : Provides information about the specific fetched page. This implements the
157
+ # `PageInfo` specification from the [Relay GraphQL Cursor Connections
158
+ # Specification](https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo).
159
+ #
160
+ # @!attribute [rw] schema_def_api
161
+ # @private
162
+ # @!attribute [rw] schema_def_state
163
+ # @private
164
+ # @!attribute [rw] names
165
+ # @private
166
+ class BuiltInTypes
167
+ attr_reader :schema_def_api, :schema_def_state, :names
168
+
169
+ # @private
170
+ def initialize(schema_def_api, schema_def_state)
171
+ @schema_def_api = schema_def_api
172
+ @schema_def_state = schema_def_state
173
+ @names = schema_def_state.schema_elements
174
+ end
175
+
176
+ # @private
177
+ def register_built_in_types
178
+ register_directives
179
+ register_standard_graphql_scalars
180
+ register_custom_elastic_graph_scalars
181
+ register_enum_types
182
+ register_date_and_time_grouped_by_types
183
+ register_standard_elastic_graph_types
184
+ end
185
+
186
+ private
187
+
188
+ def register_directives
189
+ # Note: The `eg` prefix is being used based on a GraphQL Spec recommendation:
190
+ # http://spec.graphql.org/October2021/#sec-Type-System.Directives.Custom-Directives
191
+ schema_def_api.raw_sdl <<~EOS
192
+ """
193
+ Indicates an upper bound on how quickly a query must respond to meet the service-level objective.
194
+ ElasticGraph will log a "good event" message if the query latency is less than or equal to this value,
195
+ and a "bad event" message if the query latency is greater than this value. These messages can be used
196
+ to drive an SLO dashboard.
197
+
198
+ Note that the latency compared against this only contains processing time within ElasticGraph itself.
199
+ Any time spent on sending the request or response over the network is not included in the comparison.
200
+ """
201
+ directive @#{names.eg_latency_slo}(#{names.ms}: Int!) on QUERY
202
+ EOS
203
+ end
204
+
205
+ def register_standard_elastic_graph_types
206
+ # This is a special filter on a `String` type, so we don't have a `Text` scalar to generate it from.
207
+ schema_def_state.factory.build_standard_filter_input_types_for_index_leaf_type("String", name_prefix: "Text") do |t|
208
+ # We can't support filtering on `null` within a list, so make the field non-nullable when it's the
209
+ # `ListElementFilterInput` type. See scalar_type.rb for a larger comment explaining the rationale behind this.
210
+ equal_to_any_of_type = t.type_ref.list_element_filter_input? ? "[String!]" : "[String]"
211
+ t.field names.equal_to_any_of, equal_to_any_of_type do |f|
212
+ f.documentation ScalarType::EQUAL_TO_ANY_OF_DOC
213
+ end
214
+
215
+ t.field names.matches, "String" do |f|
216
+ f.documentation <<~EOS
217
+ Matches records where the field value matches the provided value using full text search.
218
+
219
+ Will be ignored when `null` is passed.
220
+ EOS
221
+
222
+ f.directive "deprecated", reason: "Use `#{names.matches_query}` instead."
223
+ end
224
+
225
+ t.field names.matches_query, schema_def_state.type_ref("MatchesQuery").as_filter_input.name do |f|
226
+ f.documentation <<~EOS
227
+ Matches records where the field value matches the provided query using full text search.
228
+ This is more lenient than `#{names.matches_phrase}`: the order of terms is ignored, and,
229
+ by default, only one search term is required to be in the field value.
230
+
231
+ Will be ignored when `null` is passed.
232
+ EOS
233
+ end
234
+
235
+ t.field names.matches_phrase, schema_def_state.type_ref("MatchesPhrase").as_filter_input.name do |f|
236
+ f.documentation <<~EOS
237
+ Matches records where the field value has a phrase matching the provided phrase using
238
+ full text search. This is stricter than `#{names.matches_query}`: all terms must match
239
+ and be in the same order as the provided phrase.
240
+
241
+ Will be ignored when `null` is passed.
242
+ EOS
243
+ end
244
+ end.each do |input_type|
245
+ field_type = input_type.type_ref.list_filter_input? ? "[String]" : "String"
246
+ input_type.documentation <<~EOS
247
+ Input type used to specify filters on `#{field_type}` fields that have been indexed for full text search.
248
+
249
+ Will be ignored if passed as an empty object (or as `null`).
250
+ EOS
251
+
252
+ register_input_type(input_type)
253
+ end
254
+
255
+ register_filter "MatchesQuery" do |t|
256
+ t.documentation <<~EOS
257
+ Input type used to specify parameters for the `#{names.matches_query}` filtering operator.
258
+
259
+ Will be ignored if passed as `null`.
260
+ EOS
261
+
262
+ t.field names.query, "String!" do |f|
263
+ f.documentation "The input query to search for."
264
+ end
265
+
266
+ t.field names.allowed_edits_per_term, "MatchesQueryAllowedEditsPerTerm!" do |f|
267
+ f.documentation <<~EOS
268
+ Number of allowed modifications per term to arrive at a match. For example, if set to 'ONE', the input
269
+ term 'glue' would match 'blue' but not 'clued', since the latter requires two modifications.
270
+ EOS
271
+
272
+ f.default "DYNAMIC"
273
+ end
274
+
275
+ t.field names.require_all_terms, "Boolean!" do |f|
276
+ f.documentation <<~EOS
277
+ Set to `true` to match only if all terms in `#{names.query}` are found, or
278
+ `false` to only require one term to be found.
279
+ EOS
280
+
281
+ f.default false
282
+ end
283
+ end
284
+
285
+ register_filter "MatchesPhrase" do |t|
286
+ t.documentation <<~EOS
287
+ Input type used to specify parameters for the `#{names.matches_phrase}` filtering operator.
288
+
289
+ Will be ignored if passed as `null`.
290
+ EOS
291
+
292
+ t.field names.phrase, "String!" do |f|
293
+ f.documentation "The input phrase to search for."
294
+ end
295
+ end
296
+
297
+ # This is defined as a built-in ElasticGraph type so that we can leverage Elasticsearch/OpenSearch GeoLocation features
298
+ # based on the geo-point type:
299
+ # https://www.elastic.co/guide/en/elasticsearch/reference/7.10/geo-point.html
300
+ schema_def_api.object_type "GeoLocation" do |t|
301
+ t.documentation "Geographic coordinates representing a location on the Earth's surface."
302
+
303
+ # As per the Elasticsearch docs, the field MUST come in named `lat` in Elastisearch (but we want the full name in GraphQL).
304
+ t.field names.latitude, "Float", name_in_index: "lat" do |f|
305
+ f.documentation "Angular distance north or south of the Earth's equator, measured in degrees from -90 to +90."
306
+
307
+ # Note: we use `nullable: false` because we index it as a single `geo_point` field, and therefore can't
308
+ # support a `latitude` without a `longitude` or vice-versa.
309
+ f.json_schema minimum: -90, maximum: 90, nullable: false
310
+ end
311
+
312
+ # As per the Elasticsearch docs, the field MUST come in named `lon` in Elastisearch (but we want the full name in GraphQL).
313
+ t.field names.longitude, "Float", name_in_index: "lon" do |f|
314
+ f.documentation "Angular distance east or west of the Prime Meridian at Greenwich, UK, measured in degrees from -180 to +180."
315
+
316
+ # Note: we use `nullable: false` because we index it as a single `geo_point` field, and therefore can't
317
+ # support a `latitude` without a `longitude` or vice-versa.
318
+ f.json_schema minimum: -180, maximum: 180, nullable: false
319
+ end
320
+
321
+ t.mapping type: "geo_point"
322
+ end
323
+
324
+ # Note: `GeoLocation` is an index leaf type even though it is a GraphQL object type. In the datastore,
325
+ # it is indexed as an indivisible `geo_point` field.
326
+ schema_def_state.factory.build_standard_filter_input_types_for_index_leaf_type("GeoLocation") do |t|
327
+ t.field names.near, schema_def_state.type_ref("GeoLocationDistance").as_filter_input.name do |f|
328
+ f.documentation <<~EOS
329
+ Matches records where the field's geographic location is within a specified distance from the
330
+ location identified by `#{names.latitude}` and `#{names.longitude}`.
331
+
332
+ Will be ignored when `null` or an empty object is passed.
333
+ EOS
334
+ end
335
+ end.each { |input_filter| register_input_type(input_filter) }
336
+
337
+ register_filter "GeoLocationDistance" do |t|
338
+ t.documentation "Input type used to specify distance filtering parameters on `GeoLocation` fields."
339
+
340
+ # Note: all 4 of these fields (latitude, longitude, max_distance, unit) are required for this
341
+ # filter to operator properly, so they are all non-null fields.
342
+
343
+ t.field names.latitude, "Float!" do |f|
344
+ f.documentation "Angular distance north or south of the Earth's equator, measured in degrees from -90 to +90."
345
+ end
346
+
347
+ t.field names.longitude, "Float!" do |f|
348
+ f.documentation "Angular distance east or west of the Prime Meridian at Greenwich, UK, measured in degrees from -180 to +180."
349
+ end
350
+
351
+ t.field names.max_distance, "Float!" do |f|
352
+ f.documentation <<~EOS
353
+ Maximum distance (of the provided `#{names.unit}`) to consider "near" the location identified
354
+ by `#{names.latitude}` and `#{names.longitude}`.
355
+ EOS
356
+ end
357
+
358
+ t.field names.unit, "DistanceUnit!" do |f|
359
+ f.documentation "Determines the unit of the specified `#{names.max_distance}`."
360
+ end
361
+
362
+ # any_of/not don't really make sense on this filter because it doesn't make sense to
363
+ # apply an OR operator or negation to the fields of this type since they are all an
364
+ # indivisible part of a single filter operation on a specific field. So we remove them
365
+ # here.
366
+ remove_any_of_and_not_filter_operators_on(t)
367
+ end
368
+
369
+ # Note: `has_next_page`/`has_previous_page` are required to be non-null by the relay
370
+ # spec: https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo.
371
+ # The cursors are required to be non-null by the relay spec, but it is nonsensical
372
+ # when dealing with an empty collection, and relay itself implements it to be null:
373
+ #
374
+ # https://github.com/facebook/relay/commit/a17b462b3ff7355df4858a42ddda75f58c161302
375
+ #
376
+ # For more context, see:
377
+ # https://github.com/rmosolgo/graphql-ruby/pull/2886#issuecomment-618414736
378
+ # https://github.com/facebook/relay/pull/2655
379
+ #
380
+ # For now we will make the cursor fields nullable. It would be a breaking change
381
+ # to go from non-null to null, but is not a breaking change to make it non-null
382
+ # in the future.
383
+ register_framework_object_type "PageInfo" do |t|
384
+ t.documentation <<~EOS
385
+ Provides information about the specific fetched page. This implements the `PageInfo`
386
+ specification from the [Relay GraphQL Cursor Connections
387
+ Specification](https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo).
388
+ EOS
389
+
390
+ t.field names.has_next_page, "Boolean!", graphql_only: true do |f|
391
+ f.documentation "Indicates if there is another page of results available after the current one."
392
+ end
393
+
394
+ t.field names.has_previous_page, "Boolean!", graphql_only: true do |f|
395
+ f.documentation "Indicates if there is another page of results available before the current one."
396
+ end
397
+
398
+ t.field names.start_cursor, "Cursor", graphql_only: true do |f|
399
+ f.documentation <<~EOS
400
+ The `Cursor` of the first edge of the current page. This can be passed in the next query as
401
+ a `before` argument to paginate backwards.
402
+ EOS
403
+ end
404
+
405
+ t.field names.end_cursor, "Cursor", graphql_only: true do |f|
406
+ f.documentation <<~EOS
407
+ The `Cursor` of the last edge of the current page. This can be passed in the next query as
408
+ a `after` argument to paginate forwards.
409
+ EOS
410
+ end
411
+ end
412
+
413
+ schema_def_api.factory.new_input_type("DateTimeGroupingOffsetInput") do |t|
414
+ t.documentation <<~EOS
415
+ Input type offered when grouping on `DateTime` fields, representing the amount of offset
416
+ (positive or negative) to shift the `DateTime` boundaries of each grouping bucket.
417
+
418
+ For example, when grouping by `WEEK`, you can shift by 1 day to change
419
+ what day-of-week weeks are considered to start on.
420
+ EOS
421
+
422
+ t.field names.amount, "Int!" do |f|
423
+ f.documentation "Number (positive or negative) of the given `#{names.unit}` to offset the boundaries of the `DateTime` groupings."
424
+ end
425
+
426
+ t.field names.unit, "DateTimeUnit!" do |f|
427
+ f.documentation "Unit of offsetting to apply to the boundaries of the `DateTime` groupings."
428
+ end
429
+
430
+ register_input_type(t)
431
+ end
432
+
433
+ schema_def_api.factory.new_input_type("DateGroupingOffsetInput") do |t|
434
+ t.documentation <<~EOS
435
+ Input type offered when grouping on `Date` fields, representing the amount of offset
436
+ (positive or negative) to shift the `Date` boundaries of each grouping bucket.
437
+
438
+ For example, when grouping by `WEEK`, you can shift by 1 day to change
439
+ what day-of-week weeks are considered to start on.
440
+ EOS
441
+
442
+ t.field names.amount, "Int!" do |f|
443
+ f.documentation "Number (positive or negative) of the given `#{names.unit}` to offset the boundaries of the `Date` groupings."
444
+ end
445
+
446
+ t.field names.unit, "DateUnit!" do |f|
447
+ f.documentation "Unit of offsetting to apply to the boundaries of the `Date` groupings."
448
+ end
449
+
450
+ register_input_type(t)
451
+ end
452
+
453
+ schema_def_api.factory.new_input_type("DayOfWeekGroupingOffsetInput") do |t|
454
+ t.documentation <<~EOS
455
+ Input type offered when grouping on `DayOfWeek` fields, representing the amount of offset
456
+ (positive or negative) to shift the `DayOfWeek` boundaries of each grouping bucket.
457
+
458
+ For example, you can apply an offset of -2 hours to shift `DateTime` values to the prior `DayOfWeek`
459
+ when they fall between midnight and 2 AM.
460
+ EOS
461
+
462
+ t.field names.amount, "Int!" do |f|
463
+ f.documentation "Number (positive or negative) of the given `#{names.unit}` to offset the boundaries of the `DayOfWeek` groupings."
464
+ end
465
+
466
+ t.field names.unit, "DateTimeUnit!" do |f|
467
+ f.documentation "Unit of offsetting to apply to the boundaries of the `DayOfWeek` groupings."
468
+ end
469
+
470
+ register_input_type(t)
471
+ end
472
+
473
+ schema_def_api.factory.new_input_type("LocalTimeGroupingOffsetInput") do |t|
474
+ t.documentation <<~EOS
475
+ Input type offered when grouping on `LocalTime` fields, representing the amount of offset
476
+ (positive or negative) to shift the `LocalTime` boundaries of each grouping bucket.
477
+
478
+ For example, when grouping by `HOUR`, you can shift by 30 minutes to change
479
+ what minute-of-hour hours are considered to start on.
480
+ EOS
481
+
482
+ t.field names.amount, "Int!" do |f|
483
+ f.documentation "Number (positive or negative) of the given `#{names.unit}` to offset the boundaries of the `LocalTime` groupings."
484
+ end
485
+
486
+ t.field names.unit, "LocalTimeUnit!" do |f|
487
+ f.documentation "Unit of offsetting to apply to the boundaries of the `LocalTime` groupings."
488
+ end
489
+
490
+ register_input_type(t)
491
+ end
492
+
493
+ schema_def_api.factory.new_aggregated_values_type_for_index_leaf_type "NonNumeric" do |t|
494
+ t.documentation "A return type used from aggregations to provided aggregated values over non-numeric fields."
495
+ end.tap { |t| schema_def_api.state.register_object_interface_or_union_type(t) }
496
+
497
+ register_framework_object_type "AggregationCountDetail" do |t|
498
+ t.documentation "Provides detail about an aggregation `#{names.count}`."
499
+
500
+ t.field names.approximate_value, "JsonSafeLong!", graphql_only: true do |f|
501
+ f.documentation <<~EOS
502
+ The (approximate) count of documents in this aggregation bucket.
503
+
504
+ When documents in an aggregation bucket are sourced from multiple shards, the count may be only
505
+ approximate. The `#{names.upper_bound}` indicates the maximum value of the true count, but usually
506
+ the true count is much closer to this approximate value (which also provides a lower bound on the
507
+ true count).
508
+
509
+ When this approximation is known to be exact, the same value will be available from `#{names.exact_value}`
510
+ and `#{names.upper_bound}`.
511
+ EOS
512
+ end
513
+
514
+ t.field names.exact_value, "JsonSafeLong", graphql_only: true do |f|
515
+ f.documentation <<~EOS
516
+ The exact count of documents in this aggregation bucket, if an exact value can be determined.
517
+
518
+ When documents in an aggregation bucket are sourced from multiple shards, it may not be possible to
519
+ efficiently determine an exact value. When no exact value can be determined, this field will be `null`.
520
+ The `#{names.approximate_value}` field--which will never be `null`--can be used to get an approximation
521
+ for the count.
522
+ EOS
523
+ end
524
+
525
+ t.field names.upper_bound, "JsonSafeLong!", graphql_only: true do |f|
526
+ f.documentation <<~EOS
527
+ An upper bound on how large the true count of documents in this aggregation bucket could be.
528
+
529
+ When documents in an aggregation bucket are sourced from multiple shards, it may not be possible to
530
+ efficiently determine an exact value. The `#{names.approximate_value}` field provides an approximation,
531
+ and this field puts an upper bound on the true count.
532
+ EOS
533
+ end
534
+ end
535
+ end
536
+
537
+ # Registers the standard GraphQL scalar types. Note that the SDL for the scalar type itself isn't
538
+ # included in the dumped SDL, but registering it allows us to derive a filter for each,
539
+ # which we need. In addition, this lets us define the mapping and JSON schema for each standard
540
+ # scalar type.
541
+ def register_standard_graphql_scalars
542
+ schema_def_api.scalar_type "Boolean" do |t|
543
+ t.mapping type: "boolean"
544
+ t.json_schema type: "boolean"
545
+ end
546
+
547
+ schema_def_api.scalar_type "Float" do |t|
548
+ t.mapping type: "double"
549
+ t.json_schema type: "number"
550
+
551
+ t.customize_aggregated_values_type do |avt|
552
+ # not nullable, since sum(empty_set) == 0
553
+ avt.field names.approximate_sum, "Float!", graphql_only: true do |f|
554
+ f.runtime_metadata_graphql_field = f.runtime_metadata_graphql_field.with_computation_detail(
555
+ empty_bucket_value: 0,
556
+ function: :sum
557
+ )
558
+
559
+ f.documentation <<~EOS
560
+ The sum of the field values within this grouping.
561
+
562
+ As with all double-precision `Float` values, operations are subject to floating-point loss
563
+ of precision, so the value may be approximate.
564
+ EOS
565
+ end
566
+
567
+ define_exact_min_and_max_on_aggregated_values(avt, "Float") do |adjective:, full_name:|
568
+ <<~EOS
569
+ The value will be "exact" in that the aggregation computation will return
570
+ the exact value of the #{adjective} float that has been indexed, without
571
+ introducing any new imprecision. However, floats by their nature are
572
+ naturally imprecise since they cannot precisely represent all real numbers.
573
+ EOS
574
+ end
575
+
576
+ avt.field names.approximate_avg, "Float", graphql_only: true do |f|
577
+ f.runtime_metadata_graphql_field = f.runtime_metadata_graphql_field.with_computation_detail(
578
+ empty_bucket_value: nil,
579
+ function: :avg
580
+ )
581
+
582
+ f.documentation <<~EOS
583
+ The average (mean) of the field values within this grouping.
584
+
585
+ The computation of this value may introduce additional imprecision (on top of the
586
+ natural imprecision of floats) when it deals with intermediary values that are
587
+ outside the `JsonSafeLong` range (#{format_number(JSON_SAFE_LONG_MIN)} to #{format_number(JSON_SAFE_LONG_MAX)}).
588
+ EOS
589
+ end
590
+ end
591
+ end
592
+
593
+ schema_def_api.scalar_type "ID" do |t|
594
+ t.mapping type: "keyword"
595
+ t.json_schema type: "string"
596
+ end
597
+
598
+ schema_def_api.scalar_type "Int" do |t|
599
+ t.mapping type: "integer"
600
+ t.json_schema type: "integer", minimum: INT_MIN, maximum: INT_MAX
601
+
602
+ t.prepare_for_indexing_with "ElasticGraph::Indexer::IndexingPreparers::Integer",
603
+ defined_at: "elastic_graph/indexer/indexing_preparers/integer"
604
+
605
+ define_integral_aggregated_values_for(t)
606
+ end
607
+
608
+ schema_def_api.scalar_type "String" do |t|
609
+ t.mapping type: "keyword"
610
+ t.json_schema type: "string"
611
+ end
612
+ end
613
+
614
+ def register_custom_elastic_graph_scalars
615
+ schema_def_api.scalar_type "Cursor" do |t|
616
+ # Technically, we don't use the mapping or json_schema on this type since it's a return-only
617
+ # type and isn't indexed. However, `scalar_type` requires them to be set (since custom scalars
618
+ # defined by users will need those set) so we set them here to what they would be if we actually
619
+ # used them.
620
+ t.mapping type: "keyword"
621
+ t.json_schema type: "string"
622
+ t.coerce_with "ElasticGraph::GraphQL::ScalarCoercionAdapters::Cursor",
623
+ defined_at: "elastic_graph/graphql/scalar_coercion_adapters/cursor"
624
+
625
+ t.documentation <<~EOS
626
+ An opaque string value representing a specific location in a paginated connection type.
627
+ Returned cursors can be passed back in the next query via the `before` or `after`
628
+ arguments to continue paginating from that point.
629
+ EOS
630
+ end
631
+
632
+ schema_def_api.scalar_type "Date" do |t|
633
+ t.mapping type: "date", format: DATASTORE_DATE_FORMAT
634
+ t.json_schema type: "string", format: "date"
635
+ t.coerce_with "ElasticGraph::GraphQL::ScalarCoercionAdapters::Date",
636
+ defined_at: "elastic_graph/graphql/scalar_coercion_adapters/date"
637
+
638
+ t.documentation <<~EOS
639
+ A date, represented as an [ISO 8601 date string](https://en.wikipedia.org/wiki/ISO_8601).
640
+ EOS
641
+
642
+ t.customize_aggregated_values_type do |avt|
643
+ define_exact_min_max_and_approx_avg_on_aggregated_values(avt, "Date") do |adjective:, full_name:|
644
+ <<~EOS
645
+ So long as the grouping contains at least one non-null value for the
646
+ underlying indexed field, this will return an exact non-null value.
647
+ EOS
648
+ end
649
+ end
650
+ end
651
+
652
+ schema_def_api.scalar_type "DateTime" do |t|
653
+ t.mapping type: "date", format: DATASTORE_DATE_TIME_FORMAT
654
+ t.json_schema type: "string", format: "date-time"
655
+ t.coerce_with "ElasticGraph::GraphQL::ScalarCoercionAdapters::DateTime",
656
+ defined_at: "elastic_graph/graphql/scalar_coercion_adapters/date_time"
657
+
658
+ t.documentation <<~EOS
659
+ A timestamp, represented as an [ISO 8601 time string](https://en.wikipedia.org/wiki/ISO_8601).
660
+ EOS
661
+
662
+ date_time_time_of_day_ref = schema_def_state.type_ref("#{t.type_ref}TimeOfDay")
663
+
664
+ t.customize_derived_types(
665
+ t.type_ref.as_filter_input.to_final_form(as_input: true).name,
666
+ t.type_ref.as_list_element_filter_input.to_final_form(as_input: true).name
667
+ ) do |ft|
668
+ ft.field names.time_of_day, date_time_time_of_day_ref.as_filter_input.name do |f|
669
+ f.documentation "Matches records based on the time-of-day of the `DateTime` values."
670
+ end
671
+ end
672
+
673
+ t.customize_aggregated_values_type do |avt|
674
+ define_exact_min_max_and_approx_avg_on_aggregated_values(avt, "DateTime") do |adjective:, full_name:|
675
+ <<~EOS
676
+ So long as the grouping contains at least one non-null value for the
677
+ underlying indexed field, this will return an exact non-null value.
678
+ EOS
679
+ end
680
+ end
681
+
682
+ register_filter date_time_time_of_day_ref.name do |t|
683
+ t.documentation <<~EOS
684
+ Input type used to specify filters on the time-of-day of `DateTime` fields.
685
+
686
+ Will be ignored if passed as an empty object (or as `null`).
687
+ EOS
688
+
689
+ fixup_doc = ->(doc_string) do
690
+ doc_string.sub("the field value", "the time of day of the `DateTime` field value")
691
+ end
692
+
693
+ # Unlike a normal `equal_to_any_of` (which allows nullable elements to allow filtering to null values), we make
694
+ # it non-nullable here because it's nonsensical to filter to where a DateTime's time-of-day is null.
695
+ t.field names.equal_to_any_of, "[LocalTime!]" do |f|
696
+ f.documentation fixup_doc.call(ScalarType::EQUAL_TO_ANY_OF_DOC)
697
+ end
698
+
699
+ t.field names.gt, "LocalTime" do |f|
700
+ f.documentation fixup_doc.call(ScalarType::GT_DOC)
701
+ end
702
+
703
+ t.field names.gte, "LocalTime" do |f|
704
+ f.documentation fixup_doc.call(ScalarType::GTE_DOC)
705
+ end
706
+
707
+ t.field names.lt, "LocalTime" do |f|
708
+ f.documentation fixup_doc.call(ScalarType::LT_DOC)
709
+ end
710
+
711
+ t.field names.lte, "LocalTime" do |f|
712
+ f.documentation fixup_doc.call(ScalarType::LTE_DOC)
713
+ end
714
+
715
+ t.field names.time_zone, "TimeZone!" do |f|
716
+ f.documentation "TimeZone to use when comparing the `DateTime` values against the provided `LocalTime` values."
717
+ f.default "UTC"
718
+ end
719
+
720
+ # With our initial implementation of `time_of_day` filtering, it's tricky to support `any_of`/`not` within
721
+ # the `time_of_day: {...}` input object. They are still supported outside of `time_of_day` (on the parent
722
+ # input object) so no functionality is losts by omitting these. Also, this aligns with our `GeoLocationDistanceFilterInput`
723
+ # which is a similarly complex filter where we didn't include them.
724
+ remove_any_of_and_not_filter_operators_on(t)
725
+ end
726
+ end
727
+
728
+ schema_def_api.scalar_type "LocalTime" do |t|
729
+ t.documentation <<~EOS
730
+ A local time such as `"23:59:33"` or `"07:20:47.454"` without a time zone or offset, formatted based on the
731
+ [partial-time portion of RFC3339](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6).
732
+ EOS
733
+
734
+ t.coerce_with "ElasticGraph::GraphQL::ScalarCoercionAdapters::LocalTime",
735
+ defined_at: "elastic_graph/graphql/scalar_coercion_adapters/local_time"
736
+
737
+ t.mapping type: "date", format: "HH:mm:ss||HH:mm:ss.S||HH:mm:ss.SS||HH:mm:ss.SSS"
738
+
739
+ t.json_schema type: "string", pattern: VALID_LOCAL_TIME_JSON_SCHEMA_PATTERN
740
+
741
+ t.customize_aggregated_values_type do |avt|
742
+ define_exact_min_max_and_approx_avg_on_aggregated_values(avt, "LocalTime") do |adjective:, full_name:|
743
+ <<~EOS
744
+ So long as the grouping contains at least one non-null value for the
745
+ underlying indexed field, this will return an exact non-null value.
746
+ EOS
747
+ end
748
+ end
749
+ end
750
+
751
+ schema_def_api.scalar_type "TimeZone" do |t|
752
+ t.mapping type: "keyword"
753
+ t.json_schema type: "string", enum: GraphQL::ScalarCoercionAdapters::VALID_TIME_ZONES.to_a
754
+ t.coerce_with "ElasticGraph::GraphQL::ScalarCoercionAdapters::TimeZone",
755
+ defined_at: "elastic_graph/graphql/scalar_coercion_adapters/time_zone"
756
+
757
+ t.documentation <<~EOS
758
+ An [IANA time zone identifier](https://www.iana.org/time-zones), such as `America/Los_Angeles` or `UTC`.
759
+
760
+ For a full list of valid identifiers, see the [wikipedia article](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List).
761
+ EOS
762
+ end
763
+
764
+ schema_def_api.scalar_type "Untyped" do |t|
765
+ # Allow any JSON for this type. The list of supported types is taken from:
766
+ #
767
+ # https://github.com/json-schema-org/json-schema-spec/blob/draft-07/schema.json#L23-L29
768
+ #
769
+ # ...except we are omitting `null` here; it'll be added by the nullability decorator if the field is defined as nullable.
770
+ t.json_schema type: ["array", "boolean", "integer", "number", "object", "string"]
771
+
772
+ # In the index we store this as a JSON string in a `keyword` field.
773
+ t.mapping type: "keyword"
774
+
775
+ t.coerce_with "ElasticGraph::GraphQL::ScalarCoercionAdapters::Untyped",
776
+ defined_at: "elastic_graph/graphql/scalar_coercion_adapters/untyped"
777
+
778
+ t.prepare_for_indexing_with "ElasticGraph::Indexer::IndexingPreparers::Untyped",
779
+ defined_at: "elastic_graph/indexer/indexing_preparers/untyped"
780
+
781
+ t.documentation <<~EOS
782
+ A custom scalar type that allows any type of data, including:
783
+
784
+ - strings
785
+ - numbers
786
+ - objects and arrays (nested as deeply as you like)
787
+ - booleans
788
+
789
+ Note: fields of this type are effectively untyped. We recommend it only be used for
790
+ parts of your schema that can't be statically typed.
791
+ EOS
792
+ end
793
+
794
+ schema_def_api.scalar_type "JsonSafeLong" do |t|
795
+ t.mapping type: "long"
796
+ t.json_schema type: "integer", minimum: JSON_SAFE_LONG_MIN, maximum: JSON_SAFE_LONG_MAX
797
+ t.coerce_with "ElasticGraph::GraphQL::ScalarCoercionAdapters::JsonSafeLong",
798
+ defined_at: "elastic_graph/graphql/scalar_coercion_adapters/longs"
799
+
800
+ t.prepare_for_indexing_with "ElasticGraph::Indexer::IndexingPreparers::Integer",
801
+ defined_at: "elastic_graph/indexer/indexing_preparers/integer"
802
+
803
+ t.documentation <<~EOS
804
+ A numeric type for large integer values that can serialize safely as JSON.
805
+
806
+ While JSON itself has no hard limit on the size of integers, the RFC-7159 spec
807
+ mentions that values outside of the range #{format_number(JSON_SAFE_LONG_MIN)} (-(2^53) + 1)
808
+ to #{format_number(JSON_SAFE_LONG_MAX)} (2^53 - 1) may not be interopable with all JSON
809
+ implementations. As it turns out, the number implementation used by JavaScript
810
+ has this issue. When you parse a JSON string that contains a numeric value like
811
+ `4693522397653681111`, the parsed result will contain a rounded value like
812
+ `4693522397653681000`.
813
+
814
+ While this is entirely a client-side problem, we want to preserve maximum compatibility
815
+ with common client languages. Given the ubiquity of GraphiQL as a GraphQL client,
816
+ we want to avoid this problem.
817
+
818
+ Our solution is to support two separate types:
819
+
820
+ - This type (`JsonSafeLong`) is serialized as a number, but limits values to the safely
821
+ serializable range.
822
+ - The `LongString` type supports long values that use all 64 bits, but serializes as a
823
+ string rather than a number, avoiding the JavaScript compatibility problems.
824
+
825
+ For more background, see the [JavaScript `Number.MAX_SAFE_INTEGER`
826
+ docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER).
827
+ EOS
828
+
829
+ define_integral_aggregated_values_for(t)
830
+ end
831
+
832
+ schema_def_api.scalar_type "LongString" do |t|
833
+ # Note: while this type is returned from GraphQL queries as a string, we still
834
+ # require it to be an integer in the JSON documents we index. We want min/max
835
+ # validation on input (to avoid ingesting values that are larger than we can
836
+ # handle). This is easy to do if we ingest these values as numbers, but hard
837
+ # to do if we ingest them as strings. (The `pattern` regex to validate the range
838
+ # would be *extremely* complicated).
839
+ t.mapping type: "long"
840
+ t.json_schema type: "integer", minimum: LONG_STRING_MIN, maximum: LONG_STRING_MAX
841
+ t.coerce_with "ElasticGraph::GraphQL::ScalarCoercionAdapters::LongString",
842
+ defined_at: "elastic_graph/graphql/scalar_coercion_adapters/longs"
843
+ t.prepare_for_indexing_with "ElasticGraph::Indexer::IndexingPreparers::Integer",
844
+ defined_at: "elastic_graph/indexer/indexing_preparers/integer"
845
+
846
+ t.documentation <<~EOS
847
+ A numeric type for large integer values in the inclusive range -2^63
848
+ (#{format_number(LONG_STRING_MIN)}) to (2^63 - 1) (#{format_number(LONG_STRING_MAX)}).
849
+
850
+ Note that `LongString` values are serialized as strings within JSON, to avoid
851
+ interopability problems with JavaScript. If you want a large integer type that
852
+ serializes within JSON as a number, use `JsonSafeLong`.
853
+ EOS
854
+
855
+ t.customize_aggregated_values_type do |avt|
856
+ # not nullable, since sum(empty_set) == 0
857
+ avt.field names.approximate_sum, "Float!", graphql_only: true do |f|
858
+ f.runtime_metadata_graphql_field = f.runtime_metadata_graphql_field.with_computation_detail(
859
+ empty_bucket_value: 0,
860
+ function: :sum
861
+ )
862
+
863
+ f.documentation <<~EOS
864
+ The (approximate) sum of the field values within this grouping.
865
+
866
+ Sums of large `LongString` values can result in overflow, where the exact sum cannot
867
+ fit in a `LongString` return value. This field, as a double-precision `Float`, can
868
+ represent larger sums, but the value may only be approximate.
869
+ EOS
870
+ end
871
+
872
+ avt.field names.exact_sum, "JsonSafeLong", graphql_only: true do |f|
873
+ f.runtime_metadata_graphql_field = f.runtime_metadata_graphql_field.with_computation_detail(
874
+ empty_bucket_value: 0,
875
+ function: :sum
876
+ )
877
+
878
+ f.documentation <<~EOS
879
+ The exact sum of the field values within this grouping, if it fits in a `JsonSafeLong`.
880
+
881
+ Sums of large `LongString` values can result in overflow, where the exact sum cannot
882
+ fit in a `JsonSafeLong`. In that case, `null` will be returned, and `#{names.approximate_sum}`
883
+ can be used to get an approximate value.
884
+ EOS
885
+ end
886
+
887
+ define_exact_min_and_max_on_aggregated_values(avt, "JsonSafeLong") do |adjective:, full_name:|
888
+ approx_name = (full_name == "minimum") ? names.approximate_min : names.approximate_max
889
+
890
+ <<~EOS
891
+ So long as the grouping contains at least one non-null value, and no values exceed the
892
+ `JsonSafeLong` range in the underlying indexed field, this will return an exact non-null value.
893
+
894
+ If no non-null values are available, or if the #{full_name} value is outside the `JsonSafeLong`
895
+ range, `null` will be returned. `#{approx_name}` can be used to differentiate between these
896
+ cases and to get an approximate value.
897
+ EOS
898
+ end
899
+
900
+ {
901
+ names.exact_min => [:min, "minimum", names.approximate_min, "smallest"],
902
+ names.exact_max => [:max, "maximum", names.approximate_max, "largest"]
903
+ }.each do |exact_name, (func, full_name, approx_name, adjective)|
904
+ avt.field approx_name, "LongString", graphql_only: true do |f|
905
+ f.runtime_metadata_graphql_field = f.runtime_metadata_graphql_field.with_computation_detail(
906
+ empty_bucket_value: nil,
907
+ function: func
908
+ )
909
+
910
+ f.documentation <<~EOS
911
+ The #{full_name} of the field values within this grouping.
912
+
913
+ The aggregation computation performed to identify the #{adjective} value is not able
914
+ to maintain exact precision when dealing with values that are outside the `JsonSafeLong`
915
+ range (#{format_number(JSON_SAFE_LONG_MIN)} to #{format_number(JSON_SAFE_LONG_MAX)}).
916
+ In that case, the `#{exact_name}` field will return `null`, but this field will provide
917
+ a value which may be approximate.
918
+ EOS
919
+ end
920
+ end
921
+
922
+ avt.field names.approximate_avg, "Float", graphql_only: true do |f|
923
+ f.runtime_metadata_graphql_field = f.runtime_metadata_graphql_field.with_computation_detail(
924
+ empty_bucket_value: nil,
925
+ function: :avg
926
+ )
927
+
928
+ f.documentation <<~EOS
929
+ The average (mean) of the field values within this grouping.
930
+
931
+ Note that the returned value is approximate. Imprecision can be introduced by the computation if
932
+ any intermediary values fall outside the `JsonSafeLong` range (#{format_number(JSON_SAFE_LONG_MIN)}
933
+ to #{format_number(JSON_SAFE_LONG_MAX)}).
934
+ EOS
935
+ end
936
+ end
937
+ end
938
+ end
939
+
940
+ def register_enum_types
941
+ # Elasticsearch and OpenSearch treat weeks as beginning on Monday for date histogram aggregations.
942
+ # Note that I can't find clear documentation on this.
943
+ #
944
+ # https://www.elastic.co/guide/en/elasticsearch/reference/7.10/search-aggregations-bucket-datehistogram-aggregation.html#calendar_intervals
945
+ #
946
+ # > One week is the interval between the start day_of_week:hour:minute:second and
947
+ # > the same day of the week and time of the following week in the specified time zone.
948
+ #
949
+ # However, we have observed that this is how it behaves. We verify it in this test:
950
+ # elasticgraph-graphql/spec/acceptance/elasticgraph_graphql_spec.rb
951
+ es_first_day_of_week = "Monday"
952
+
953
+ # TODO: Drop support for legacy grouping schema
954
+ schema_def_api.enum_type "DateGroupingGranularity" do |t|
955
+ t.documentation <<~EOS
956
+ Enumerates the supported granularities of a `Date`.
957
+ EOS
958
+
959
+ t.value "YEAR" do |v|
960
+ v.documentation "The year a `Date` falls in."
961
+ v.update_runtime_metadata datastore_value: "year"
962
+ end
963
+
964
+ t.value "QUARTER" do |v|
965
+ v.documentation "The quarter a `Date` falls in."
966
+ v.update_runtime_metadata datastore_value: "quarter"
967
+ end
968
+
969
+ t.value "MONTH" do |v|
970
+ v.documentation "The month a `Date` falls in."
971
+ v.update_runtime_metadata datastore_value: "month"
972
+ end
973
+
974
+ t.value "WEEK" do |v|
975
+ v.documentation "The week, beginning on #{es_first_day_of_week}, a `Date` falls in."
976
+ v.update_runtime_metadata datastore_value: "week"
977
+ end
978
+
979
+ t.value "DAY" do |v|
980
+ v.documentation "The exact day of a `Date`."
981
+ v.update_runtime_metadata datastore_value: "day"
982
+ end
983
+ end
984
+
985
+ schema_def_api.enum_type "DateGroupingTruncationUnit" do |t|
986
+ t.documentation <<~EOS
987
+ Enumerates the supported truncation units of a `Date`.
988
+ EOS
989
+
990
+ t.value "YEAR" do |v|
991
+ v.documentation "The year a `Date` falls in."
992
+ v.update_runtime_metadata datastore_value: "year"
993
+ end
994
+
995
+ t.value "QUARTER" do |v|
996
+ v.documentation "The quarter a `Date` falls in."
997
+ v.update_runtime_metadata datastore_value: "quarter"
998
+ end
999
+
1000
+ t.value "MONTH" do |v|
1001
+ v.documentation "The month a `Date` falls in."
1002
+ v.update_runtime_metadata datastore_value: "month"
1003
+ end
1004
+
1005
+ t.value "WEEK" do |v|
1006
+ v.documentation "The week, beginning on #{es_first_day_of_week}, a `Date` falls in."
1007
+ v.update_runtime_metadata datastore_value: "week"
1008
+ end
1009
+
1010
+ t.value "DAY" do |v|
1011
+ v.documentation "The exact day of a `Date`."
1012
+ v.update_runtime_metadata datastore_value: "day"
1013
+ end
1014
+ end
1015
+
1016
+ # TODO: Drop support for legacy grouping schema
1017
+ schema_def_api.enum_type "DateTimeGroupingGranularity" do |t|
1018
+ t.documentation <<~EOS
1019
+ Enumerates the supported granularities of a `DateTime`.
1020
+ EOS
1021
+
1022
+ t.value "YEAR" do |v|
1023
+ v.documentation "The year a `DateTime` falls in."
1024
+ v.update_runtime_metadata datastore_value: "year"
1025
+ end
1026
+
1027
+ t.value "QUARTER" do |v|
1028
+ v.documentation "The quarter a `DateTime` falls in."
1029
+ v.update_runtime_metadata datastore_value: "quarter"
1030
+ end
1031
+
1032
+ t.value "MONTH" do |v|
1033
+ v.documentation "The month a `DateTime` falls in."
1034
+ v.update_runtime_metadata datastore_value: "month"
1035
+ end
1036
+
1037
+ t.value "WEEK" do |v|
1038
+ v.documentation "The week, beginning on #{es_first_day_of_week}, a `DateTime` falls in."
1039
+ v.update_runtime_metadata datastore_value: "week"
1040
+ end
1041
+
1042
+ t.value "DAY" do |v|
1043
+ v.documentation "The day a `DateTime` falls in."
1044
+ v.update_runtime_metadata datastore_value: "day"
1045
+ end
1046
+
1047
+ t.value "HOUR" do |v|
1048
+ v.documentation "The hour a `DateTime` falls in."
1049
+ v.update_runtime_metadata datastore_value: "hour"
1050
+ end
1051
+
1052
+ t.value "MINUTE" do |v|
1053
+ v.documentation "The minute a `DateTime` falls in."
1054
+ v.update_runtime_metadata datastore_value: "minute"
1055
+ end
1056
+
1057
+ t.value "SECOND" do |v|
1058
+ v.documentation "The second a `DateTime` falls in."
1059
+ v.update_runtime_metadata datastore_value: "second"
1060
+ end
1061
+ end
1062
+
1063
+ schema_def_api.enum_type "DateTimeGroupingTruncationUnit" do |t|
1064
+ t.documentation <<~EOS
1065
+ Enumerates the supported truncation units of a `DateTime`.
1066
+ EOS
1067
+
1068
+ t.value "YEAR" do |v|
1069
+ v.documentation "The year a `DateTime` falls in."
1070
+ v.update_runtime_metadata datastore_value: "year"
1071
+ end
1072
+
1073
+ t.value "QUARTER" do |v|
1074
+ v.documentation "The quarter a `DateTime` falls in."
1075
+ v.update_runtime_metadata datastore_value: "quarter"
1076
+ end
1077
+
1078
+ t.value "MONTH" do |v|
1079
+ v.documentation "The month a `DateTime` falls in."
1080
+ v.update_runtime_metadata datastore_value: "month"
1081
+ end
1082
+
1083
+ t.value "WEEK" do |v|
1084
+ v.documentation "The week, beginning on #{es_first_day_of_week}, a `DateTime` falls in."
1085
+ v.update_runtime_metadata datastore_value: "week"
1086
+ end
1087
+
1088
+ t.value "DAY" do |v|
1089
+ v.documentation "The day a `DateTime` falls in."
1090
+ v.update_runtime_metadata datastore_value: "day"
1091
+ end
1092
+
1093
+ t.value "HOUR" do |v|
1094
+ v.documentation "The hour a `DateTime` falls in."
1095
+ v.update_runtime_metadata datastore_value: "hour"
1096
+ end
1097
+
1098
+ t.value "MINUTE" do |v|
1099
+ v.documentation "The minute a `DateTime` falls in."
1100
+ v.update_runtime_metadata datastore_value: "minute"
1101
+ end
1102
+
1103
+ t.value "SECOND" do |v|
1104
+ v.documentation "The second a `DateTime` falls in."
1105
+ v.update_runtime_metadata datastore_value: "second"
1106
+ end
1107
+ end
1108
+
1109
+ schema_def_api.enum_type "LocalTimeGroupingTruncationUnit" do |t|
1110
+ t.documentation <<~EOS
1111
+ Enumerates the supported truncation units of a `LocalTime`.
1112
+ EOS
1113
+
1114
+ t.value "HOUR" do |v|
1115
+ v.documentation "The hour a `LocalTime` falls in."
1116
+ v.update_runtime_metadata datastore_value: "hour"
1117
+ end
1118
+
1119
+ t.value "MINUTE" do |v|
1120
+ v.documentation "The minute a `LocalTime` falls in."
1121
+ v.update_runtime_metadata datastore_value: "minute"
1122
+ end
1123
+
1124
+ t.value "SECOND" do |v|
1125
+ v.documentation "The second a `LocalTime` falls in."
1126
+ v.update_runtime_metadata datastore_value: "second"
1127
+ end
1128
+ end
1129
+
1130
+ schema_def_api.enum_type "DistanceUnit" do |t|
1131
+ t.documentation "Enumerates the supported distance units."
1132
+
1133
+ # Values here are taken from: https://www.elastic.co/guide/en/elasticsearch/reference/7.10/common-options.html#distance-units
1134
+ t.value "MILE" do |v|
1135
+ v.documentation "A United States customary unit of 5,280 feet."
1136
+ v.update_runtime_metadata datastore_abbreviation: :mi
1137
+ end
1138
+
1139
+ t.value "YARD" do |v|
1140
+ v.documentation "A United States customary unit of 3 feet."
1141
+ v.update_runtime_metadata datastore_abbreviation: :yd
1142
+ end
1143
+
1144
+ t.value "FOOT" do |v|
1145
+ v.documentation "A United States customary unit of 12 inches."
1146
+ v.update_runtime_metadata datastore_abbreviation: :ft
1147
+ end
1148
+
1149
+ t.value "INCH" do |v|
1150
+ v.documentation "A United States customary unit equal to 1/12th of a foot."
1151
+ v.update_runtime_metadata datastore_abbreviation: :in
1152
+ end
1153
+
1154
+ t.value "KILOMETER" do |v|
1155
+ v.documentation "A metric system unit equal to 1,000 meters."
1156
+ v.update_runtime_metadata datastore_abbreviation: :km
1157
+ end
1158
+
1159
+ t.value "METER" do |v|
1160
+ v.documentation "The base unit of length in the metric system."
1161
+ v.update_runtime_metadata datastore_abbreviation: :m
1162
+ end
1163
+
1164
+ t.value "CENTIMETER" do |v|
1165
+ v.documentation "A metric system unit equal to 1/100th of a meter."
1166
+ v.update_runtime_metadata datastore_abbreviation: :cm
1167
+ end
1168
+
1169
+ t.value "MILLIMETER" do |v|
1170
+ v.documentation "A metric system unit equal to 1/1,000th of a meter."
1171
+ v.update_runtime_metadata datastore_abbreviation: :mm
1172
+ end
1173
+
1174
+ t.value "NAUTICAL_MILE" do |v|
1175
+ v.documentation "An international unit of length used for air, marine, and space navigation. Equivalent to 1,852 meters."
1176
+ v.update_runtime_metadata datastore_abbreviation: :nmi
1177
+ end
1178
+ end
1179
+
1180
+ schema_def_api.enum_type "DateTimeUnit" do |t|
1181
+ t.documentation "Enumeration of `DateTime` units."
1182
+
1183
+ # Values here are taken from: https://www.elastic.co/guide/en/elasticsearch/reference/7.10/common-options.html#time-units
1184
+ t.value "DAY" do |v|
1185
+ v.documentation "The time period of a full rotation of the Earth with respect to the Sun."
1186
+ v.update_runtime_metadata datastore_abbreviation: :d, datastore_value: 86_400_000
1187
+ end
1188
+
1189
+ t.value "HOUR" do |v|
1190
+ v.documentation "1/24th of a day."
1191
+ v.update_runtime_metadata datastore_abbreviation: :h, datastore_value: 3_600_000
1192
+ end
1193
+
1194
+ t.value "MINUTE" do |v|
1195
+ v.documentation "1/60th of an hour."
1196
+ v.update_runtime_metadata datastore_abbreviation: :m, datastore_value: 60_000
1197
+ end
1198
+
1199
+ t.value "SECOND" do |v|
1200
+ v.documentation "1/60th of a minute."
1201
+ v.update_runtime_metadata datastore_abbreviation: :s, datastore_value: 1_000
1202
+ end
1203
+
1204
+ t.value "MILLISECOND" do |v|
1205
+ v.documentation "1/1000th of a second."
1206
+ v.update_runtime_metadata datastore_abbreviation: :ms, datastore_value: 1
1207
+ end
1208
+
1209
+ # These units, which Elasticsearch and OpenSearch support, only make sense to use when using the
1210
+ # Date nanoseconds type:
1211
+ #
1212
+ # https://www.elastic.co/guide/en/elasticsearch/reference/7.10/date_nanos.html
1213
+ #
1214
+ # However, we currently only use the standard `Date` type, which has millisecond granularity,
1215
+ # For now these sub-millisecond granularities aren't useful to support, so we're not including
1216
+ # them at this time.
1217
+ #
1218
+ # t.value "MICROSECOND" do |v|
1219
+ # v.documentation "1/1000th of a millisecond."
1220
+ # v.update_runtime_metadata datastore_abbreviation: :micros
1221
+ # end
1222
+ #
1223
+ # t.value "NANOSECOND" do |v|
1224
+ # v.documentation "1/1000th of a microsecond."
1225
+ # v.update_runtime_metadata datastore_abbreviation: :nanos
1226
+ # end
1227
+ end
1228
+
1229
+ schema_def_api.enum_type "DateUnit" do |t|
1230
+ t.documentation "Enumeration of `Date` units."
1231
+
1232
+ # Values here are taken from: https://www.elastic.co/guide/en/elasticsearch/reference/7.10/common-options.html#time-units
1233
+ t.value "DAY" do |v|
1234
+ v.documentation "The time period of a full rotation of the Earth with respect to the Sun."
1235
+ v.update_runtime_metadata datastore_abbreviation: :d, datastore_value: 86_400_000
1236
+ end
1237
+ end
1238
+
1239
+ schema_def_api.enum_type "LocalTimeUnit" do |t|
1240
+ t.documentation "Enumeration of `LocalTime` units."
1241
+
1242
+ # Values here are taken from: https://www.elastic.co/guide/en/elasticsearch/reference/7.10/common-options.html#time-units
1243
+ t.value "HOUR" do |v|
1244
+ v.documentation "1/24th of a day."
1245
+ v.update_runtime_metadata datastore_abbreviation: :h, datastore_value: 3_600_000
1246
+ end
1247
+
1248
+ t.value "MINUTE" do |v|
1249
+ v.documentation "1/60th of an hour."
1250
+ v.update_runtime_metadata datastore_abbreviation: :m, datastore_value: 60_000
1251
+ end
1252
+
1253
+ t.value "SECOND" do |v|
1254
+ v.documentation "1/60th of a minute."
1255
+ v.update_runtime_metadata datastore_abbreviation: :s, datastore_value: 1_000
1256
+ end
1257
+
1258
+ t.value "MILLISECOND" do |v|
1259
+ v.documentation "1/1000th of a second."
1260
+ v.update_runtime_metadata datastore_abbreviation: :ms, datastore_value: 1
1261
+ end
1262
+ end
1263
+
1264
+ schema_def_api.enum_type "MatchesQueryAllowedEditsPerTerm" do |t|
1265
+ t.documentation "Enumeration of allowed values for the `#{names.matches_query}: {#{names.allowed_edits_per_term}: ...}` filter option."
1266
+
1267
+ t.value "NONE" do |v|
1268
+ v.documentation "No allowed edits per term."
1269
+ v.update_runtime_metadata datastore_abbreviation: :"0"
1270
+ end
1271
+
1272
+ t.value "ONE" do |v|
1273
+ v.documentation "One allowed edit per term."
1274
+ v.update_runtime_metadata datastore_abbreviation: :"1"
1275
+ end
1276
+
1277
+ t.value "TWO" do |v|
1278
+ v.documentation "Two allowed edits per term."
1279
+ v.update_runtime_metadata datastore_abbreviation: :"2"
1280
+ end
1281
+
1282
+ t.value "DYNAMIC" do |v|
1283
+ v.documentation "Allowed edits per term is dynamically chosen based on the length of the term."
1284
+ v.update_runtime_metadata datastore_abbreviation: :AUTO
1285
+ end
1286
+ end
1287
+ end
1288
+
1289
+ def register_date_and_time_grouped_by_types
1290
+ # DateGroupedBy
1291
+ date = schema_def_state.type_ref("Date")
1292
+ register_framework_object_type date.as_grouped_by.name do |t|
1293
+ t.documentation "Allows for grouping `Date` values based on the desired return type."
1294
+ t.runtime_metadata_overrides = {elasticgraph_category: :date_grouped_by_object}
1295
+
1296
+ t.field names.as_date, "Date", graphql_only: true do |f|
1297
+ f.documentation "Used when grouping on the full `Date` value."
1298
+ define_date_grouping_arguments(f, omit_timezone: true)
1299
+ end
1300
+
1301
+ t.field names.as_day_of_week, "DayOfWeek", graphql_only: true do |f|
1302
+ f.documentation "An alternative to `#{names.as_date}` for when grouping on the day-of-week is desired."
1303
+ define_day_of_week_grouping_arguments(f, omit_timezone: true)
1304
+ end
1305
+ end
1306
+
1307
+ # DateTimeGroupedBy
1308
+ date_time = schema_def_state.type_ref("DateTime")
1309
+ register_framework_object_type date_time.as_grouped_by.name do |t|
1310
+ t.documentation "Allows for grouping `DateTime` values based on the desired return type."
1311
+ t.runtime_metadata_overrides = {elasticgraph_category: :date_grouped_by_object}
1312
+
1313
+ t.field names.as_date_time, "DateTime", graphql_only: true do |f|
1314
+ f.documentation "Used when grouping on the full `DateTime` value."
1315
+ define_date_time_grouping_arguments(f)
1316
+ end
1317
+
1318
+ t.field names.as_date, "Date", graphql_only: true do |f|
1319
+ f.documentation "An alternative to `#{names.as_date_time}` for when grouping on just the date is desired."
1320
+ define_date_grouping_arguments(f)
1321
+ end
1322
+
1323
+ t.field names.as_time_of_day, "LocalTime", graphql_only: true do |f|
1324
+ f.documentation "An alternative to `#{names.as_date_time}` for when grouping on just the time-of-day is desired."
1325
+ define_local_time_grouping_arguments(f)
1326
+ end
1327
+
1328
+ t.field names.as_day_of_week, "DayOfWeek", graphql_only: true do |f|
1329
+ f.documentation "An alternative to `#{names.as_date_time}` for when grouping on the day-of-week is desired."
1330
+ define_day_of_week_grouping_arguments(f)
1331
+ end
1332
+ end
1333
+
1334
+ schema_def_api.enum_type "DayOfWeek" do |t|
1335
+ t.documentation "Indicates the specific day of the week."
1336
+
1337
+ t.value "MONDAY" do |v|
1338
+ v.documentation "Monday."
1339
+ end
1340
+
1341
+ t.value "TUESDAY" do |v|
1342
+ v.documentation "Tuesday."
1343
+ end
1344
+
1345
+ t.value "WEDNESDAY" do |v|
1346
+ v.documentation "Wednesday."
1347
+ end
1348
+
1349
+ t.value "THURSDAY" do |v|
1350
+ v.documentation "Thursday."
1351
+ end
1352
+
1353
+ t.value "FRIDAY" do |v|
1354
+ v.documentation "Friday."
1355
+ end
1356
+
1357
+ t.value "SATURDAY" do |v|
1358
+ v.documentation "Saturday."
1359
+ end
1360
+
1361
+ t.value "SUNDAY" do |v|
1362
+ v.documentation "Sunday."
1363
+ end
1364
+ end
1365
+ end
1366
+
1367
+ def define_date_grouping_arguments(grouping_field, omit_timezone: false)
1368
+ define_calendar_type_grouping_arguments(grouping_field, schema_def_state.type_ref("Date"), <<~EOS, omit_timezone: omit_timezone)
1369
+ For example, when grouping by `WEEK`, you can shift by 1 day to change what day-of-week weeks are considered to start on.
1370
+ EOS
1371
+ end
1372
+
1373
+ def define_date_time_grouping_arguments(grouping_field)
1374
+ define_calendar_type_grouping_arguments(grouping_field, schema_def_state.type_ref("DateTime"), <<~EOS)
1375
+ For example, when grouping by `WEEK`, you can shift by 1 day to change what day-of-week weeks are considered to start on.
1376
+ EOS
1377
+ end
1378
+
1379
+ def define_local_time_grouping_arguments(grouping_field)
1380
+ define_calendar_type_grouping_arguments(grouping_field, schema_def_state.type_ref("LocalTime"), <<~EOS)
1381
+ For example, when grouping by `HOUR`, you can apply an offset of -5 minutes to shift `LocalTime`
1382
+ values to the prior hour when they fall between the the top of an hour and 5 after.
1383
+ EOS
1384
+ end
1385
+
1386
+ def define_day_of_week_grouping_arguments(grouping_field, omit_timezone: false)
1387
+ define_calendar_type_grouping_arguments(grouping_field, schema_def_state.type_ref("DayOfWeek"), <<~EOS, omit_timezone: omit_timezone, omit_truncation_unit: true)
1388
+ For example, you can apply an offset of -2 hours to shift `DateTime` values to the prior `DayOfWeek`
1389
+ when they fall between midnight and 2 AM.
1390
+ EOS
1391
+ end
1392
+
1393
+ def define_calendar_type_grouping_arguments(grouping_field, calendar_type, offset_example_description, omit_timezone: false, omit_truncation_unit: false)
1394
+ define_grouping_argument_offset(grouping_field, calendar_type, offset_example_description)
1395
+ define_grouping_argument_time_zone(grouping_field, calendar_type) unless omit_timezone
1396
+ define_grouping_argument_truncation_unit(grouping_field, calendar_type) unless omit_truncation_unit
1397
+ end
1398
+
1399
+ def define_grouping_argument_offset(grouping_field, calendar_type, example_description)
1400
+ grouping_field.argument schema_def_state.schema_elements.offset, "#{calendar_type.name}GroupingOffsetInput" do |a|
1401
+ a.documentation <<~EOS
1402
+ Amount of offset (positive or negative) to shift the `#{calendar_type.name}` boundaries of each grouping bucket.
1403
+
1404
+ #{example_description.strip}
1405
+ EOS
1406
+ end
1407
+ end
1408
+
1409
+ def define_grouping_argument_time_zone(grouping_field, calendar_type)
1410
+ grouping_field.argument schema_def_state.schema_elements.time_zone, "TimeZone!" do |a|
1411
+ a.documentation "The time zone to use when determining which grouping a `#{calendar_type.name}` value falls in."
1412
+ a.default "UTC"
1413
+ end
1414
+ end
1415
+
1416
+ def define_grouping_argument_truncation_unit(grouping_field, calendar_type)
1417
+ grouping_field.argument schema_def_state.schema_elements.truncation_unit, "#{calendar_type.name}GroupingTruncationUnit!" do |a|
1418
+ a.documentation "Determines the grouping truncation unit for this field."
1419
+ end
1420
+ end
1421
+
1422
+ def define_integral_aggregated_values_for(scalar_type, long_type: "JsonSafeLong")
1423
+ scalar_type_name = scalar_type.name
1424
+ scalar_type.customize_aggregated_values_type do |t|
1425
+ # not nullable, since sum(empty_set) == 0
1426
+ t.field names.approximate_sum, "Float!", graphql_only: true do |f|
1427
+ f.runtime_metadata_graphql_field = f.runtime_metadata_graphql_field.with_computation_detail(
1428
+ empty_bucket_value: 0,
1429
+ function: :sum
1430
+ )
1431
+
1432
+ f.documentation <<~EOS
1433
+ The (approximate) sum of the field values within this grouping.
1434
+
1435
+ Sums of large `#{scalar_type_name}` values can result in overflow, where the exact sum cannot
1436
+ fit in a `#{long_type}` return value. This field, as a double-precision `Float`, can
1437
+ represent larger sums, but the value may only be approximate.
1438
+ EOS
1439
+ end
1440
+
1441
+ t.field names.exact_sum, long_type, graphql_only: true do |f|
1442
+ f.runtime_metadata_graphql_field = f.runtime_metadata_graphql_field.with_computation_detail(
1443
+ empty_bucket_value: 0,
1444
+ function: :sum
1445
+ )
1446
+
1447
+ f.documentation <<~EOS
1448
+ The exact sum of the field values within this grouping, if it fits in a `#{long_type}`.
1449
+
1450
+ Sums of large `#{scalar_type_name}` values can result in overflow, where the exact sum cannot
1451
+ fit in a `#{long_type}`. In that case, `null` will be returned, and `#{names.approximate_sum}`
1452
+ can be used to get an approximate value.
1453
+ EOS
1454
+ end
1455
+
1456
+ define_exact_min_and_max_on_aggregated_values(t, scalar_type_name) do |adjective:, full_name:|
1457
+ <<~EOS
1458
+ So long as the grouping contains at least one non-null value for the
1459
+ underlying indexed field, this will return an exact non-null value.
1460
+ EOS
1461
+ end
1462
+
1463
+ t.field names.approximate_avg, "Float", graphql_only: true do |f|
1464
+ f.runtime_metadata_graphql_field = f.runtime_metadata_graphql_field.with_computation_detail(
1465
+ empty_bucket_value: nil,
1466
+ function: :avg
1467
+ )
1468
+
1469
+ f.documentation <<~EOS
1470
+ The average (mean) of the field values within this grouping.
1471
+
1472
+ Note that the returned value is approximate. Imprecision can be introduced by the computation if
1473
+ any intermediary values fall outside the `JsonSafeLong` range (#{format_number(JSON_SAFE_LONG_MIN)}
1474
+ to #{format_number(JSON_SAFE_LONG_MAX)}).
1475
+ EOS
1476
+ end
1477
+ end
1478
+ end
1479
+
1480
+ def define_exact_min_max_and_approx_avg_on_aggregated_values(aggregated_values_type, scalar_type, &block)
1481
+ define_exact_min_and_max_on_aggregated_values(aggregated_values_type, scalar_type, &block)
1482
+
1483
+ aggregated_values_type.field names.approximate_avg, scalar_type, graphql_only: true do |f|
1484
+ f.runtime_metadata_graphql_field = f.runtime_metadata_graphql_field.with_computation_detail(
1485
+ empty_bucket_value: nil,
1486
+ function: :avg
1487
+ )
1488
+
1489
+ f.documentation <<~EOS
1490
+ The average (mean) of the field values within this grouping.
1491
+ The returned value will be rounded to the nearest `#{scalar_type}` value.
1492
+ EOS
1493
+ end
1494
+ end
1495
+
1496
+ def define_exact_min_and_max_on_aggregated_values(aggregated_values_type, scalar_type)
1497
+ {
1498
+ names.exact_min => [:min, "minimum", "smallest"],
1499
+ names.exact_max => [:max, "maximum", "largest"]
1500
+ }.each do |name, (func, full_name, adjective)|
1501
+ discussion = yield(adjective: adjective, full_name: full_name)
1502
+
1503
+ aggregated_values_type.field name, scalar_type, graphql_only: true do |f|
1504
+ f.runtime_metadata_graphql_field = f.runtime_metadata_graphql_field.with_computation_detail(
1505
+ empty_bucket_value: nil,
1506
+ function: func
1507
+ )
1508
+
1509
+ f.documentation ["The #{full_name} of the field values within this grouping.", discussion].compact.join("\n\n")
1510
+ end
1511
+ end
1512
+ end
1513
+
1514
+ def register_framework_object_type(name)
1515
+ schema_def_api.object_type(name) do |t|
1516
+ t.graphql_only true
1517
+ yield t
1518
+ end
1519
+ end
1520
+
1521
+ def format_number(num)
1522
+ abs_value_formatted = num.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
1523
+ (num < 0) ? "-#{abs_value_formatted}" : abs_value_formatted
1524
+ end
1525
+
1526
+ def register_filter(type, &block)
1527
+ register_input_type(schema_def_state.factory.new_filter_input_type(type, &block))
1528
+ end
1529
+
1530
+ def register_input_type(input_type)
1531
+ schema_def_state.register_input_type(input_type)
1532
+ end
1533
+
1534
+ def remove_any_of_and_not_filter_operators_on(type)
1535
+ type.graphql_fields_by_name.delete(names.any_of)
1536
+ type.graphql_fields_by_name.delete(names.not)
1537
+ end
1538
+ end
1539
+ end
1540
+ end
1541
+ end