elasticgraph-schema_definition 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +7 -0
  4. data/elasticgraph-schema_definition.gemspec +26 -0
  5. data/lib/elastic_graph/schema_definition/api.rb +359 -0
  6. data/lib/elastic_graph/schema_definition/factory.rb +506 -0
  7. data/lib/elastic_graph/schema_definition/indexing/derived_fields/append_only_set.rb +79 -0
  8. data/lib/elastic_graph/schema_definition/indexing/derived_fields/field_initializer_support.rb +59 -0
  9. data/lib/elastic_graph/schema_definition/indexing/derived_fields/immutable_value.rb +99 -0
  10. data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +62 -0
  11. data/lib/elastic_graph/schema_definition/indexing/derived_indexed_type.rb +346 -0
  12. data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +74 -0
  13. data/lib/elastic_graph/schema_definition/indexing/field.rb +181 -0
  14. data/lib/elastic_graph/schema_definition/indexing/field_reference.rb +51 -0
  15. data/lib/elastic_graph/schema_definition/indexing/field_type/enum.rb +65 -0
  16. data/lib/elastic_graph/schema_definition/indexing/field_type/object.rb +113 -0
  17. data/lib/elastic_graph/schema_definition/indexing/field_type/scalar.rb +51 -0
  18. data/lib/elastic_graph/schema_definition/indexing/field_type/union.rb +70 -0
  19. data/lib/elastic_graph/schema_definition/indexing/index.rb +318 -0
  20. data/lib/elastic_graph/schema_definition/indexing/json_schema_field_metadata.rb +34 -0
  21. data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +234 -0
  22. data/lib/elastic_graph/schema_definition/indexing/list_counts_mapping.rb +53 -0
  23. data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +96 -0
  24. data/lib/elastic_graph/schema_definition/indexing/rollover_config.rb +25 -0
  25. data/lib/elastic_graph/schema_definition/indexing/update_target_factory.rb +54 -0
  26. data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +195 -0
  27. data/lib/elastic_graph/schema_definition/json_schema_pruner.rb +61 -0
  28. data/lib/elastic_graph/schema_definition/mixins/can_be_graphql_only.rb +31 -0
  29. data/lib/elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations.rb +119 -0
  30. data/lib/elastic_graph/schema_definition/mixins/has_directives.rb +65 -0
  31. data/lib/elastic_graph/schema_definition/mixins/has_documentation.rb +74 -0
  32. data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +281 -0
  33. data/lib/elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect.rb +46 -0
  34. data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +116 -0
  35. data/lib/elastic_graph/schema_definition/mixins/has_type_info.rb +181 -0
  36. data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +122 -0
  37. data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +47 -0
  38. data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +267 -0
  39. data/lib/elastic_graph/schema_definition/mixins/verifies_graphql_name.rb +38 -0
  40. data/lib/elastic_graph/schema_definition/rake_tasks.rb +190 -0
  41. data/lib/elastic_graph/schema_definition/results.rb +404 -0
  42. data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +482 -0
  43. data/lib/elastic_graph/schema_definition/schema_elements/argument.rb +56 -0
  44. data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +1541 -0
  45. data/lib/elastic_graph/schema_definition/schema_elements/deprecated_element.rb +21 -0
  46. data/lib/elastic_graph/schema_definition/schema_elements/directive.rb +40 -0
  47. data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +189 -0
  48. data/lib/elastic_graph/schema_definition/schema_elements/enum_value.rb +73 -0
  49. data/lib/elastic_graph/schema_definition/schema_elements/enum_value_namer.rb +89 -0
  50. data/lib/elastic_graph/schema_definition/schema_elements/enums_for_indexed_types.rb +82 -0
  51. data/lib/elastic_graph/schema_definition/schema_elements/field.rb +1085 -0
  52. data/lib/elastic_graph/schema_definition/schema_elements/field_path.rb +112 -0
  53. data/lib/elastic_graph/schema_definition/schema_elements/field_source.rb +16 -0
  54. data/lib/elastic_graph/schema_definition/schema_elements/graphql_sdl_enumerator.rb +113 -0
  55. data/lib/elastic_graph/schema_definition/schema_elements/input_field.rb +31 -0
  56. data/lib/elastic_graph/schema_definition/schema_elements/input_type.rb +60 -0
  57. data/lib/elastic_graph/schema_definition/schema_elements/interface_type.rb +72 -0
  58. data/lib/elastic_graph/schema_definition/schema_elements/list_counts_state.rb +40 -0
  59. data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +53 -0
  60. data/lib/elastic_graph/schema_definition/schema_elements/relationship.rb +218 -0
  61. data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +310 -0
  62. data/lib/elastic_graph/schema_definition/schema_elements/sort_order_enum_value.rb +36 -0
  63. data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +66 -0
  64. data/lib/elastic_graph/schema_definition/schema_elements/type_namer.rb +237 -0
  65. data/lib/elastic_graph/schema_definition/schema_elements/type_reference.rb +353 -0
  66. data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +579 -0
  67. data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +157 -0
  68. data/lib/elastic_graph/schema_definition/scripting/file_system_repository.rb +77 -0
  69. data/lib/elastic_graph/schema_definition/scripting/script.rb +48 -0
  70. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_day_of_week.painless +24 -0
  71. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_time_of_day.painless +41 -0
  72. data/lib/elastic_graph/schema_definition/scripting/scripts/filter/by_time_of_day.painless +22 -0
  73. data/lib/elastic_graph/schema_definition/scripting/scripts/update/index_data.painless +93 -0
  74. data/lib/elastic_graph/schema_definition/state.rb +212 -0
  75. data/lib/elastic_graph/schema_definition/test_support.rb +113 -0
  76. metadata +513 -0
