elasticgraph-graphql 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 (81) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +3 -0
  4. data/elasticgraph-graphql.gemspec +23 -0
  5. data/lib/elastic_graph/graphql/aggregation/composite_grouping_adapter.rb +79 -0
  6. data/lib/elastic_graph/graphql/aggregation/computation.rb +39 -0
  7. data/lib/elastic_graph/graphql/aggregation/date_histogram_grouping.rb +83 -0
  8. data/lib/elastic_graph/graphql/aggregation/field_path_encoder.rb +47 -0
  9. data/lib/elastic_graph/graphql/aggregation/field_term_grouping.rb +26 -0
  10. data/lib/elastic_graph/graphql/aggregation/key.rb +87 -0
  11. data/lib/elastic_graph/graphql/aggregation/nested_sub_aggregation.rb +37 -0
  12. data/lib/elastic_graph/graphql/aggregation/non_composite_grouping_adapter.rb +129 -0
  13. data/lib/elastic_graph/graphql/aggregation/path_segment.rb +31 -0
  14. data/lib/elastic_graph/graphql/aggregation/query.rb +172 -0
  15. data/lib/elastic_graph/graphql/aggregation/query_adapter.rb +345 -0
  16. data/lib/elastic_graph/graphql/aggregation/query_optimizer.rb +187 -0
  17. data/lib/elastic_graph/graphql/aggregation/resolvers/aggregated_values.rb +41 -0
  18. data/lib/elastic_graph/graphql/aggregation/resolvers/count_detail.rb +44 -0
  19. data/lib/elastic_graph/graphql/aggregation/resolvers/grouped_by.rb +30 -0
  20. data/lib/elastic_graph/graphql/aggregation/resolvers/node.rb +64 -0
  21. data/lib/elastic_graph/graphql/aggregation/resolvers/relay_connection_builder.rb +83 -0
  22. data/lib/elastic_graph/graphql/aggregation/resolvers/sub_aggregations.rb +82 -0
  23. data/lib/elastic_graph/graphql/aggregation/script_term_grouping.rb +32 -0
  24. data/lib/elastic_graph/graphql/aggregation/term_grouping.rb +118 -0
  25. data/lib/elastic_graph/graphql/client.rb +43 -0
  26. data/lib/elastic_graph/graphql/config.rb +81 -0
  27. data/lib/elastic_graph/graphql/datastore_query/document_paginator.rb +100 -0
  28. data/lib/elastic_graph/graphql/datastore_query/index_expression_builder.rb +142 -0
  29. data/lib/elastic_graph/graphql/datastore_query/paginator.rb +199 -0
  30. data/lib/elastic_graph/graphql/datastore_query/routing_picker.rb +239 -0
  31. data/lib/elastic_graph/graphql/datastore_query.rb +372 -0
  32. data/lib/elastic_graph/graphql/datastore_response/document.rb +78 -0
  33. data/lib/elastic_graph/graphql/datastore_response/search_response.rb +79 -0
  34. data/lib/elastic_graph/graphql/datastore_search_router.rb +151 -0
  35. data/lib/elastic_graph/graphql/decoded_cursor.rb +120 -0
  36. data/lib/elastic_graph/graphql/filtering/boolean_query.rb +45 -0
  37. data/lib/elastic_graph/graphql/filtering/field_path.rb +81 -0
  38. data/lib/elastic_graph/graphql/filtering/filter_args_translator.rb +58 -0
  39. data/lib/elastic_graph/graphql/filtering/filter_interpreter.rb +526 -0
  40. data/lib/elastic_graph/graphql/filtering/filter_value_set_extractor.rb +148 -0
  41. data/lib/elastic_graph/graphql/filtering/range_query.rb +56 -0
  42. data/lib/elastic_graph/graphql/http_endpoint.rb +229 -0
  43. data/lib/elastic_graph/graphql/monkey_patches/schema_field.rb +56 -0
  44. data/lib/elastic_graph/graphql/monkey_patches/schema_object.rb +48 -0
  45. data/lib/elastic_graph/graphql/query_adapter/filters.rb +161 -0
  46. data/lib/elastic_graph/graphql/query_adapter/pagination.rb +27 -0
  47. data/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +124 -0
  48. data/lib/elastic_graph/graphql/query_adapter/sort.rb +32 -0
  49. data/lib/elastic_graph/graphql/query_details_tracker.rb +60 -0
  50. data/lib/elastic_graph/graphql/query_executor.rb +200 -0
  51. data/lib/elastic_graph/graphql/resolvers/get_record_field_value.rb +49 -0
  52. data/lib/elastic_graph/graphql/resolvers/graphql_adapter.rb +114 -0
  53. data/lib/elastic_graph/graphql/resolvers/list_records.rb +29 -0
  54. data/lib/elastic_graph/graphql/resolvers/nested_relationships.rb +74 -0
  55. data/lib/elastic_graph/graphql/resolvers/query_adapter.rb +85 -0
  56. data/lib/elastic_graph/graphql/resolvers/query_source.rb +46 -0
  57. data/lib/elastic_graph/graphql/resolvers/relay_connection/array_adapter.rb +71 -0
  58. data/lib/elastic_graph/graphql/resolvers/relay_connection/generic_adapter.rb +65 -0
  59. data/lib/elastic_graph/graphql/resolvers/relay_connection/page_info.rb +82 -0
  60. data/lib/elastic_graph/graphql/resolvers/relay_connection/search_response_adapter_builder.rb +40 -0
  61. data/lib/elastic_graph/graphql/resolvers/relay_connection.rb +42 -0
  62. data/lib/elastic_graph/graphql/resolvers/resolvable_value.rb +56 -0
  63. data/lib/elastic_graph/graphql/scalar_coercion_adapters/cursor.rb +35 -0
  64. data/lib/elastic_graph/graphql/scalar_coercion_adapters/date.rb +64 -0
  65. data/lib/elastic_graph/graphql/scalar_coercion_adapters/date_time.rb +60 -0
  66. data/lib/elastic_graph/graphql/scalar_coercion_adapters/local_time.rb +30 -0
  67. data/lib/elastic_graph/graphql/scalar_coercion_adapters/longs.rb +47 -0
  68. data/lib/elastic_graph/graphql/scalar_coercion_adapters/no_op.rb +24 -0
  69. data/lib/elastic_graph/graphql/scalar_coercion_adapters/time_zone.rb +44 -0
  70. data/lib/elastic_graph/graphql/scalar_coercion_adapters/untyped.rb +32 -0
  71. data/lib/elastic_graph/graphql/scalar_coercion_adapters/valid_time_zones.rb +634 -0
  72. data/lib/elastic_graph/graphql/schema/arguments.rb +78 -0
  73. data/lib/elastic_graph/graphql/schema/enum_value.rb +30 -0
  74. data/lib/elastic_graph/graphql/schema/field.rb +147 -0
  75. data/lib/elastic_graph/graphql/schema/relation_join.rb +103 -0
  76. data/lib/elastic_graph/graphql/schema/type.rb +263 -0
  77. data/lib/elastic_graph/graphql/schema.rb +164 -0
  78. data/lib/elastic_graph/graphql.rb +253 -0
  79. data/script/dump_time_zones +81 -0
  80. data/script/dump_time_zones.java +17 -0
  81. metadata +503 -0
