elasticgraph-support 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,6 @@
1
+ # ElasticGraph::Support
2
+
3
+ This gem provides support utilities for the rest of the ElasticGraph gems. As
4
+ such, it is not intended to provide any public APIs for ElasticGraph users.
5
+
6
+ Importantly, it is intended to have *zero* dependencies.
@@ -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