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,120 @@
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 "base64"
10
+ require "elastic_graph/constants"
11
+ require "elastic_graph/error"
12
+ require "elastic_graph/support/memoizable_data"
13
+ require "json"
14
+
15
+ module ElasticGraph
16
+ class GraphQL
17
+ # Provides the in-memory representation of a cursor after it has been decoded, as a simple hash of sort values.
18
+ #
19
+ # The datastore's `search_after` pagination uses an array of values (which represent values of the fields you are
20
+ # sorting by). A cursor returned when we applied one sort is generally not valid when we apply a completely
21
+ # different sort. To ensure we can detect this, the encoder encodes a hash of sort fields and values, ensuring
22
+ # each value in the cursor is properly labeled with what field it came from. This allows us
23
+ # to detect situations where the client uses a cursor with a completely different sort applied, while
24
+ # allowing some minor variation in the sort. The following are still allowed:
25
+ #
26
+ # - Changing the direction of the sort (from `asc` to `desc` or vice-versa)
27
+ # - Re-ordering the sort (e.g. changing from `[amount_money_DESC, created_at_ASC]`
28
+ # to `[created_at_ASC, amount_money_DESC]`
29
+ # - Removing fields from the sort (e.g. changing from `[amount_money_DESC, created_at_ASC]`
30
+ # to `[amount_money_DESC]`) -- but adding fields is not allowed
31
+ #
32
+ # While we don't necessarily recommend clients change these things between pagination requests (the
33
+ # behavior may be surprising to the user), there is no ambiguity in how to support them, and we do not
34
+ # feel like it makes sense to restrict it at this point.
35
+ class DecodedCursor < Support::MemoizableData.define(:sort_values)
36
+ # Methods provided by `MemoizableData.define`:
37
+ # @dynamic initialize, sort_values
38
+
39
+ # Tries to decode the given string cursor, returning `nil` if it is invalid.
40
+ def self.try_decode(string)
41
+ decode!(string)
42
+ rescue InvalidCursorError
43
+ nil
44
+ end
45
+
46
+ # Tries to decode the given string cursor, raising an `InvalidCursorError` if it's invalid.
47
+ def self.decode!(string)
48
+ return SINGLETON if string == SINGLETON_CURSOR
49
+ json = ::Base64.urlsafe_decode64(string)
50
+ new(::JSON.parse(json))
51
+ rescue ::ArgumentError, ::JSON::ParserError
52
+ raise InvalidCursorError, "`#{string}` is an invalid cursor."
53
+ end
54
+
55
+ # Encodes the cursor to a string using JSON and Base64 encoding.
56
+ def encode
57
+ @encode ||= begin
58
+ json = ::JSON.fast_generate(sort_values)
59
+ ::Base64.urlsafe_encode64(json, padding: false)
60
+ end
61
+ end
62
+
63
+ # A special cursor instance for when we need a cursor but have only a static collection of a single
64
+ # element without any sort of key we can encode.
65
+ SINGLETON = new({}).tap do |sc|
66
+ # Ensure the special string value is returned even though our `sort_values` are empty.
67
+ def sc.encode
68
+ SINGLETON_CURSOR
69
+ end
70
+ end
71
+
72
+ # Used to build decoded cursor values for the given `sort_fields`.
73
+ class Factory < Data.define(:sort_fields)
74
+ # Methods provided by `Data.define`:
75
+ # @dynamic initialize, sort_fields
76
+
77
+ # Builds a factory from a list like:
78
+ # `[{ 'amount_money.amount' => 'asc' }, { 'created_at' => 'desc' }]`.
79
+ def self.from_sort_list(sort_list)
80
+ sort_fields = sort_list.map do |hash|
81
+ if hash.values.any? { |v| !v.is_a?(::Hash) } || hash.values.flat_map(&:keys) != ["order"]
82
+ raise InvalidSortFieldsError,
83
+ "Given `sort_list` contained an invalid entry. Each must be a flat hash with one entry. Got: #{sort_list.inspect}"
84
+ end
85
+
86
+ # Steep thinks it could be `nil` because `hash.keys` could be empty, but we raise an error above in
87
+ # that case, so we know this will wind up being a `String`. `_` here silences Steep's type check error.
88
+ _ = hash.keys.first
89
+ end
90
+
91
+ if sort_fields.uniq.size < sort_fields.size
92
+ raise InvalidSortFieldsError,
93
+ "Given `sort_list` contains a duplicate field, which the CursorEncoder cannot handler. " \
94
+ "The caller is responsible for de-duplicating the sort list fist. Got: #{sort_list.inspect}"
95
+ end
96
+
97
+ new(sort_fields)
98
+ end
99
+
100
+ def build(sort_values)
101
+ unless sort_values.size == sort_fields.size
102
+ raise CursorEncodingError,
103
+ "size of sort values (#{sort_values.inspect}) does not match the " \
104
+ "size of sort fields (#{sort_fields.inspect})"
105
+ end
106
+
107
+ DecodedCursor.new(sort_fields.zip(sort_values).to_h)
108
+ end
109
+
110
+ alias_method :to_s, :inspect
111
+
112
+ module Null
113
+ def self.build(sort_values)
114
+ DecodedCursor.new(sort_values.map(&:to_s).zip(sort_values).to_h)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,45 @@
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
+ module ElasticGraph
10
+ class GraphQL
11
+ module Filtering
12
+ # BooleanQuery is an internal class for composing a datastore query:
13
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
14
+ #
15
+ # It is composed of:
16
+ # 1) The occurrence type (:must, :filter, :should, or :must_not)
17
+ # 2) A list of query clauses evaluated by the given occurrence type
18
+ # 3) An optional flag indicating whether the occurrence should be negated
19
+ class BooleanQuery < ::Data.define(:occurrence, :clauses)
20
+ def self.must(*clauses)
21
+ new(:must, clauses)
22
+ end
23
+
24
+ def self.filter(*clauses)
25
+ new(:filter, clauses)
26
+ end
27
+
28
+ def self.should(*clauses)
29
+ new(:should, clauses)
30
+ end
31
+
32
+ def merge_into(bool_node)
33
+ bool_node[occurrence].concat(clauses)
34
+ end
35
+
36
+ # For `any_of: []` we need a way to force the datastore to match no documents, but
37
+ # I haven't found any sort of literal `false` we can pass in the compound expression
38
+ # or even a literal `1 = 0` as is sometimes used in SQL. Instead, we use this for that
39
+ # case.
40
+ empty_array = [] # : ::Array[untyped]
41
+ ALWAYS_FALSE_FILTER = filter({ids: {values: empty_array}})
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,81 @@
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
+
11
+ module ElasticGraph
12
+ class GraphQL
13
+ module Filtering
14
+ # Tracks state related to field paths as we traverse our filtering data structure in order to translate
15
+ # it to its Elasticsearch/OpenSearch form.
16
+ #
17
+ # Instances of this class are immutable--callers must use the provided APIs (`+`, `counts_path`, `nested`)
18
+ # to get back new instances with state changes applied.
19
+ FieldPath = ::Data.define(
20
+ # The path from the overall document root.
21
+ :from_root,
22
+ # The path from the current parent document. Usually `from_parent` and `from_root` are the same,
23
+ # but they'll be different when we encounter a list field indexed using the `nested` mapping type.
24
+ # When we're traversing a subfield of a `nested` field, `from_root` will contain the full path from
25
+ # the original, overall document root, while `from_parent` will contain the path from the current
26
+ # nested document's root.
27
+ :from_parent
28
+ ) do
29
+ # @implements FieldPath
30
+
31
+ # Builds an empty instance.
32
+ def self.empty
33
+ new([], [])
34
+ end
35
+
36
+ def self.of(parts)
37
+ new(parts, parts)
38
+ end
39
+
40
+ # Used when we encounter a `nested` field to restart the `from_parent` path (while preserving the `from_root` path).
41
+ def nested
42
+ FieldPath.new(from_root, [])
43
+ end
44
+
45
+ # Creates a new instance with `sub_path` appended.
46
+ def +(other)
47
+ FieldPath.new(from_root + [other], from_parent + [other])
48
+ end
49
+
50
+ # Converts the current paths to what they need to be to be able to query our hidden `__counts` field (which
51
+ # is a map containing the counts of elements of every list field on the document). The `__counts` field
52
+ # sits a the root of every document (for both an overall root document and a `nested` document). Here's an
53
+ # example (which assumes `seasons` and `seasons.players` fields which are both `nested` and an `awards` field
54
+ # which is a list of strings). Given a filter like this:
55
+ #
56
+ # filter: {seasons: {any_satisfy: {players: {any_satisfy: {results: {awards: {count: {gt: 1}}}}}}}}
57
+ #
58
+ # ...after processing the `awards` key, our `FieldPath` will be:
59
+ #
60
+ # FieldPath.new(["seasons", "players", "results", "awards"], ["results", "awards"])
61
+ #
62
+ # When we then reach the `count` sub field and `counts_path` is called on it, the following will be returned:
63
+ #
64
+ # FieldPath.new(["seasons", "players", LIST_COUNTS_FIELD, "results|awards"], [LIST_COUNTS_FIELD, "results|awards"])
65
+ #
66
+ # This gives us what we want:
67
+ # - The path from the root is `seasons.players.__counts.results|awards`.
68
+ # - The path from the (nested) parent is `__counts.results|awards`.
69
+ #
70
+ # Note that our `__counts` field is a flat map which uses `|` (the `LIST_COUNTS_FIELD_PATH_KEY_SEPARATOR` character)
71
+ # to separate its parts (hence, it's `results|awards` instead of `results.awards`).
72
+ def counts_path
73
+ from_root_to_parent_of_counts_field = from_root[0...-from_parent.size] # : ::Array[::String]
74
+ counts_sub_field = [LIST_COUNTS_FIELD, from_parent.join(LIST_COUNTS_FIELD_PATH_KEY_SEPARATOR)]
75
+
76
+ FieldPath.new(from_root_to_parent_of_counts_field + counts_sub_field, counts_sub_field)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,58 @@
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
+ module ElasticGraph
10
+ class GraphQL
11
+ module Filtering
12
+ # Responsible for translating a `filter` expression from GraphQL field names to the internal
13
+ # `name_in_index` of each field. This is necessary so that when a field is defined with
14
+ # an alternate `name_in_index`, the query against the index uses that name even while
15
+ # the name in the GraphQL schema is different.
16
+ #
17
+ # In addition, we translate the enum value names to enum value objects, so that any runtime
18
+ # metadata associated with that enum value is available to our `FilterInterpreter`.
19
+ class FilterArgsTranslator < ::Data.define(:filter_arg_name)
20
+ def initialize(schema_element_names:)
21
+ super(filter_arg_name: schema_element_names.filter)
22
+ end
23
+
24
+ # Translates the `filter` expression from the given `args` and `field` into their equivalent
25
+ # form using the `name_in_index` for any fields that are named differently in the index
26
+ # vs GraphQL.
27
+ def translate_filter_args(field:, args:)
28
+ return nil unless (filter_hash = args[filter_arg_name])
29
+ filter_type = field.schema.type_from(field.graphql_field.arguments[filter_arg_name].type)
30
+ convert(filter_type, filter_hash)
31
+ end
32
+
33
+ private
34
+
35
+ def convert(parent_type, filter_object)
36
+ case filter_object
37
+ when ::Hash
38
+ filter_object.to_h do |key, value|
39
+ field = parent_type.field_named(key)
40
+ [field.name_in_index.to_s, convert(field.type.unwrap_fully, value)]
41
+ end
42
+ when ::Array
43
+ filter_object.map { |value| convert(parent_type, value) }
44
+ when nil
45
+ nil
46
+ else
47
+ if parent_type.enum?
48
+ # Replace the name of an enum value with the value itself.
49
+ parent_type.enum_value_named(filter_object)
50
+ else
51
+ filter_object
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end