elasticgraph-schema_definition 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +7 -0
  4. data/elasticgraph-schema_definition.gemspec +26 -0
  5. data/lib/elastic_graph/schema_definition/api.rb +359 -0
  6. data/lib/elastic_graph/schema_definition/factory.rb +506 -0
  7. data/lib/elastic_graph/schema_definition/indexing/derived_fields/append_only_set.rb +79 -0
  8. data/lib/elastic_graph/schema_definition/indexing/derived_fields/field_initializer_support.rb +59 -0
  9. data/lib/elastic_graph/schema_definition/indexing/derived_fields/immutable_value.rb +99 -0
  10. data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +62 -0
  11. data/lib/elastic_graph/schema_definition/indexing/derived_indexed_type.rb +346 -0
  12. data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +74 -0
  13. data/lib/elastic_graph/schema_definition/indexing/field.rb +181 -0
  14. data/lib/elastic_graph/schema_definition/indexing/field_reference.rb +51 -0
  15. data/lib/elastic_graph/schema_definition/indexing/field_type/enum.rb +65 -0
  16. data/lib/elastic_graph/schema_definition/indexing/field_type/object.rb +113 -0
  17. data/lib/elastic_graph/schema_definition/indexing/field_type/scalar.rb +51 -0
  18. data/lib/elastic_graph/schema_definition/indexing/field_type/union.rb +70 -0
  19. data/lib/elastic_graph/schema_definition/indexing/index.rb +318 -0
  20. data/lib/elastic_graph/schema_definition/indexing/json_schema_field_metadata.rb +34 -0
  21. data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +234 -0
  22. data/lib/elastic_graph/schema_definition/indexing/list_counts_mapping.rb +53 -0
  23. data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +96 -0
  24. data/lib/elastic_graph/schema_definition/indexing/rollover_config.rb +25 -0
  25. data/lib/elastic_graph/schema_definition/indexing/update_target_factory.rb +54 -0
  26. data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +195 -0
  27. data/lib/elastic_graph/schema_definition/json_schema_pruner.rb +61 -0
  28. data/lib/elastic_graph/schema_definition/mixins/can_be_graphql_only.rb +31 -0
  29. data/lib/elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations.rb +119 -0
  30. data/lib/elastic_graph/schema_definition/mixins/has_directives.rb +65 -0
  31. data/lib/elastic_graph/schema_definition/mixins/has_documentation.rb +74 -0
  32. data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +281 -0
  33. data/lib/elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect.rb +46 -0
  34. data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +116 -0
  35. data/lib/elastic_graph/schema_definition/mixins/has_type_info.rb +181 -0
  36. data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +122 -0
  37. data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +47 -0
  38. data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +267 -0
  39. data/lib/elastic_graph/schema_definition/mixins/verifies_graphql_name.rb +38 -0
  40. data/lib/elastic_graph/schema_definition/rake_tasks.rb +190 -0
  41. data/lib/elastic_graph/schema_definition/results.rb +404 -0
  42. data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +482 -0
  43. data/lib/elastic_graph/schema_definition/schema_elements/argument.rb +56 -0
  44. data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +1541 -0
  45. data/lib/elastic_graph/schema_definition/schema_elements/deprecated_element.rb +21 -0
  46. data/lib/elastic_graph/schema_definition/schema_elements/directive.rb +40 -0
  47. data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +189 -0
  48. data/lib/elastic_graph/schema_definition/schema_elements/enum_value.rb +73 -0
  49. data/lib/elastic_graph/schema_definition/schema_elements/enum_value_namer.rb +89 -0
  50. data/lib/elastic_graph/schema_definition/schema_elements/enums_for_indexed_types.rb +82 -0
  51. data/lib/elastic_graph/schema_definition/schema_elements/field.rb +1085 -0
  52. data/lib/elastic_graph/schema_definition/schema_elements/field_path.rb +112 -0
  53. data/lib/elastic_graph/schema_definition/schema_elements/field_source.rb +16 -0
  54. data/lib/elastic_graph/schema_definition/schema_elements/graphql_sdl_enumerator.rb +113 -0
  55. data/lib/elastic_graph/schema_definition/schema_elements/input_field.rb +31 -0
  56. data/lib/elastic_graph/schema_definition/schema_elements/input_type.rb +60 -0
  57. data/lib/elastic_graph/schema_definition/schema_elements/interface_type.rb +72 -0
  58. data/lib/elastic_graph/schema_definition/schema_elements/list_counts_state.rb +40 -0
  59. data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +53 -0
  60. data/lib/elastic_graph/schema_definition/schema_elements/relationship.rb +218 -0
  61. data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +310 -0
  62. data/lib/elastic_graph/schema_definition/schema_elements/sort_order_enum_value.rb +36 -0
  63. data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +66 -0
  64. data/lib/elastic_graph/schema_definition/schema_elements/type_namer.rb +237 -0
  65. data/lib/elastic_graph/schema_definition/schema_elements/type_reference.rb +353 -0
  66. data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +579 -0
  67. data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +157 -0
  68. data/lib/elastic_graph/schema_definition/scripting/file_system_repository.rb +77 -0
  69. data/lib/elastic_graph/schema_definition/scripting/script.rb +48 -0
  70. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_day_of_week.painless +24 -0
  71. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_time_of_day.painless +41 -0
  72. data/lib/elastic_graph/schema_definition/scripting/scripts/filter/by_time_of_day.painless +22 -0
  73. data/lib/elastic_graph/schema_definition/scripting/scripts/update/index_data.painless +93 -0
  74. data/lib/elastic_graph/schema_definition/state.rb +212 -0
  75. data/lib/elastic_graph/schema_definition/test_support.rb +113 -0
  76. metadata +513 -0
@@ -0,0 +1,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