@@ -0,0 +1,164 @@
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 "digest/md5"
10
+ require "forwardable"
11
+ require "graphql"
12
+ require "elastic_graph/constants"
13
+ require "elastic_graph/error"
14
+ require "elastic_graph/graphql/monkey_patches/schema_field"
15
+ require "elastic_graph/graphql/monkey_patches/schema_object"
16
+ require "elastic_graph/graphql/schema/field"
17
+ require "elastic_graph/graphql/schema/type"
18
+ require "elastic_graph/support/hash_util"
19
+
20
+ module ElasticGraph
21
+ # Wraps a GraphQL::Schema object in order to provide higher-level, more convenient APIs
22
+ # on top of that. The schema is assumed to be immutable, so this class memoizes many
23
+ # computations it does, ensuring we never need to traverse the schema graph multiple times.
24
+ class GraphQL
25
+ class Schema
26
+ BUILT_IN_TYPE_NAMES = (
27
+ scalar_types = ::GraphQL::Schema::BUILT_IN_TYPES.keys # Int, ID, String, etc
28
+ introspection_types = ::GraphQL::Schema.types.keys # __Type, __Schema, etc
29
+ scalar_types.to_set.union(introspection_types)
30
+ )
31
+
32
+ attr_reader :element_names, :defined_types, :config, :graphql_schema, :runtime_metadata
33
+
34
+ def initialize(
35
+ graphql_schema_string:,
36
+ config:,
37
+ runtime_metadata:,
38
+ index_definitions_by_graphql_type:,
39
+ graphql_gem_plugins:,
40
+ &build_resolver
41
+ )
42
+ @element_names = runtime_metadata.schema_element_names
43
+ @config = config
44
+ @runtime_metadata = runtime_metadata
45
+
46
+ @types_by_graphql_type = Hash.new do |hash, key|
47
+ hash[key] = Type.new(
48
+ self,
49
+ key,
50
+ index_definitions_by_graphql_type[key.graphql_name] || [],
51
+ runtime_metadata.object_types_by_name[key.graphql_name],
52
+ runtime_metadata.enum_types_by_name[key.graphql_name]
53
+ )
54
+ end
55
+
56
+ @types_by_name = Hash.new { |hash, key| hash[key] = lookup_type_by_name(key) }
57
+ @build_resolver = build_resolver
58
+
59
+ # Note: as part of loading the schema, the GraphQL gem may use the resolver (such
60
+ # when a directive has a custom scalar) so we must wait to instantiate the schema
61
+ # as late as possible here. If we do this before initializing some of the instance
62
+ # variables above we'll get `NoMethodError` on `nil`.
63
+ @graphql_schema = ::GraphQL::Schema.from_definition(
64
+ graphql_schema_string,
65
+ default_resolve: LazyResolverAdapter.new(method(:resolver)),
66
+ using: graphql_gem_plugins
67
+ )
68
+
69
+ # Pre-load all defined types so that all field extras can get configured as part
70
+ # of loading the schema, before we execute the first query.
71
+ @defined_types = build_defined_types_array(@graphql_schema)
72
+ end
73
+
74
+ def type_from(graphql_type)
75
+ @types_by_graphql_type[graphql_type]
76
+ end
77
+
78
+ # Note: this does not support "wrapped" types (e.g. `Int!` or `[Int]` compared to `Int`),
79
+ # as the graphql schema object does not give us an index of those by name. You can still
80
+ # get type objects for wrapped types, but you need to get it from a field object of that
81
+ # type.
82
+ def type_named(type_name)
83
+ @types_by_name[type_name.to_s]
84
+ end
85
+
86
+ def document_type_stored_in(index_definition_name)
87
+ indexed_document_types_by_index_definition_name.fetch(index_definition_name) do
88
+ if index_definition_name.include?(ROLLOVER_INDEX_INFIX_MARKER)
89
+ raise ArgumentError, "`#{index_definition_name}` is the name of a rollover index; pass the name of the parent index definition instead."
90
+ else
91
+ raise NotFoundError, "The index definition `#{index_definition_name}` does not appear to exist. Is it misspelled?"
92
+ end
93
+ end
94
+ end
95
+
96
+ def field_named(type_name, field_name)
97
+ type_named(type_name).field_named(field_name)
98
+ end
99
+
100
+ def enum_value_named(type_name, enum_value_name)
101
+ type_named(type_name).enum_value_named(enum_value_name)
102
+ end
103
+
104
+ # The list of user-defined types that are indexed document types. (Indexed aggregation types will not be included in this.)
105
+ def indexed_document_types
106
+ @indexed_document_types ||= defined_types.select(&:indexed_document?)
107
+ end
108
+
109
+ def to_s
110
+ "#<#{self.class.name} 0x#{__id__.to_s(16)} indexed_document_types=#{indexed_document_types.map(&:name).sort.to_s.delete(":")}>"
111
+ end
112
+ alias_method :inspect, :to_s
113
+
114
+ private
115
+
116
+ # Adapter class to allow us to lazily load the resolver instance.
117
+ #
118
+ # Necessary because the resolver must be provided to `GraphQL::Schema.from_definition`,
119
+ # but the resolver logic itself depends upon the loaded schema to know how to resolve.
120
+ # To work around the circular dependency, we build the schema with this lazy adapter,
121
+ # then build the resolver with the schema, and then the lazy resolver lazily loads the resolver.
122
+ LazyResolverAdapter = Struct.new(:builder) do
123
+ def resolver
124
+ @resolver ||= builder.call
125
+ end
126
+
127
+ extend Forwardable
128
+ def_delegators :resolver, :call, :resolve_type, :coerce_input, :coerce_result
129
+ end
130
+
131
+ def lookup_type_by_name(type_name)
132
+ type_from(@graphql_schema.types.fetch(type_name))
133
+ rescue KeyError => e
134
+ msg = "No type named #{type_name} could be found"
135
+ msg += "; Possible alternatives: [#{e.corrections.join(", ").delete('"')}]." if e.corrections.any?
136
+ raise NotFoundError, msg
137
+ end
138
+
139
+ def resolver
140
+ @resolver ||= @build_resolver.call(self)
141
+ end
142
+
143
+ def build_defined_types_array(graphql_schema)
144
+ graphql_schema
145
+ .types
146
+ .values
147
+ .reject { |t| BUILT_IN_TYPE_NAMES.include?(t.graphql_name) }
148
+ .map { |t| type_named(t.graphql_name) }
149
+ end
150
+
151
+ def indexed_document_types_by_index_definition_name
152
+ @indexed_document_types_by_index_definition_name ||= indexed_document_types.each_with_object({}) do |type, hash|
153
+ type.index_definitions.each do |index_def|
154
+ if hash.key?(index_def.name)
155
+ raise SchemaError, "DatastoreCore::IndexDefinition #{index_def.name} is used multiple times: #{type} vs #{hash[index_def.name]}"
156
+ end
157
+
158
+ hash[index_def.name] = type
159
+ end
160
+ end.freeze
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,253 @@
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/datastore_core"
10
+ require "elastic_graph/graphql/config"
11
+ require "elastic_graph/support/from_yaml_file"
12
+
13
+ module ElasticGraph
14
+ # The main entry point for ElasticGraph GraphQL handling. Instantiate this to get access to the
15
+ # different parts of this library.
16
+ class GraphQL
17
+ extend Support::FromYamlFile
18
+
19
+ # @private
20
+ # @dynamic config, logger, runtime_metadata, graphql_schema_string, datastore_core, clock
21
+ attr_reader :config, :logger, :runtime_metadata, :graphql_schema_string, :datastore_core, :clock
22
+
23
+ # @private
24
+ # A factory method that builds a GraphQL instance from the given parsed YAML config.
25
+ # `from_yaml_file(file_name, &block)` is also available (via `Support::FromYamlFile`).
26
+ def self.from_parsed_yaml(parsed_yaml, &datastore_client_customization_block)
27
+ new(
28
+ config: GraphQL::Config.from_parsed_yaml(parsed_yaml),
29
+ datastore_core: DatastoreCore.from_parsed_yaml(parsed_yaml, for_context: :graphql, &datastore_client_customization_block)
30
+ )
31
+ end
32
+
33
+ # @private
34
+ def initialize(
35
+ config:,
36
+ datastore_core:,
37
+ graphql_adapter: nil,
38
+ datastore_search_router: nil,
39
+ filter_interpreter: nil,
40
+ sub_aggregation_grouping_adapter: nil,
41
+ monotonic_clock: nil,
42
+ clock: ::Time
43
+ )
44
+ @config = config
45
+ @datastore_core = datastore_core
46
+ @graphql_adapter = graphql_adapter
47
+ @datastore_search_router = datastore_search_router
48
+ @filter_interpreter = filter_interpreter
49
+ @sub_aggregation_grouping_adapter = sub_aggregation_grouping_adapter
50
+ @monotonic_clock = monotonic_clock
51
+ @clock = clock
52
+ @logger = @datastore_core.logger
53
+ @runtime_metadata = @datastore_core.schema_artifacts.runtime_metadata
54
+ @graphql_schema_string = @datastore_core.schema_artifacts.graphql_schema_string
55
+
56
+ # Apply any extension modules that have been configured.
57
+ @config.extension_modules.each { |mod| extend mod }
58
+ @runtime_metadata.graphql_extension_modules.each { |ext_mod| extend ext_mod.extension_class }
59
+ end
60
+
61
+ # @private
62
+ def graphql_http_endpoint
63
+ @graphql_http_endpoint ||= begin
64
+ require "elastic_graph/graphql/http_endpoint"
65
+ HTTPEndpoint.new(
66
+ query_executor: graphql_query_executor,
67
+ monotonic_clock: monotonic_clock,
68
+ client_resolver: config.client_resolver
69
+ )
70
+ end
71
+ end
72
+
73
+ # @private
74
+ def graphql_query_executor
75
+ @graphql_query_executor ||= begin
76
+ require "elastic_graph/graphql/query_executor"
77
+ QueryExecutor.new(
78
+ schema: schema,
79
+ monotonic_clock: monotonic_clock,
80
+ logger: logger,
81
+ slow_query_threshold_ms: @config.slow_query_latency_warning_threshold_in_ms,
82
+ datastore_search_router: datastore_search_router
83
+ )
84
+ end
85
+ end
86
+
87
+ # @private
88
+ def schema
89
+ @schema ||= begin
90
+ require "elastic_graph/graphql/schema"
91
+
92
+ Schema.new(
93
+ graphql_schema_string: graphql_schema_string,
94
+ config: config,
95
+ runtime_metadata: runtime_metadata,
96
+ index_definitions_by_graphql_type: @datastore_core.index_definitions_by_graphql_type,
97
+ graphql_gem_plugins: graphql_gem_plugins
98
+ ) do |schema|
99
+ @graphql_adapter || begin
100
+ @schema = schema # assign this so that `#schema` returns the schema when `datastore_query_adapters` is called below
101
+ require "elastic_graph/graphql/resolvers/graphql_adapter"
102
+ Resolvers::GraphQLAdapter.new(
103
+ schema: schema,
104
+ datastore_query_builder: datastore_query_builder,
105
+ datastore_query_adapters: datastore_query_adapters,
106
+ runtime_metadata: runtime_metadata,
107
+ resolvers: graphql_resolvers
108
+ )
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ # @private
115
+ def datastore_search_router
116
+ @datastore_search_router ||= begin
117
+ require "elastic_graph/graphql/datastore_search_router"
118
+ DatastoreSearchRouter.new(
119
+ datastore_clients_by_name: @datastore_core.clients_by_name,
120
+ logger: logger,
121
+ monotonic_clock: monotonic_clock,
122
+ config: @config
123
+ )
124
+ end
125
+ end
126
+
127
+ # @private
128
+ def datastore_query_builder
129
+ @datastore_query_builder ||= begin
130
+ require "elastic_graph/graphql/datastore_query"
131
+ DatastoreQuery::Builder.with(
132
+ filter_interpreter: filter_interpreter,
133
+ runtime_metadata: runtime_metadata,
134
+ logger: logger,
135
+ default_page_size: @config.default_page_size,
136
+ max_page_size: @config.max_page_size
137
+ )
138
+ end
139
+ end
140
+
141
+ # @private
142
+ def graphql_gem_plugins
143
+ @graphql_gem_plugins ||= begin
144
+ require "graphql"
145
+ {::GraphQL::Dataloader => {}}
146
+ end
147
+ end
148
+
149
+ # @private
150
+ def graphql_resolvers
151
+ @graphql_resolvers ||= begin
152
+ require "elastic_graph/graphql/resolvers/get_record_field_value"
153
+ require "elastic_graph/graphql/resolvers/list_records"
154
+ require "elastic_graph/graphql/resolvers/nested_relationships"
155
+
156
+ nested_relationships = Resolvers::NestedRelationships.new(
157
+ schema_element_names: runtime_metadata.schema_element_names,
158
+ logger: logger
159
+ )
160
+
161
+ list_records = Resolvers::ListRecords.new
162
+
163
+ get_record_field_value = Resolvers::GetRecordFieldValue.new(
164
+ schema_element_names: runtime_metadata.schema_element_names
165
+ )
166
+
167
+ [nested_relationships, list_records, get_record_field_value]
168
+ end
169
+ end
170
+
171
+ # @private
172
+ def datastore_query_adapters
173
+ @datastore_query_adapters ||= begin
174
+ require "elastic_graph/graphql/aggregation/non_composite_grouping_adapter"
175
+ require "elastic_graph/graphql/aggregation/query_adapter"
176
+ require "elastic_graph/graphql/query_adapter/filters"
177
+ require "elastic_graph/graphql/query_adapter/pagination"
178
+ require "elastic_graph/graphql/query_adapter/sort"
179
+ require "elastic_graph/graphql/query_adapter/requested_fields"
180
+
181
+ schema_element_names = runtime_metadata.schema_element_names
182
+
183
+ [
184
+ GraphQL::QueryAdapter::Pagination.new(schema_element_names: schema_element_names),
185
+ GraphQL::QueryAdapter::Filters.new(schema_element_names: schema_element_names, filter_args_translator: filter_args_translator),
186
+ GraphQL::QueryAdapter::Sort.new(order_by_arg_name: schema_element_names.order_by),
187
+ Aggregation::QueryAdapter.new(
188
+ schema: schema,
189
+ config: config,
190
+ filter_args_translator: filter_args_translator,
191
+ runtime_metadata: runtime_metadata,
192
+ sub_aggregation_grouping_adapter: sub_aggregation_grouping_adapter
193
+ ),
194
+ GraphQL::QueryAdapter::RequestedFields.new(schema)
195
+ ]
196
+ end
197
+ end
198
+
199
+ # @private
200
+ def filter_interpreter
201
+ @filter_interpreter ||= begin
202
+ require "elastic_graph/graphql/filtering/filter_interpreter"
203
+ Filtering::FilterInterpreter.new(runtime_metadata: runtime_metadata, logger: logger)
204
+ end
205
+ end
206
+
207
+ # @private
208
+ def filter_args_translator
209
+ @filter_args_translator ||= begin
210
+ require "elastic_graph/graphql/filtering/filter_args_translator"
211
+ Filtering::FilterArgsTranslator.new(schema_element_names: runtime_metadata.schema_element_names)
212
+ end
213
+ end
214
+
215
+ # @private
216
+ def sub_aggregation_grouping_adapter
217
+ @sub_aggregation_grouping_adapter ||= begin
218
+ require "elastic_graph/graphql/aggregation/non_composite_grouping_adapter"
219
+ Aggregation::NonCompositeGroupingAdapter
220
+ end
221
+ end
222
+
223
+ # @private
224
+ def monotonic_clock
225
+ @monotonic_clock ||= begin
226
+ require "elastic_graph/support/monotonic_clock"
227
+ Support::MonotonicClock.new
228
+ end
229
+ end
230
+
231
+ # @private
232
+ # Loads dependencies eagerly. In some environments (such as in an AWS Lambda) this is desirable as we to load all dependencies
233
+ # at boot time instead of deferring dependency loading until we handle the first query. In other environments (such as tests),
234
+ # it's nice to load dependencies when needed.
235
+ def load_dependencies_eagerly
236
+ # run a simple GraphQL query to force load any dependencies needed to handle GraphQL queries
237
+ graphql_query_executor.execute(EAGER_LOAD_QUERY, client: Client::ELASTICGRAPH_INTERNAL)
238
+ graphql_http_endpoint # force load this too.
239
+ end
240
+
241
+ private
242
+
243
+ EAGER_LOAD_QUERY = <<~EOS.strip
244
+ query ElasticGraphEagerLoadBootQuery {
245
+ __schema {
246
+ types {
247
+ kind
248
+ }
249
+ }
250
+ }
251
+ EOS
252
+ end
253
+ end
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "fileutils"
4
+
5
+ updated_code_filename = "#{__dir__}/../../tmp/updated_valid_time_zones.rb"
6
+
7
+ # Note: CI does not appear to have java 14 or 17 available, so we use java 11 here.
8
+ java_time_zones = `java --source 11 #{__dir__}/dump_time_zones.java`.split("\n")
9
+
10
+ ::File.write(updated_code_filename, <<~EOS)
11
+ # Copyright 2024 Block, Inc.
12
+ #
13
+ # Use of this source code is governed by an MIT-style
14
+ # license that can be found in the LICENSE file or at
15
+ # https://opensource.org/licenses/MIT.
16
+ #
17
+ # frozen_string_literal: true
18
+
19
+ module ElasticGraph
20
+ class GraphQL
21
+ module ScalarCoercionAdapters
22
+ # The set of all valid time zones. We expect this set to align with the official IANA
23
+ # list[^1][^2][^3], but ultimately we pass these time zones to the datastore and need
24
+ # to enumerate all time zones it supports. Since Elasticsearch and OpenSearch run on the JVM and the
25
+ # date format docs[^4] link to Java's `java.time.format.DateTimeFormatter` class, we
26
+ # can conclude that they support the time zones that the java class supports. This set
27
+ # is generated by `script/dump_time_zones`, which queries Java's `java.time.ZoneId`[^5]
28
+ # class to get the set of time zones supported on the JVM.
29
+ #
30
+ # DO NOT EDIT BY HAND.
31
+ #
32
+ # [^1]: https://www.iana.org/time-zones
33
+ # [^2]: https://en.wikipedia.org/wiki/Tz_database
34
+ # [^3]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
35
+ # [^4]: https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-date-format.html#custom-date-formats
36
+ # [^5]: https://docs.oracle.com/javase/8/docs/api/java/time/ZoneId.html
37
+ VALID_TIME_ZONES = %w[
38
+ #{java_time_zones.join("\n ")}
39
+ ].to_set
40
+ end
41
+ end
42
+ end
43
+ EOS
44
+
45
+ verify_code_command = %(ruby -r#{updated_code_filename} -e "puts ElasticGraph::GraphQL::ScalarCoercionAdapters::VALID_TIME_ZONES.size")
46
+ verify_code_output = `#{verify_code_command} 2>&1`
47
+
48
+ if verify_code_output.to_i < 600 # As of Nov 2022 there are 601 time zones.
49
+ abort <<~EOS.strip
50
+ It appears that the generated code is invalid. Check `#{updated_code_filename}` to see what was generated.
51
+
52
+ Output from `#{verify_code_command}`:
53
+
54
+ #{verify_code_output}
55
+ EOS
56
+ end
57
+
58
+ if ARGV.include?("--print")
59
+ puts ::File.read(updated_code_filename)
60
+ else
61
+ filename = "#{__dir__}/../lib/elastic_graph/graphql/scalar_coercion_adapters/valid_time_zones.rb"
62
+ ::FileUtils.cp(updated_code_filename, filename)
63
+ puts "Timezones have been written to `#{filename}`"
64
+ end
65
+
66
+ # if ARGV.include?("--verify")
67
+ # existing_contents = ::File.exist?(filename) ? ::File.read(filename) : ""
68
+ #
69
+ # if existing_contents == valid_timezones_ruby_code
70
+ # puts "`#{filename}` is up to date!"
71
+ # else
72
+ # diff = `git diff --no-index #{"--color" if $stdout.tty?} --binary #{updated_code_filename} #{filename}`
73
+ #
74
+ # abort <<~EOS.strip
75
+ # `#{filename}` is not up to date! Rerun `script/dump_time_zones` to correct.
76
+ #
77
+ # #{diff}
78
+ # EOS
79
+ # end
80
+ # else
81
+ # end
@@ -0,0 +1,17 @@
1
+ import java.time.ZoneId;
2
+ import java.util.Set;
3
+
4
+ /**
5
+ * Run this java file via `java --source 11 script/dump_time_zones.java`.
6
+ * `script/dump_time_zones` is a higher level wrapper that delegates to this
7
+ * and applies additional logic.
8
+ */
9
+ public class DumpTimeZones {
10
+ public static void main(String[] args) {
11
+ Set<String> availableZones = ZoneId.getAvailableZoneIds();
12
+
13
+ availableZones.stream()
14
+ .sorted()
15
+ .forEach(it -> System.out.println(it));
16
+ }
17
+ }