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 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