@@ -0,0 +1,181 @@
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/json_schema/meta_schema_validator"
10
+
11
+ module ElasticGraph
12
+ module SchemaDefinition
13
+ module Mixins
14
+ # Mixin used to specify non-GraphQL type info (datastore index and JSON schema type info).
15
+ # Exists as a mixin so we can apply the same consistent API to every place we need to use this.
16
+ # Currently it's used in 3 places:
17
+ #
18
+ # - {SchemaElements::ScalarType}: allows specification of how scalars are represented in JSON schema and the index.
19
+ # - {SchemaElements::TypeWithSubfields}: allows customization of how an object type is represented in JSON schema and the index.
20
+ # - {SchemaElements::Field}: allows customization of a specific field over the field type's standard JSON schema and the index mapping.
21
+ module HasTypeInfo
22
+ # @return [Hash<Symbol, Object>] datastore mapping options
23
+ def mapping_options
24
+ @mapping_options ||= {}
25
+ end
26
+
27
+ # @return [Hash<Symbol, Object>] JSON schema options
28
+ def json_schema_options
29
+ @json_schema_options ||= {}
30
+ end
31
+
32
+ # Set of mapping parameters that it makes sense to allow customization of, based on
33
+ # [the Elasticsearch docs](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/mapping-params.html).
34
+ CUSTOMIZABLE_DATASTORE_PARAMS = Set[
35
+ :analyzer,
36
+ :eager_global_ordinals,
37
+ :enabled,
38
+ :fields,
39
+ :format,
40
+ :index,
41
+ :meta, # not actually in the doc above. Added to support some `index_configurator` tests on 7.9+.
42
+ :norms,
43
+ :null_value,
44
+ :search_analyzer,
45
+ :type,
46
+ ]
47
+
48
+ # Defines the Elasticsearch/OpenSearch [field mapping type](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-types.html)
49
+ # and [mapping parameters](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-params.html) for a field or type.
50
+ # The options passed here will be included in the generated `datastore_config.yaml` artifact that ElasticGraph uses to configure
51
+ # Elasticsearch/OpenSearch.
52
+ #
53
+ # Can be called multiple times; each time, the options will be merged into the existing options.
54
+ #
55
+ # This is required on a {SchemaElements::ScalarType}; without it, ElasticGraph would have no way to know how the datatype should be
56
+ # indexed in the datastore.
57
+ #
58
+ # On a {SchemaElements::Field}, this can be used to customize how a field is indexed. For example, `String` fields are normally
59
+ # indexed as [keywords](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/keyword.html); to instead index a `String`
60
+ # field for full text search, you’d need to configure `mapping type: "text"`.
61
+ #
62
+ # On a {SchemaElements::ObjectType}, this can be used to use a specific Elasticsearch/OpenSearch data type for something that is
63
+ # modeled as an object in GraphQL. For example, we use it for the `GeoLocation` type so they get indexed in Elasticsearch using the
64
+ # [geo_point type](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/geo-point.html).
65
+ #
66
+ # @param options [Hash<Symbol, Object>] mapping options--must be limited to {CUSTOMIZABLE_DATASTORE_PARAMS}
67
+ # @return [void]
68
+ #
69
+ # @example Define the mapping of a custom scalar type
70
+ # ElasticGraph.define_schema do |schema|
71
+ # schema.scalar_type "URL" do |t|
72
+ # t.mapping type: "keyword"
73
+ # t.json_schema type: "string", format: "uri"
74
+ # end
75
+ # end
76
+ #
77
+ # @example Customize the mapping of a field
78
+ # ElasticGraph.define_schema do |schema|
79
+ # schema.object_type "Card" do |t|
80
+ # t.field "id", "ID!"
81
+ #
82
+ # t.field "cardholderName", "String" do |f|
83
+ # # index this field for full text search
84
+ # f.mapping type: "text"
85
+ # end
86
+ #
87
+ # t.field "expYear", "Int" do |f|
88
+ # # Use a smaller numeric type to save space in the datastore
89
+ # f.mapping type: "short"
90
+ # f.json_schema minimum: 2000, maximum: 2099
91
+ # end
92
+ #
93
+ # t.field "expMonth", "Int" do |f|
94
+ # # Use a smaller numeric type to save space in the datastore
95
+ # f.mapping type: "byte"
96
+ # f.json_schema minimum: 1, maximum: 12
97
+ # end
98
+ #
99
+ # t.index "cards"
100
+ # end
101
+ # end
102
+ def mapping(**options)
103
+ param_diff = (options.keys.to_set - CUSTOMIZABLE_DATASTORE_PARAMS).to_a
104
+
105
+ unless param_diff.empty?
106
+ raise SchemaError, "Some configured mapping overrides are unsupported: #{param_diff.inspect}"
107
+ end
108
+
109
+ mapping_options.update(options)
110
+ end
111
+
112
+ # Defines the [JSON schema](https://json-schema.org/understanding-json-schema/) validations for this field or type. Validations
113
+ # defined here will be included in the generated `json_schemas.yaml` artifact, which is used by the ElasticGraph indexer to
114
+ # validate events before indexing their data in the datastore. In addition, the publisher may use `json_schemas.yaml` for code
115
+ # generation and to apply validation before publishing an event to ElasticGraph.
116
+ #
117
+ # Can be called multiple times; each time, the options will be merged into the existing options.
118
+ #
119
+ # This is _required_ on a {SchemaElements::ScalarType} (since we don’t know how a custom scalar type should be represented in
120
+ # JSON!). On a {SchemaElements::Field}, this is optional, but can be used to make the JSON schema validation stricter then it
121
+ # would otherwise be. For example, you could use `json_schema maxLength: 30` on a `String` field to limit the length.
122
+ #
123
+ # You can use any of the JSON schema validation keywords here. In addition, `nullable: false` is supported to configure the
124
+ # generated JSON schema to disallow `null` values for the field. Note that if you define a field with a non-nullable GraphQL type
125
+ # (e.g. `Int!`), the JSON schema will automatically disallow nulls. However, as explained in the
126
+ # {SchemaElements::TypeWithSubfields#field} documentation, we generally recommend against defining non-nullable GraphQL fields.
127
+ # `json_schema nullable: false` will disallow `null` values from being indexed, while still keeping the field nullable in the
128
+ # GraphQL schema. If you think you might want to make a field non-nullable in the GraphQL schema some day, it’s a good idea to use
129
+ # `json_schema nullable: false` now to ensure every indexed record has a non-null value for the field.
130
+ #
131
+ # @note We recommend using JSON schema validations in a limited fashion. Validations that are appropriate to apply when data is
132
+ # entering the system-of-record are often not appropriate on a secondary index like ElasticGraph. Events that violate a JSON
133
+ # schema validation will fail to index (typically they will be sent to the dead letter queue and page an oncall engineer). If an
134
+ # ElasticGraph instance is meant to contain all the data of some source system, you probably don’t want it applying stricter
135
+ # validations than the source system itself has. We recommend limiting your JSON schema validations to situations where
136
+ # violations would prevent ElasticGraph from operating correctly.
137
+ #
138
+ # @param options [Hash<Symbol, Object>] JSON schema options
139
+ # @return [void]
140
+ #
141
+ # @example Define the JSON schema validations of a custom scalar type
142
+ # ElasticGraph.define_schema do |schema|
143
+ # schema.scalar_type "URL" do |t|
144
+ # t.mapping type: "keyword"
145
+ #
146
+ # # JSON schema has a built-in URI format validator:
147
+ # # https://json-schema.org/understanding-json-schema/reference/string.html#resource-identifiers
148
+ # t.json_schema type: "string", format: "uri"
149
+ # end
150
+ # end
151
+ #
152
+ # @example Define additional validations on a field
153
+ # ElasticGraph.define_schema do |schema|
154
+ # schema.object_type "Card" do |t|
155
+ # t.field "id", "ID!"
156
+ #
157
+ # t.field "expYear", "Int" do |f|
158
+ # # Use JSON schema to ensure the publisher is sending us 4 digit years, not 2 digit years.
159
+ # f.json_schema minimum: 2000, maximum: 2099
160
+ # end
161
+ #
162
+ # t.field "expMonth", "Int" do |f|
163
+ # f.json_schema minimum: 1, maximum: 12
164
+ # end
165
+ #
166
+ # t.index "cards"
167
+ # end
168
+ # end
169
+ def json_schema(**options)
170
+ validatable_json_schema = Support::HashUtil.stringify_keys(options)
171
+
172
+ if (error_msg = JSONSchema.strict_meta_schema_validator.validate_with_error_message(validatable_json_schema))
173
+ raise SchemaError, "Invalid JSON schema options set on #{self}:\n\n#{error_msg}"
174
+ end
175
+
176
+ json_schema_options.update(options)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,122 @@
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/error"
10
+
11
+ module ElasticGraph
12
+ module SchemaDefinition
13
+ module Mixins
14
+ # Mixin for types that can implement interfaces ({SchemaElements::ObjectType} and {SchemaElements::InterfaceType}).
15
+ module ImplementsInterfaces
16
+ # Declares that the current type implements the specified interface, making the current type a subtype of the interface. The
17
+ # current type must define all of the fields of the named interface, with the exact same field types.
18
+ #
19
+ # @param interface_names [Array<String>] names of interface types implemented by this type
20
+ # @return [void]
21
+ #
22
+ # @example Implement an interface
23
+ # ElasticGraph.define_schema do |schema|
24
+ # schema.interface_type "Athlete" do |t|
25
+ # t.field "name", "String"
26
+ # t.field "team", "String"
27
+ # end
28
+ #
29
+ # schema.object_type "BaseballPlayer" do |t|
30
+ # t.implements "Athlete"
31
+ # t.field "name", "String"
32
+ # t.field "team", "String"
33
+ # t.field "battingAvg", "Float"
34
+ # end
35
+ #
36
+ # schema.object_type "BasketballPlayer" do |t|
37
+ # t.implements "Athlete"
38
+ # t.field "name", "String"
39
+ # t.field "team", "String"
40
+ # t.field "pointsPerGame", "Float"
41
+ # end
42
+ # end
43
+ def implements(*interface_names)
44
+ interface_refs = interface_names.map do |interface_name|
45
+ schema_def_state.type_ref(interface_name).to_final_form.tap do |interface_ref|
46
+ schema_def_state.implementations_by_interface_ref[interface_ref] << self
47
+ end
48
+ end
49
+
50
+ implemented_interfaces.concat(interface_refs)
51
+ end
52
+
53
+ # @return [Array<SchemaElements::TypeReference>] list of type references for the interface types implemented by this type
54
+ def implemented_interfaces
55
+ @implemented_interfaces ||= []
56
+ end
57
+
58
+ # Called after the schema definition is complete, before dumping artifacts. Here we validate
59
+ # the correctness of interface implementations. We defer it until this time to not require the
60
+ # interface and fields to be defined before the `implements` call.
61
+ #
62
+ # Note that the GraphQL gem on its own supports a form of "interface inheritance": if declaring
63
+ # that an object type implements an interface, and the object type is missing one or more of the
64
+ # interface fields, the GraphQL gem dynamically adds the missing interface fields to the object
65
+ # type (at least, that's the result I noted when dumping the GraphQL SDL after trying that!).
66
+ # However, we cannot allow that, because our schema definition is used to generate non-GrapQL
67
+ # artifacts (e.g. the JSON schema and the index mapping), and all the artifacts must agree
68
+ # on the fields. Therefore, we use this method to verify that the object type fully implements
69
+ # the specified interfaces.
70
+ #
71
+ # @return [void]
72
+ # @private
73
+ def verify_graphql_correctness!
74
+ schema_error_messages = implemented_interfaces.filter_map do |interface_ref|
75
+ interface = interface_ref.resolved
76
+
77
+ case interface
78
+ when SchemaElements::InterfaceType
79
+ differences = (_ = interface).interface_fields_by_name.values.filter_map do |interface_field|
80
+ my_field_sdl = graphql_fields_by_name[interface_field.name]&.to_sdl(type_structure_only: true)
81
+ interface_field_sdl = interface_field.to_sdl(type_structure_only: true)
82
+
83
+ if my_field_sdl.nil?
84
+ "missing `#{interface_field.name}`"
85
+ elsif my_field_sdl != interface_field_sdl
86
+ "`#{interface_field_sdl.strip}` vs `#{my_field_sdl.strip}`"
87
+ end
88
+ end
89
+
90
+ unless differences.empty?
91
+ "Type `#{name}` does not correctly implement interface `#{interface_ref}` " \
92
+ "due to field differences: #{differences.join("; ")}."
93
+ end
94
+ when nil
95
+ "Type `#{name}` cannot implement `#{interface_ref}` because `#{interface_ref}` is not defined."
96
+ else
97
+ "Type `#{name}` cannot implement `#{interface_ref}` because `#{interface_ref}` is not an interface."
98
+ end
99
+ end
100
+
101
+ unless schema_error_messages.empty?
102
+ raise SchemaError, schema_error_messages.join("\n\n")
103
+ end
104
+ end
105
+
106
+ # @yield [SchemaElements::Argument] an argument
107
+ # @yieldreturn [Boolean] whether or not to include the argument in the generated GraphQL SDL
108
+ # @return [String] SDL string of the type
109
+ def to_sdl(&field_arg_selector)
110
+ name_section =
111
+ if implemented_interfaces.empty?
112
+ name
113
+ else
114
+ "#{name} implements #{implemented_interfaces.join(" & ")}"
115
+ end
116
+
117
+ generate_sdl(name_section: name_section, &field_arg_selector)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,47 @@
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/support/graphql_formatter"
10
+
11
+ module ElasticGraph
12
+ module SchemaDefinition
13
+ module Mixins
14
+ # A mixin designed to be included in a schema element class that supports default values.
15
+ # Designed to be `prepended` so that it can hook into `initialize`.
16
+ module SupportsDefaultValue
17
+ # @private
18
+ def initialize(...)
19
+ __skip__ = super # steep can't type this.
20
+ @default_value = NO_DEFAULT_PROVIDED
21
+ end
22
+
23
+ # Used to specify the default value for this field or argument.
24
+ #
25
+ # @param default_value [Object] default value for this field or argument
26
+ # @return [void]
27
+ def default(default_value)
28
+ @default_value = default_value
29
+ end
30
+
31
+ # Generates SDL for the default value. Suitable for inclusion in the schema elememnts `#to_sdl`.
32
+ #
33
+ # @return [String]
34
+ def default_value_sdl
35
+ return nil if @default_value == NO_DEFAULT_PROVIDED
36
+ " = #{Support::GraphQLFormatter.serialize(@default_value)}"
37
+ end
38
+
39
+ private
40
+
41
+ # A sentinel value that we can use to detect when a default has been provided.
42
+ # We can't use `nil` to detect if a default has been provided because `nil` is a valid default value!
43
+ NO_DEFAULT_PROVIDED = Module.new
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,267 @@
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 SchemaDefinition
11
+ module Mixins
12
+ # Responsible for building object types for filtering and aggregation, from an existing object type.
13
+ #
14
+ # This is specifically designed to support {SchemaElements::TypeWithSubfields} (where we have the fields directly available) and
15
+ # {SchemaElements::UnionType} (where we will need to compute the list of fields by resolving the subtypes and merging their fields).
16
+ #
17
+ # @private
18
+ module SupportsFilteringAndAggregation
19
+ # Indicates if this type supports a given feature (e.g. `filterable?`).
20
+ def supports?(&feature_predicate)
21
+ # If the type uses a custom mapping type we don't know if it can support a feature, so we assume it can't.
22
+ # TODO: clean this up using an interface instead of checking mapping options.
23
+ return false if has_custom_mapping_type?
24
+
25
+ graphql_fields_by_name.values.any?(&feature_predicate)
26
+ end
27
+
28
+ # Inverse of `supports?`.
29
+ def does_not_support?(&feature_predicate)
30
+ !supports?(&feature_predicate)
31
+ end
32
+
33
+ def derived_graphql_types
34
+ return [] if graphql_only?
35
+
36
+ indexed_agg_type = to_indexed_aggregation_type
37
+ indexed_aggregation_pagination_types =
38
+ if indexed_agg_type
39
+ schema_def_state.factory.build_relay_pagination_types(indexed_agg_type.name)
40
+ else
41
+ [] # : ::Array[SchemaElements::ObjectType]
42
+ end
43
+
44
+ sub_aggregation_types = sub_aggregation_types_for_nested_field_references.flat_map do |type|
45
+ [type] + schema_def_state.factory.build_relay_pagination_types(type.name, support_pagination: false) do |t|
46
+ # Record metadata that is necessary for elasticgraph-graphql to correctly recognize and handle
47
+ # this sub-aggregation correctly.
48
+ t.runtime_metadata_overrides = {elasticgraph_category: :nested_sub_aggregation_connection}
49
+ end
50
+ end
51
+
52
+ document_pagination_types =
53
+ if indexed?
54
+ schema_def_state.factory.build_relay_pagination_types(name, include_total_edge_count: true, derived_indexed_types: (_ = self).derived_indexed_types)
55
+ elsif schema_def_state.paginated_collection_element_types.include?(name)
56
+ schema_def_state.factory.build_relay_pagination_types(name, include_total_edge_count: true)
57
+ else
58
+ [] # : ::Array[SchemaElements::ObjectType]
59
+ end
60
+
61
+ sort_order_enum_type = schema_def_state.enums_for_indexed_types.sort_order_enum_for(self)
62
+ derived_sort_order_enum_types = [sort_order_enum_type].compact + (sort_order_enum_type&.derived_graphql_types || [])
63
+
64
+ to_input_filters +
65
+ document_pagination_types +
66
+ indexed_aggregation_pagination_types +
67
+ sub_aggregation_types +
68
+ derived_sort_order_enum_types +
69
+ build_aggregation_sub_aggregations_types + [
70
+ indexed_agg_type,
71
+ to_grouped_by_type,
72
+ to_aggregated_values_type
73
+ ].compact
74
+ end
75
+
76
+ def has_custom_mapping_type?
77
+ mapping_type = mapping_options[:type]
78
+ mapping_type && mapping_type != "object"
79
+ end
80
+
81
+ private
82
+
83
+ # Converts the type to the corresponding input filter type.
84
+ def to_input_filters
85
+ return [] if does_not_support?(&:filterable?)
86
+
87
+ schema_def_state.factory.build_standard_filter_input_types_for_index_object_type(name) do |t|
88
+ graphql_fields_by_name.values.each do |field|
89
+ if field.filterable?
90
+ t.graphql_fields_by_name[field.name] = field.to_filter_field(parent_type: t)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ # Generates the `*SubAggregation` types for all of the `mapping type: "nested"` fields that reference this type.
97
+ # A different `*SubAggregation` type needs to be generated for each nested field reference, and for each parent nesting
98
+ # context of that nested field reference. This is necessary because we will support different available `sub_aggregations`
99
+ # based on the parents of a particular nested field.
100
+ #
101
+ # For example, given a `Player` object type definition and a `Team` type definition like this:
102
+ #
103
+ # schema.object_type "Team" do |t|
104
+ # t.field "id", "ID!"
105
+ # t.field "name", "String"
106
+ # t.field "players", "[Player!]!" do |f|
107
+ # f.mapping type: "nested"
108
+ # end
109
+ # t.index "teams"
110
+ # end
111
+ #
112
+ # ...we will generate a `TeamPlayerSubAggregation` type which will have a `sub_aggregations` field which can have
113
+ # `parent_team` and `seasons` fields (assuming `Player` has a `seasons` nested field...).
114
+ def sub_aggregation_types_for_nested_field_references
115
+ schema_def_state.user_defined_field_references_by_type_name.fetch(name) { [] }.select(&:nested?).flat_map do |nested_field_ref|
116
+ schema_def_state.sub_aggregation_paths_for(nested_field_ref.parent_type).map do |path|
117
+ schema_def_state.factory.new_object_type type_ref.as_sub_aggregation(parent_doc_types: path.parent_doc_types).name do |t|
118
+ t.documentation "Return type representing a bucket of `#{name}` objects for a sub-aggregation within each `#{type_ref.as_parent_aggregation(parent_doc_types: path.parent_doc_types).name}`."
119
+
120
+ t.field schema_def_state.schema_elements.count_detail, "AggregationCountDetail", graphql_only: true do |f|
121
+ f.documentation "Details of the count of `#{name}` documents in a sub-aggregation bucket."
122
+ end
123
+
124
+ if supports?(&:groupable?)
125
+ t.field schema_def_state.schema_elements.grouped_by, type_ref.as_grouped_by.name, graphql_only: true do |f|
126
+ f.documentation "Used to specify the `#{name}` fields to group by. The returned values identify each sub-aggregation bucket."
127
+ end
128
+ end
129
+
130
+ if supports?(&:aggregatable?)
131
+ t.field schema_def_state.schema_elements.aggregated_values, type_ref.as_aggregated_values.name, graphql_only: true do |f|
132
+ f.documentation "Provides computed aggregated values over all `#{name}` documents in a sub-aggregation bucket."
133
+ end
134
+ end
135
+
136
+ if graphql_fields_by_name.values.any?(&:sub_aggregatable?)
137
+ sub_aggs_name = type_ref.as_aggregation_sub_aggregations(parent_doc_types: path.parent_doc_types + [name]).name
138
+ t.field schema_def_state.schema_elements.sub_aggregations, sub_aggs_name, graphql_only: true do |f|
139
+ f.documentation "Used to perform sub-aggregations of `#{t.name}` data."
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ # Builds the `*AggregationSubAggregations` types. For example, for an indexed type named `Team` which has nested fields,
148
+ # this would generate a `TeamAggregationSubAggregations` type. This type provides access to the various sub-aggregation
149
+ # fields.
150
+ def build_aggregation_sub_aggregations_types
151
+ # The sub-aggregation types do not generate correctly for abstract types, so for now we omit sub-aggregations for abstract types.
152
+ return [] if abstract?
153
+
154
+ sub_aggregatable_fields = graphql_fields_by_name.values.select(&:sub_aggregatable?)
155
+ return [] if sub_aggregatable_fields.empty?
156
+
157
+ schema_def_state.sub_aggregation_paths_for(self).map do |path|
158
+ agg_sub_aggs_type_ref = type_ref.as_aggregation_sub_aggregations(
159
+ parent_doc_types: path.parent_doc_types,
160
+ field_path: path.field_path
161
+ )
162
+
163
+ schema_def_state.factory.new_object_type agg_sub_aggs_type_ref.name do |t|
164
+ under_field_description = "under `#{path.field_path_string}` " unless path.field_path.empty?
165
+ t.documentation "Provides access to the `#{schema_def_state.schema_elements.sub_aggregations}` #{under_field_description}within each `#{type_ref.as_parent_aggregation(parent_doc_types: path.parent_doc_types).name}`."
166
+
167
+ sub_aggregatable_fields.each do |field|
168
+ if field.nested?
169
+ unwrapped_type = field.type_for_derived_types.fully_unwrapped
170
+ field_type_name = unwrapped_type
171
+ .as_sub_aggregation(parent_doc_types: path.parent_doc_types)
172
+ .as_connection
173
+ .name
174
+
175
+ field.define_sub_aggregations_field(parent_type: t, type: field_type_name) do |f|
176
+ f.argument schema_def_state.schema_elements.filter, unwrapped_type.as_filter_input.name do |a|
177
+ a.documentation "Used to filter the `#{unwrapped_type.name}` documents included in this sub-aggregation based on the provided criteria."
178
+ end
179
+
180
+ f.argument schema_def_state.schema_elements.first, "Int" do |a|
181
+ a.documentation "Determines how many sub-aggregation buckets should be returned."
182
+ end
183
+ end
184
+ else
185
+ field_type_name = type_ref.as_aggregation_sub_aggregations(
186
+ parent_doc_types: path.parent_doc_types,
187
+ field_path: path.field_path + [field]
188
+ ).name
189
+
190
+ field.define_sub_aggregations_field(parent_type: t, type: field_type_name)
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ def to_indexed_aggregation_type
198
+ return nil unless indexed?
199
+
200
+ schema_def_state.factory.new_object_type type_ref.as_aggregation.name do |t|
201
+ t.documentation "Return type representing a bucket of `#{name}` documents for an aggregations query."
202
+
203
+ if supports?(&:groupable?)
204
+ t.field schema_def_state.schema_elements.grouped_by, type_ref.as_grouped_by.name, graphql_only: true do |f|
205
+ f.documentation "Used to specify the `#{name}` fields to group by. The returned values identify each aggregation bucket."
206
+ end
207
+ end
208
+
209
+ t.field schema_def_state.schema_elements.count, "JsonSafeLong!", graphql_only: true do |f|
210
+ f.documentation "The count of `#{name}` documents in an aggregation bucket."
211
+ end
212
+
213
+ if supports?(&:aggregatable?)
214
+ t.field schema_def_state.schema_elements.aggregated_values, type_ref.as_aggregated_values.name, graphql_only: true do |f|
215
+ f.documentation "Provides computed aggregated values over all `#{name}` documents in an aggregation bucket."
216
+ end
217
+ end
218
+
219
+ # The sub-aggregation types do not generate correctly for abstract types, so for now we omit sub-aggregations for abstract types.
220
+ if !abstract? && supports?(&:sub_aggregatable?)
221
+ t.field schema_def_state.schema_elements.sub_aggregations, type_ref.as_aggregation_sub_aggregations.name, graphql_only: true do |f|
222
+ f.documentation "Used to perform sub-aggregations of `#{t.name}` data."
223
+ end
224
+ end
225
+
226
+ # Record metadata that is necessary for elasticgraph-graphql to correctly recognize and handle
227
+ # this indexed aggregation type correctly.
228
+ t.runtime_metadata_overrides = {source_type: name, elasticgraph_category: :indexed_aggregation}
229
+ end
230
+ end
231
+
232
+ def to_grouped_by_type
233
+ # If the type uses a custom mapping type we don't know how it can be aggregated, so we assume it needs no aggregation type.
234
+ # TODO: clean this up using an interface instead of checking mapping options.
235
+ return nil if has_custom_mapping_type?
236
+
237
+ new_non_empty_object_type type_ref.as_grouped_by.name do |t|
238
+ t.documentation "Type used to specify the `#{name}` fields to group by for aggregations."
239
+
240
+ graphql_fields_by_name.values.each do |field|
241
+ field.define_grouped_by_field(t)
242
+ end
243
+ end
244
+ end
245
+
246
+ def to_aggregated_values_type
247
+ # If the type uses a custom mapping type we don't know how it can be aggregated, so we assume it needs no aggregation type.
248
+ # TODO: clean this up using an interface instead of checking mapping options.
249
+ return nil if has_custom_mapping_type?
250
+
251
+ new_non_empty_object_type type_ref.as_aggregated_values.name do |t|
252
+ t.documentation "Type used to perform aggregation computations on `#{name}` fields."
253
+
254
+ graphql_fields_by_name.values.each do |field|
255
+ field.define_aggregated_values_field(t)
256
+ end
257
+ end
258
+ end
259
+
260
+ def new_non_empty_object_type(name, &block)
261
+ type = schema_def_state.factory.new_object_type(name, &block)
262
+ type unless type.graphql_fields_by_name.empty?
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,38 @@
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
+ module SchemaDefinition
13
+ module Mixins
14
+ # Used to verify the validity of the name of GraphQL schema elements.
15
+ #
16
+ # @note This mixin is designed to be used via `prepend`, so it can add a constructor override that enforces
17
+ # the GraphQL name pattern as the object is built.
18
+ module VerifiesGraphQLName
19
+ # @private
20
+ def initialize(...)
21
+ __skip__ = super(...) # __skip__ tells Steep to ignore this
22
+
23
+ VerifiesGraphQLName.verify_name!(name)
24
+ end
25
+
26
+ # Raises if the provided name is invalid.
27
+ #
28
+ # @param name [String] name of GraphQL schema element
29
+ # @return [void]
30
+ # @raise [InvalidGraphQLNameError] if the name is invalid
31
+ def self.verify_name!(name)
32
+ return if GRAPHQL_NAME_PATTERN.match?(name)
33
+ raise InvalidGraphQLNameError, "Not a valid GraphQL name: `#{name}`. #{GRAPHQL_NAME_VALIDITY_DESCRIPTION}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end