elasticgraph-graphql 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }