elasticgraph-support 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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +6 -0
- data/elasticgraph-support.gemspec +16 -0
- data/lib/elastic_graph/constants.rb +220 -0
- data/lib/elastic_graph/error.rb +99 -0
- data/lib/elastic_graph/support/faraday_middleware/msearch_using_get_instead_of_post.rb +31 -0
- data/lib/elastic_graph/support/faraday_middleware/support_timeouts.rb +36 -0
- data/lib/elastic_graph/support/from_yaml_file.rb +53 -0
- data/lib/elastic_graph/support/graphql_formatter.rb +66 -0
- data/lib/elastic_graph/support/hash_util.rb +191 -0
- data/lib/elastic_graph/support/logger.rb +82 -0
- data/lib/elastic_graph/support/memoizable_data.rb +147 -0
- data/lib/elastic_graph/support/monotonic_clock.rb +20 -0
- data/lib/elastic_graph/support/threading.rb +42 -0
- data/lib/elastic_graph/support/time_set.rb +293 -0
- data/lib/elastic_graph/support/time_util.rb +108 -0
- data/lib/elastic_graph/support/untyped_encoder.rb +67 -0
- data/lib/elastic_graph/version.rb +15 -0
- metadata +256 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0a6c3aca3ffb3ca4835a6e051afe7dee323348ef1d819a30dca05b1f6d06d452
|
4
|
+
data.tar.gz: a52ecb10ebd1065e9780aa3dabf71848200e96409b110122f00bd5569e485fa7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6ab3fb42baad12fcab9414a262f0b2ddfddb20d90cbd0913f65fb0a1dfecbb1e5c9b17231ea337d0a1a3ea20854ce310b8ecf905fb528a975e14406f6e46905d
|
7
|
+
data.tar.gz: 391e5007037fbdfd29134d2dd6cbaa5313fa8b031b2a4366c58d8c036940ef318d1571d16aee18aeab520eca2c8275f1d79b5842b07943cebed143797d069ac5
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Block, Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,16 @@
|
|
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_relative "../gemspec_helper"
|
10
|
+
|
11
|
+
ElasticGraphGemspecHelper.define_elasticgraph_gem(gemspec_file: __FILE__, category: :core) do |spec, eg_version|
|
12
|
+
spec.summary = "ElasticGraph gem providing support utilities to the other ElasticGraph gems."
|
13
|
+
|
14
|
+
spec.add_development_dependency "faraday", "~> 2.10"
|
15
|
+
spec.add_development_dependency "rake", "~> 13.2"
|
16
|
+
end
|
@@ -0,0 +1,220 @@
|
|
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
|
+
# Enumerates constants that are used from multiple places in the code.
|
10
|
+
module ElasticGraph
|
11
|
+
# The datastore date format used by ElasticGraph. Matches ISO-8601/RFC-3339.
|
12
|
+
# See https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html#built-in-date-formats
|
13
|
+
DATASTORE_DATE_FORMAT = "strict_date"
|
14
|
+
|
15
|
+
# The datastore date time format used by ElasticGraph. Matches ISO-8601/RFC-3339.
|
16
|
+
# See https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html#built-in-date-formats
|
17
|
+
DATASTORE_DATE_TIME_FORMAT = "strict_date_time"
|
18
|
+
|
19
|
+
# HTTP header that ElasticGraph HTTP implementations (e.g. elasticgraph-rack, elasticgraph-lambda)
|
20
|
+
# look at to determine a client-specified request timeout.
|
21
|
+
TIMEOUT_MS_HEADER = "ElasticGraph-Request-Timeout-Ms"
|
22
|
+
|
23
|
+
# Min/max values for the `Int` type.
|
24
|
+
# Based on the GraphQL spec:
|
25
|
+
#
|
26
|
+
# > If the integer internal value represents a value less than -2^31 or greater
|
27
|
+
# > than or equal to 2^31, a field error should be raised.
|
28
|
+
#
|
29
|
+
# (from http://spec.graphql.org/June2018/#sec-Int)
|
30
|
+
INT_MIN = -(2**31).to_int
|
31
|
+
INT_MAX = -INT_MIN - 1
|
32
|
+
|
33
|
+
# Min/max values for our `JsonSafeLong` type.
|
34
|
+
# Based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER
|
35
|
+
JSON_SAFE_LONG_MIN = -((2**53) - 1).to_int
|
36
|
+
JSON_SAFE_LONG_MAX = -JSON_SAFE_LONG_MIN
|
37
|
+
|
38
|
+
# Min/max values for our `LongString` type.
|
39
|
+
# This range is derived from the Elasticsearch docs on its longs:
|
40
|
+
# > A signed 64-bit integer with a minimum value of -2^63 and a maximum value of 2^63 - 1.
|
41
|
+
# (from https://www.elastic.co/guide/en/elasticsearch/reference/current/number.html)
|
42
|
+
LONG_STRING_MIN = -(2**63).to_int
|
43
|
+
LONG_STRING_MAX = -LONG_STRING_MIN - 1
|
44
|
+
|
45
|
+
# When indexing large string values into the datastore, we've observed errors like:
|
46
|
+
#
|
47
|
+
# > bytes can be at most 32766 in length
|
48
|
+
#
|
49
|
+
# This is also documented on the Elasticsearch docs site, under "Choosing a keyword family field type":
|
50
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/8.2/keyword.html#wildcard-field-type
|
51
|
+
#
|
52
|
+
# Note that it's a byte limit, but JSON schema's maxLength is a limit on the number of characters.
|
53
|
+
# UTF8 uses up to 4 bytes per character so to guard against a maliciously crafted payload, we limit
|
54
|
+
# the length to a quarter of 32766.
|
55
|
+
DEFAULT_MAX_KEYWORD_LENGTH = 32766 / 4
|
56
|
+
|
57
|
+
# Strings indexed as `text` can be much larger than `keyword` fields. In fact, there's no limitation
|
58
|
+
# on the `text` length, except for the overall size of the HTTP request body when we attempt to index
|
59
|
+
# a `text` field. By default it's limited to 100MB via the `http.max_content_length` setting:
|
60
|
+
#
|
61
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/8.11/modules-network.html#http-settings
|
62
|
+
#
|
63
|
+
# Note: there's no guarantee that `text` values shorter than this will succeed when indexing them--it
|
64
|
+
# depends on how many other fields and documents are included in the indexing payload, since the limit
|
65
|
+
# is on the overall payload size, and not on the size of one field. Given that, there's not really a
|
66
|
+
# discrete value we can use for the max length that guarantees successful indexing. But we know that
|
67
|
+
# values larger than this will fail, so this is the limit we use.
|
68
|
+
DEFAULT_MAX_TEXT_LENGTH = 100 * (2**20).to_int
|
69
|
+
|
70
|
+
# The name of the JSON schema definition for the ElasticGraph event envelope.
|
71
|
+
EVENT_ENVELOPE_JSON_SCHEMA_NAME = "ElasticGraphEventEnvelope"
|
72
|
+
|
73
|
+
# For some queries, we wind up needing a pagination cursor for a collection
|
74
|
+
# that will only ever contain a single value (and has no "key" to speak of
|
75
|
+
# to encode into a cursor). In those contexts, we'll use this as the cursor value.
|
76
|
+
# Ideally, we want this to be a value that could never be produced by our normal
|
77
|
+
# cursor encoding logic. This cursor is encoded from data that includes a UUID,
|
78
|
+
# which we can trust is unique.
|
79
|
+
SINGLETON_CURSOR = "eyJ1dWlkIjoiZGNhMDJkMjAtYmFlZS00ZWU5LWEwMjctZmVlY2UwYTZkZTNhIn0="
|
80
|
+
|
81
|
+
# Schema artifact file names.
|
82
|
+
GRAPHQL_SCHEMA_FILE = "schema.graphql"
|
83
|
+
JSON_SCHEMAS_FILE = "json_schemas.yaml"
|
84
|
+
DATASTORE_CONFIG_FILE = "datastore_config.yaml"
|
85
|
+
RUNTIME_METADATA_FILE = "runtime_metadata.yaml"
|
86
|
+
|
87
|
+
# Name for directory that contains versioned json_schemas files.
|
88
|
+
JSON_SCHEMAS_BY_VERSION_DIRECTORY = "json_schemas_by_version"
|
89
|
+
# Name for field in json schemas files that represents schema "version".
|
90
|
+
JSON_SCHEMA_VERSION_KEY = "json_schema_version"
|
91
|
+
|
92
|
+
# String that goes in the middle of a rollover index name, used to mark it as a rollover
|
93
|
+
# index (and split on to parse a rollover index name).
|
94
|
+
ROLLOVER_INDEX_INFIX_MARKER = "_rollover__"
|
95
|
+
|
96
|
+
DERIVED_INDEX_FAILURE_MESSAGE_PREAMBLE = "Derived index update failed due to bad input data"
|
97
|
+
|
98
|
+
# The current id of our static `index_data` update script. Verified by a test so you can count
|
99
|
+
# on it being accurate. We expose this as a constant so that we can detect this specific script
|
100
|
+
# in environments where we can't count on `elasticgraph-schema_definition` (where the script is
|
101
|
+
# defined) being available, since that gem is usually only used in development.
|
102
|
+
#
|
103
|
+
# Note: this constant is automatically kept up-to-date by our `schema_artifacts:dump` rake task.
|
104
|
+
INDEX_DATA_UPDATE_SCRIPT_ID = "update_index_data_d577eb4b07ee3c53b59f2f6d6c7b2413"
|
105
|
+
|
106
|
+
# The id of the old version of the update data script before ElasticGraph v0.9. For now, we are maintaining
|
107
|
+
# backwards compatibility with how it recorded event versions, and we have test coverage for that which relies
|
108
|
+
# upon this id.
|
109
|
+
#
|
110
|
+
# TODO: Drop this when we no longer need to maintain backwards-compatibility.
|
111
|
+
OLD_INDEX_DATA_UPDATE_SCRIPT_ID = "update_index_data_9b97090d5c97c4adc82dc7f4c2b89bc5"
|
112
|
+
|
113
|
+
# When an update script has a no-op result we often want to communicate more information about
|
114
|
+
# why it was a no-op back to ElatsicGraph from the script. The only way to do that is to throw
|
115
|
+
# an exception with an error message, but, as far as I can tell, painless doesn't let you define
|
116
|
+
# custom exception classes. To allow elasticgraph-indexer to detect that the script "failed" due
|
117
|
+
# to a no-op (rather than a true failure) we include this common preamble in the exception message
|
118
|
+
# thrown from our update scripts for the no-op case.
|
119
|
+
UPDATE_WAS_NOOP_MESSAGE_PREAMBLE = "ElasticGraph update was a no-op: "
|
120
|
+
|
121
|
+
# The name used to refer to a document's own/primary source event (that is, the event that has a `type`
|
122
|
+
# matching the document's type). The name here was chosen to avoid naming collisions with relationships
|
123
|
+
# defined via the `relates_to_one`/`relates_to_many` APIs. The GraphQL spec reserves the double-underscore
|
124
|
+
# prefix on field names, which means that users cannot define a relationship named `__self` via the
|
125
|
+
# `relates_to_one`/`relates_to_many` APIs.
|
126
|
+
SELF_RELATIONSHIP_NAME = "__self"
|
127
|
+
|
128
|
+
# This regex aligns with the datastore format of HH:mm:ss || HH:mm:ss.S || HH:mm:ss.SS || HH:mm:ss.SSS
|
129
|
+
# See https://rubular.com/r/NHjBWrpZvzOTJO for examples.
|
130
|
+
VALID_LOCAL_TIME_REGEX = /\A(([0-1][0-9])|(2[0-3])):[0-5][0-9]:[0-5][0-9](\.[0-9]{1,3})?\z/
|
131
|
+
|
132
|
+
# `VALID_LOCAL_TIME_REGEX`, expressed as a JSON schema pattern. JSON schema supports a subset of
|
133
|
+
# Ruby Regexp features and is expressed as a String object. Here we convert from the Ruby Regexp
|
134
|
+
# start-and-end-of-string anchors (\A and \z) and convert them to the JSON schema ones (^ and $).
|
135
|
+
#
|
136
|
+
# For more info, see:
|
137
|
+
# https://json-schema.org/understanding-json-schema/reference/regular_expressions.html
|
138
|
+
# http://www.rexegg.com/regex-anchors.html
|
139
|
+
VALID_LOCAL_TIME_JSON_SCHEMA_PATTERN = VALID_LOCAL_TIME_REGEX.source.sub(/\A\\A/, "^").sub(/\\z\z/, "$")
|
140
|
+
|
141
|
+
# Special hidden field defined in an index where we store the count of elements in each list field.
|
142
|
+
# We index the list counts so that we can offer a `count` filter operator on list fields, allowing
|
143
|
+
# clients to query on the count of list elements.
|
144
|
+
#
|
145
|
+
# The field name has a leading `__` because the GraphQL spec reserves that prefix for its own use,
|
146
|
+
# and we can therefore assume that no GraphQL fields have this name.
|
147
|
+
LIST_COUNTS_FIELD = "__counts"
|
148
|
+
|
149
|
+
# Character used to separate parts of a field path for the keys in the special `__counts`
|
150
|
+
# field which contains the counts of the various list fields. We were going to use a dot
|
151
|
+
# (as you'd expect) but ran into errors like this from the datastore:
|
152
|
+
#
|
153
|
+
# > can't merge a non object mapping [seasons.players.__counts.seasons] with an object mapping
|
154
|
+
#
|
155
|
+
# When we have a list of `object`, and then a list field on that object type, we want to
|
156
|
+
# store the count of both the parent list and the child list, but if we use dots then the datastore
|
157
|
+
# treats it like a nested JSON object, and the JSON entry at the parent path can't both be an integer
|
158
|
+
# (for the parent list count) and an object containing counts of its child lists.
|
159
|
+
#
|
160
|
+
# By using `|` instead of `.`, we avoid this problem.
|
161
|
+
LIST_COUNTS_FIELD_PATH_KEY_SEPARATOR = "|"
|
162
|
+
|
163
|
+
# The set of datastore field types which have no `properties` in the mapping, but which
|
164
|
+
# can be represented as a JSON object at indexing time.
|
165
|
+
#
|
166
|
+
# I built this list by auditing the full list of index field mapping types:
|
167
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/8.9/mapping-types.html
|
168
|
+
DATASTORE_PROPERTYLESS_OBJECT_TYPES = [
|
169
|
+
"aggregate_metric_double", # https://www.elastic.co/guide/en/elasticsearch/reference/8.9/aggregate-metric-double.html
|
170
|
+
"completion", # https://www.elastic.co/guide/en/elasticsearch/reference/8.9/search-suggesters.html#completion-suggester
|
171
|
+
"flattened", # https://www.elastic.co/guide/en/elasticsearch/reference/8.9/flattened.html
|
172
|
+
"geo_point", # https://www.elastic.co/guide/en/elasticsearch/reference/8.9/geo-point.html
|
173
|
+
"geo_shape", # https://www.elastic.co/guide/en/elasticsearch/reference/8.9/geo-shape.html
|
174
|
+
"histogram", # https://www.elastic.co/guide/en/elasticsearch/reference/8.9/histogram.html
|
175
|
+
"join", # https://www.elastic.co/guide/en/elasticsearch/reference/8.9/parent-join.html
|
176
|
+
"percolator", # https://www.elastic.co/guide/en/elasticsearch/reference/8.9/percolator.html
|
177
|
+
"point", # https://www.elastic.co/guide/en/elasticsearch/reference/8.9/point.html
|
178
|
+
"range", # https://www.elastic.co/guide/en/elasticsearch/reference/8.9/range.html
|
179
|
+
"rank_features", # https://www.elastic.co/guide/en/elasticsearch/reference/8.9/rank-features.html
|
180
|
+
"shape" # https://www.elastic.co/guide/en/elasticsearch/reference/8.9/shape.html
|
181
|
+
].to_set
|
182
|
+
|
183
|
+
# This pattern matches the spec for a valid GraphQL name:
|
184
|
+
# http://spec.graphql.org/June2018/#sec-Names
|
185
|
+
#
|
186
|
+
# ...however, it allows additional non-valid characters before and after it.
|
187
|
+
GRAPHQL_NAME_WITHIN_LARGER_STRING_PATTERN = /[_A-Za-z][_0-9A-Za-z]*/
|
188
|
+
|
189
|
+
# This pattern exactly matches a valid GraphQL name, with no extra characters allowed before or after.
|
190
|
+
GRAPHQL_NAME_PATTERN = /\A#{GRAPHQL_NAME_WITHIN_LARGER_STRING_PATTERN}\z/
|
191
|
+
|
192
|
+
# Description in English of the requirements for GraphQL names. (Used in multiple error messages).
|
193
|
+
GRAPHQL_NAME_VALIDITY_DESCRIPTION = "Names are limited to ASCII alphanumeric characters (plus underscore), and cannot start with a number."
|
194
|
+
|
195
|
+
# The standard set of scalars that are defined by the GraphQL spec:
|
196
|
+
# https://spec.graphql.org/October2021/#sec-Scalars
|
197
|
+
STOCK_GRAPHQL_SCALARS = %w[Boolean Float ID Int String].to_set.freeze
|
198
|
+
|
199
|
+
# The current variant of JSON schema that we use.
|
200
|
+
JSON_META_SCHEMA = "http://json-schema.org/draft-07/schema#"
|
201
|
+
|
202
|
+
# Filter the bulk response payload with a comma separated list using dot notation.
|
203
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/7.10/common-options.html#common-options-response-filtering
|
204
|
+
#
|
205
|
+
# Note: anytime you change this constant, be sure to check all the comments in the unit specs that mention this constant.
|
206
|
+
# When stubbing a datastore client test double, it doesn't respect this filtering obviously, so it's up to us
|
207
|
+
# to accurately mimic the filtering in our stubbed responses.
|
208
|
+
DATASTORE_BULK_FILTER_PATH = [
|
209
|
+
# The key under `items` names the type of operation (e.g. `index` or `update`) and
|
210
|
+
# we use a `*` for it since we always use that key, regardless of which operation it is.
|
211
|
+
"items.*.status", "items.*.result", "items.*.error"
|
212
|
+
].join(",")
|
213
|
+
|
214
|
+
# HTTP header set by `elasticgraph-graphql_lambda` to indicate the AWS ARN of the caller.
|
215
|
+
GRAPHQL_LAMBDA_AWS_ARN_HEADER = "X-AWS-LAMBDA-CALLER-ARN"
|
216
|
+
|
217
|
+
# TODO(steep): it complains about `define_schema` not being defined but it is defined
|
218
|
+
# in another file; I shouldn't have to say it's dynamic here. For now this works though.
|
219
|
+
# @dynamic self.define_schema
|
220
|
+
end
|
@@ -0,0 +1,99 @@
|
|
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 Error < StandardError
|
11
|
+
end
|
12
|
+
|
13
|
+
class CursorEncoderError < Error
|
14
|
+
end
|
15
|
+
|
16
|
+
class InvalidSortFieldsError < CursorEncoderError
|
17
|
+
end
|
18
|
+
|
19
|
+
class InvalidCursorError < CursorEncoderError
|
20
|
+
end
|
21
|
+
|
22
|
+
class CursorEncodingError < CursorEncoderError
|
23
|
+
end
|
24
|
+
|
25
|
+
class CountUnavailableError < Error
|
26
|
+
end
|
27
|
+
|
28
|
+
class InvalidArgumentValueError < Error
|
29
|
+
end
|
30
|
+
|
31
|
+
class InvalidAggregationKeyError < Error
|
32
|
+
end
|
33
|
+
|
34
|
+
class InvalidMergeError < Error
|
35
|
+
end
|
36
|
+
|
37
|
+
class QueryMergeError < Error
|
38
|
+
end
|
39
|
+
|
40
|
+
class SchemaError < Error
|
41
|
+
end
|
42
|
+
|
43
|
+
class InvalidGraphQLNameError < SchemaError
|
44
|
+
end
|
45
|
+
|
46
|
+
class NotFoundError < Error
|
47
|
+
end
|
48
|
+
|
49
|
+
class SearchFailedError < Error
|
50
|
+
end
|
51
|
+
|
52
|
+
class RequestExceededDeadlineError < SearchFailedError
|
53
|
+
end
|
54
|
+
|
55
|
+
class IdentifyDocumentVersionsFailedError < Error
|
56
|
+
end
|
57
|
+
|
58
|
+
class IndexOperationError < Error
|
59
|
+
end
|
60
|
+
|
61
|
+
class ClusterOperationError < Error
|
62
|
+
end
|
63
|
+
|
64
|
+
class InvalidEventIDError < Error
|
65
|
+
end
|
66
|
+
|
67
|
+
class UnsupportedOperationError < Error
|
68
|
+
end
|
69
|
+
|
70
|
+
class InvalidExtensionError < Error
|
71
|
+
end
|
72
|
+
|
73
|
+
class ConfigError < Error
|
74
|
+
end
|
75
|
+
|
76
|
+
class ConfigSettingNotSetError < ConfigError
|
77
|
+
end
|
78
|
+
|
79
|
+
class ConfigCannotBeMutatedError < ConfigError
|
80
|
+
end
|
81
|
+
|
82
|
+
class UnknownYAMLSettingError < ConfigError
|
83
|
+
end
|
84
|
+
|
85
|
+
class InvalidScriptDirectoryError < Error
|
86
|
+
end
|
87
|
+
|
88
|
+
class MissingSchemaArtifactError < Error
|
89
|
+
end
|
90
|
+
|
91
|
+
class S3OperationFailedError < Error
|
92
|
+
end
|
93
|
+
|
94
|
+
class MessageIdsMissingError < Error
|
95
|
+
end
|
96
|
+
|
97
|
+
class BadDatastoreRequest < Error
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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
|
+
module Support
|
11
|
+
module FaradayMiddleware
|
12
|
+
# Custom Faraday middleware that forces `msearch` calls to use an HTTP GET instead of an HTTP POST. While not
|
13
|
+
# necessary, it preserves a useful property: all "read" calls made by ElasticGraph use an HTTP GET, and HTTP POST
|
14
|
+
# requests are "write" calls. This allows the access policy to only grant HTTP GET access from the GraphQL endpoint,
|
15
|
+
# which leads to a more secure setup (as the GraphQL endpoint can be blocked from performing any writes).
|
16
|
+
#
|
17
|
+
# Note: before elasticsearch-ruby 7.9.0, `msearch` used an HTTP GET request, so this simply restores that behavior.
|
18
|
+
# This results in an HTTP GET with a request body, but it works just fine and its what the Ruby Elasticsearch client
|
19
|
+
# did for years.
|
20
|
+
#
|
21
|
+
# For more info, see: https://github.com/elastic/elasticsearch-ruby/issues/1005
|
22
|
+
MSearchUsingGetInsteadOfPost = ::Data.define(:app) do
|
23
|
+
# @implements MSearchUsingGetInsteadOfPost
|
24
|
+
def call(env)
|
25
|
+
env.method = :get if env.url.path.to_s.end_with?("/_msearch")
|
26
|
+
app.call(env)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,36 @@
|
|
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/error"
|
11
|
+
|
12
|
+
module ElasticGraph
|
13
|
+
module Support
|
14
|
+
module FaradayMiddleware
|
15
|
+
# Faraday supports specifying a timeout at both the client level (when building the Faraday connection) or on a
|
16
|
+
# per-request basis. We want to specify it on a per-request basis, but unfortunately, the Elasticsearch/OpenSearch
|
17
|
+
# clients don't provide any per-request API to specify the timeout (it only supports it when instantiating your
|
18
|
+
# client).
|
19
|
+
#
|
20
|
+
# This middleware helps us work around this deficiency by looking for the TIMEOUT_MS_HEADER. If present, it deletes
|
21
|
+
# it from the headers and instead sets it as the request timeout.
|
22
|
+
SupportTimeouts = ::Data.define(:app) do
|
23
|
+
# @implements SupportTimeouts
|
24
|
+
def call(env)
|
25
|
+
if (timeout_ms = env.request_headers.delete(TIMEOUT_MS_HEADER))
|
26
|
+
env.request.timeout = timeout_ms / 1000.0
|
27
|
+
end
|
28
|
+
|
29
|
+
app.call(env)
|
30
|
+
rescue ::Faraday::TimeoutError
|
31
|
+
raise RequestExceededDeadlineError, "Datastore request exceeded timeout of #{timeout_ms} ms."
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,53 @@
|
|
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 "yaml"
|
10
|
+
|
11
|
+
module ElasticGraph
|
12
|
+
module Support
|
13
|
+
module FromYamlFile
|
14
|
+
# Factory method that will build an instance from the provided `yaml_file`.
|
15
|
+
# `datastore_client_customization_block:` can be passed to customize the datastore clients.
|
16
|
+
# In addition, a block is accepted that can prepare the settings before the object is built
|
17
|
+
# (e.g. to override specific settings).
|
18
|
+
def from_yaml_file(yaml_file, datastore_client_customization_block: nil)
|
19
|
+
parsed_yaml = ::YAML.safe_load_file(yaml_file, aliases: true)
|
20
|
+
parsed_yaml = yield(parsed_yaml) if block_given?
|
21
|
+
from_parsed_yaml(parsed_yaml, &datastore_client_customization_block)
|
22
|
+
end
|
23
|
+
|
24
|
+
# An extension module that provides a `from_yaml_file` factory method on a `RakeTasks` class.
|
25
|
+
#
|
26
|
+
# This is designed for a `RakeTasks` class that needs an ElasticGraph component (e.g. an
|
27
|
+
# `ElasticGraph::GraphQL`, `ElasticGraph::Admin`, or `ElasticGraph::Indexer` instance).
|
28
|
+
# When the schema artifacts are out of date, loading those components can fail. This gracefully
|
29
|
+
# handles that for you, giving you clear instructions of what to do when this happens.
|
30
|
+
#
|
31
|
+
# This requires the `RakeTasks` class to accept the ElasticGraph component instance via a block
|
32
|
+
# so that it happens lazily.
|
33
|
+
class ForRakeTasks < ::Module
|
34
|
+
# @dynamic from_yaml_file
|
35
|
+
|
36
|
+
def initialize(component_class)
|
37
|
+
define_method :from_yaml_file do |yaml_file, *args, **options|
|
38
|
+
__skip__ = new(*args, **options) do
|
39
|
+
component_class.from_yaml_file(yaml_file)
|
40
|
+
rescue => e
|
41
|
+
raise <<~EOS
|
42
|
+
Failed to load `#{component_class}` with `#{yaml_file}`. This can happen if the schema artifacts are out of date.
|
43
|
+
Run `rake schema_artifacts:dump` and try again.
|
44
|
+
|
45
|
+
#{e.class}: #{e.message}
|
46
|
+
EOS
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,66 @@
|
|
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 "json"
|
10
|
+
|
11
|
+
module ElasticGraph
|
12
|
+
module Support
|
13
|
+
# Utility module that provides helper methods for generating well-formatted GraphQL syntax.
|
14
|
+
module GraphQLFormatter
|
15
|
+
# Formats the given hash as an argument list. If `args` is empty, returns an empty string.
|
16
|
+
# Otherwise, wraps the args list in parens. This allows the returned string to be appended
|
17
|
+
# to a field or directive, and it'll correctly use parens (or not) based on if there are args
|
18
|
+
# or not.
|
19
|
+
def self.format_args(**args)
|
20
|
+
return "" if args.empty?
|
21
|
+
"(#{serialize(args, wrap_hash_with_braces: false)})"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Formats the given value in GraphQL syntax. This method was derived
|
25
|
+
# from a similar method from the graphql-ruby gem:
|
26
|
+
#
|
27
|
+
# https://github.com/rmosolgo/graphql-ruby/blob/v1.11.4/lib/graphql/language.rb#L17-L33
|
28
|
+
#
|
29
|
+
# We don't want to use that method because it is marked as `@api private`, indicating
|
30
|
+
# it could be removed in any release of the graphql gem. If we used it, it could hinder
|
31
|
+
# future upgrades.
|
32
|
+
#
|
33
|
+
# Our implementation here differs in a few ways:
|
34
|
+
#
|
35
|
+
# - case statement instead of multiple `if value.is_a?` checks (a bit cleaner)
|
36
|
+
# - `wrap_hash_with_braces` since we do not want to wrap an args hash with braces.
|
37
|
+
# - Readable spacing has been added so we get `foo: [1, 2], bar: 3` instead of `foo:[1,2],bar:3`.
|
38
|
+
# - Symbol support has been added. Symbols are converted to strings (with no quotes), allowing
|
39
|
+
# callers to pass them for GraphQL enums.
|
40
|
+
# - We've removed the `quirks_mode: true` flag passed to `JSON.generate` since it has been
|
41
|
+
# deprecated for a while: https://github.com/flori/json/issues/309
|
42
|
+
def self.serialize(value, wrap_hash_with_braces: true)
|
43
|
+
case value
|
44
|
+
when ::Hash
|
45
|
+
serialized_hash = value.map do |k, v|
|
46
|
+
"#{k}: #{serialize v}"
|
47
|
+
end.join(", ")
|
48
|
+
|
49
|
+
return serialized_hash unless wrap_hash_with_braces
|
50
|
+
|
51
|
+
"{#{serialized_hash}}"
|
52
|
+
when ::Array
|
53
|
+
serialized_array = value.map do |v|
|
54
|
+
serialize v
|
55
|
+
end.join(", ")
|
56
|
+
|
57
|
+
"[#{serialized_array}]"
|
58
|
+
when ::Symbol
|
59
|
+
value.to_s
|
60
|
+
else
|
61
|
+
::JSON.generate(value)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|