elasticgraph-schema_definition 0.18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,190 @@
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 "rake/tasklib"
10
+ require "elastic_graph/schema_artifacts/runtime_metadata/schema_element_names"
11
+
12
+ module ElasticGraph
13
+ module SchemaDefinition
14
+ # Defines rake tasks for managing artifacts generated from a schema definition.
15
+ #
16
+ # @note {ElasticGraph::Local::RakeTasks} wraps this and provides additional functionality. Most users will not need to interact with
17
+ # this class directly.
18
+ class RakeTasks < ::Rake::TaskLib
19
+ # @private
20
+ attr_reader :output
21
+
22
+ # @param index_document_sizes [Boolean] When enabled, ElasticGraph will configure the index mappings so that the datastore indexes a
23
+ # `_size` field in each document. ElasticGraph itself does not do anything with this field, but it will be available for your use
24
+ # in any direct queries (e.g. via Kibana). Important note: this requires the [mapper-size
25
+ # plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/8.15/mapper-size.html) to be installed on your datastore cluster.
26
+ # You are responsible for ensuring that is installed if you enable this feature. If you enable this and the plugin is not
27
+ # installed, you will get errors!
28
+ # @param path_to_schema [String, Pathname] path to the main (or only) schema definition file
29
+ # @param schema_artifacts_directory [String, Pathname] Directory to dump the schema artifacts in
30
+ # @param schema_element_name_form [:camelCase, :snake_case] the form of names for schema elements (fields, arguments, directives)
31
+ # generated by ElasticGraph.
32
+ # @param schema_element_name_overrides [Hash<Symbol, String>] overrides for specific names of schema elements (fields, arguments,
33
+ # directives) generated by ElasticGraph. For example, to rename the `gt` filter field to `greaterThan`, pass `{gt: "greaterThan"}`.
34
+ # @param derived_type_name_formats [Hash<Symbol, String>] overrides for the naming formats used by ElasticGraph for derived GraphQL
35
+ # type names. For example, to use `Metrics` instead of `AggregatedValues` as the suffix for the generated types supporting
36
+ # getting aggregated metrid values, pass `{AggregatedValues: "%{base}Metrics"}`. See {SchemaElements::TypeNamer::DEFAULT_FORMATS}
37
+ # for the available formats.
38
+ # @param type_name_overrides [Hash<Symbol, String>] overrides for the names of specific GraphQL types. For example, to rename the
39
+ # `DateTime` scalar to `Timestamp`, pass `{DateTime: "Timestamp}`.
40
+ # @param enum_value_overrides_by_type [Hash<Symbol, Hash<Symbol, String>>] overrides for the names of specific enum values for
41
+ # specific enum types. For example, to rename the `DayOfWeek.MONDAY` enum to `DayOfWeek.MON`, pass `{DayOfWeek: {MONDAY: "MON"}}`.
42
+ # @param extension_modules [Array<Module>] List of Ruby modules to extend onto the `SchemaDefinition::API` instance. Designed to
43
+ # support ElasticGraph extension gems (such as `elasticgraph-apollo`).
44
+ # @param enforce_json_schema_version [Boolean] Whether or not to enforce the requirement that the JSON schema version is incremented
45
+ # every time dumping the JSON schemas results in a changed artifact. Generally speaking, you will want this to be `true` for any
46
+ # ElasticGraph application that is in production as the versioning of JSON schemas is what supports safe schema evolution as it
47
+ # allows ElasticGraph to identify which version of the JSON schema the publishing system was operating on when it published an
48
+ # event. It can be useful to set it to `false` before your application is in production, as you do not want to be forced to bump
49
+ # the version after every single schema change while you are building an initial prototype.
50
+ # @param output [IO] used for printing task output
51
+ #
52
+ # @example Minimal setup with defaults
53
+ # ElasticGraph::SchemaDefinition::RakeTasks.new(
54
+ # index_document_sizes: false,
55
+ # path_to_schema: "config/schema.rb",
56
+ # schema_artifacts_directory: "config/schema/artifacts",
57
+ # schema_element_name_form: :camelCase
58
+ # )
59
+ #
60
+ # @example Spell out the full names of the `gt`/`gte`/`lt`/`lte` filter operators
61
+ # ElasticGraph::SchemaDefinition::RakeTasks.new(
62
+ # index_document_sizes: false,
63
+ # path_to_schema: "config/schema.rb",
64
+ # schema_artifacts_directory: "config/schema/artifacts",
65
+ # schema_element_name_form: :camelCase,
66
+ # schema_element_name_overrides: {
67
+ # gt: "greaterThan",
68
+ # gte: "greaterThanOrEqualTo",
69
+ # lt: "lessThan",
70
+ # lte: "lessThanOrEqualTo"
71
+ # }
72
+ # )
73
+ #
74
+ # @example Change the `AggregatedValues` type suffix to `Metrics`
75
+ # ElasticGraph::SchemaDefinition::RakeTasks.new(
76
+ # index_document_sizes: false,
77
+ # path_to_schema: "config/schema.rb",
78
+ # schema_artifacts_directory: "config/schema/artifacts",
79
+ # schema_element_name_form: :camelCase,
80
+ # derived_type_name_formats: {AggregatedValues: "Metrics"}
81
+ # )
82
+ #
83
+ # @example Rename `JsonSafeLong` to `BigInt`
84
+ # ElasticGraph::SchemaDefinition::RakeTasks.new(
85
+ # index_document_sizes: false,
86
+ # path_to_schema: "config/schema.rb",
87
+ # schema_artifacts_directory: "config/schema/artifacts",
88
+ # schema_element_name_form: :camelCase,
89
+ # type_name_overrides: {JsonSafeLong: "BigInt"}
90
+ # )
91
+ #
92
+ # @example Shorten the names of the `DayOfWeek` enum values
93
+ # ElasticGraph::SchemaDefinition::RakeTasks.new(
94
+ # index_document_sizes: false,
95
+ # path_to_schema: "config/schema.rb",
96
+ # schema_artifacts_directory: "config/schema/artifacts",
97
+ # schema_element_name_form: :camelCase,
98
+ # enum_value_overrides_by_type: {
99
+ # DayOfWeek: {
100
+ # MONDAY: "MON",
101
+ # TUESDAY: "TUE",
102
+ # WEDNESDAY: "WED",
103
+ # THURSDAY: "THU",
104
+ # FRIDAY: "FRI",
105
+ # SATURDAY: "SAT",
106
+ # SUNDAY: "SUN"
107
+ # }
108
+ # }
109
+ # )
110
+ def initialize(
111
+ index_document_sizes:,
112
+ path_to_schema:,
113
+ schema_artifacts_directory:,
114
+ schema_element_name_form:,
115
+ schema_element_name_overrides: {},
116
+ derived_type_name_formats: {},
117
+ type_name_overrides: {},
118
+ enum_value_overrides_by_type: {},
119
+ extension_modules: [],
120
+ enforce_json_schema_version: true,
121
+ output: $stdout
122
+ )
123
+ @schema_element_names = SchemaArtifacts::RuntimeMetadata::SchemaElementNames.new(
124
+ form: schema_element_name_form,
125
+ overrides: schema_element_name_overrides
126
+ )
127
+
128
+ @derived_type_name_formats = derived_type_name_formats
129
+ @type_name_overrides = type_name_overrides
130
+ @enum_value_overrides_by_type = enum_value_overrides_by_type
131
+ @index_document_sizes = index_document_sizes
132
+ @path_to_schema = path_to_schema
133
+ @schema_artifacts_directory = schema_artifacts_directory
134
+ @enforce_json_schema_version = enforce_json_schema_version
135
+ @extension_modules = extension_modules
136
+ @output = output
137
+
138
+ define_tasks
139
+ end
140
+
141
+ private
142
+
143
+ def define_tasks
144
+ namespace :schema_artifacts do
145
+ desc "Dumps all schema artifacts based on the current ElasticGraph schema definition"
146
+ task :dump do
147
+ schema_artifact_manager.dump_artifacts
148
+ end
149
+
150
+ desc "Checks the artifacts to make sure they are up-to-date, raising an exception if not"
151
+ task :check do
152
+ schema_artifact_manager.check_artifacts
153
+ end
154
+ end
155
+ end
156
+
157
+ def schema_artifact_manager
158
+ require "elastic_graph/schema_definition/schema_artifact_manager"
159
+
160
+ # :nocov: -- tests don't cover the `VERBOSE` side
161
+ max_diff_lines = ENV["VERBOSE"] ? 999999999 : 50
162
+ # :nocov:
163
+
164
+ SchemaArtifactManager.new(
165
+ schema_definition_results: schema_definition_results,
166
+ schema_artifacts_directory: @schema_artifacts_directory.to_s,
167
+ enforce_json_schema_version: @enforce_json_schema_version,
168
+ output: @output,
169
+ max_diff_lines: max_diff_lines
170
+ )
171
+ end
172
+
173
+ def schema_definition_results
174
+ require "elastic_graph/schema_definition/api"
175
+
176
+ API.new(
177
+ @schema_element_names,
178
+ @index_document_sizes,
179
+ extension_modules: @extension_modules,
180
+ derived_type_name_formats: @derived_type_name_formats,
181
+ type_name_overrides: @type_name_overrides,
182
+ enum_value_overrides_by_type: @enum_value_overrides_by_type,
183
+ output: @output
184
+ ).tap do |api|
185
+ api.as_active_instance { load @path_to_schema.to_s }
186
+ end.results
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,404 @@
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
+ require "elastic_graph/schema_artifacts/runtime_metadata/schema"
12
+ require "elastic_graph/schema_artifacts/artifacts_helper_methods"
13
+ require "elastic_graph/schema_definition/indexing/event_envelope"
14
+ require "elastic_graph/schema_definition/indexing/json_schema_with_metadata"
15
+ require "elastic_graph/schema_definition/indexing/relationship_resolver"
16
+ require "elastic_graph/schema_definition/indexing/update_target_resolver"
17
+ require "elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect"
18
+ require "elastic_graph/schema_definition/schema_elements/field_path"
19
+ require "elastic_graph/schema_definition/scripting/file_system_repository"
20
+ require "elastic_graph/support/memoizable_data"
21
+
22
+ module ElasticGraph
23
+ module SchemaDefinition
24
+ # Provides the results of defining a schema.
25
+ #
26
+ # @note This class is designed to implement the same interface as `ElasticGraph::SchemaArtifacts::FromDisk`, so that it can be used
27
+ # interchangeably with schema artifacts loaded from disk. This allows the artifacts to be used in tests without having to dump them or
28
+ # reload them.
29
+ class Results < Support::MemoizableData.define(:state)
30
+ include Mixins::HasReadableToSAndInspect.new
31
+ include SchemaArtifacts::ArtifactsHelperMethods
32
+
33
+ # @return [String] the generated GraphQL SDL schema string dumped as `schema.graphql`
34
+ def graphql_schema_string
35
+ @graphql_schema_string ||= generate_sdl
36
+ end
37
+
38
+ # @return [Hash<String, Object>] the Elasticsearch/OpenSearch configuration dumped as `datastore_config.yaml`
39
+ def datastore_config
40
+ @datastore_config ||= generate_datastore_config
41
+ end
42
+
43
+ # @return [Hash<String, Object>] runtime metadata used by other parts of ElasticGraph and dumped as `runtime_metadata.yaml`
44
+ def runtime_metadata
45
+ @runtime_metadata ||= build_runtime_metadata
46
+ end
47
+
48
+ # @param version [Integer] desired JSON schema version
49
+ # @return [Hash<String, Object>] the JSON schema for the requested version, if available
50
+ # @raise [NotFoundError] if the requested JSON schema version is not available
51
+ def json_schemas_for(version)
52
+ unless available_json_schema_versions.include?(version)
53
+ raise NotFoundError, "The requested json schema version (#{version}) is not available. Available versions: #{available_json_schema_versions.to_a.join(", ")}."
54
+ end
55
+
56
+ @latest_versioned_json_schema ||= merge_field_metadata_into_json_schema(current_public_json_schema).json_schema
57
+ end
58
+
59
+ # @return [Set<Integer>] set of available JSON schema versions
60
+ def available_json_schema_versions
61
+ @available_json_schema_versions ||= Set[latest_json_schema_version]
62
+ end
63
+
64
+ # @return [Hash<String, Object>] the newly generated JSON schema
65
+ def latest_json_schema_version
66
+ current_public_json_schema[JSON_SCHEMA_VERSION_KEY]
67
+ end
68
+
69
+ # @private
70
+ def json_schema_version_setter_location
71
+ state.json_schema_version_setter_location
72
+ end
73
+
74
+ # @private
75
+ def json_schema_field_metadata_by_type_and_field_name
76
+ @json_schema_field_metadata_by_type_and_field_name ||= json_schema_indexing_field_types_by_name
77
+ .transform_values(&:json_schema_field_metadata_by_field_name)
78
+ end
79
+
80
+ # @private
81
+ def current_public_json_schema
82
+ @current_public_json_schema ||= build_public_json_schema
83
+ end
84
+
85
+ # @private
86
+ def merge_field_metadata_into_json_schema(json_schema)
87
+ json_schema_with_metadata_merger.merge_metadata_into(json_schema)
88
+ end
89
+
90
+ # @private
91
+ def unused_deprecated_elements
92
+ json_schema_with_metadata_merger.unused_deprecated_elements
93
+ end
94
+
95
+ # @private
96
+ STATIC_SCRIPT_REPO = Scripting::FileSystemRepository.new(::File.join(__dir__.to_s, "scripting", "scripts"))
97
+
98
+ # @private
99
+ def derived_indexing_type_names
100
+ @derived_indexing_type_names ||= state
101
+ .object_types_by_name
102
+ .values
103
+ .flat_map { |type| type.derived_indexed_types.map { |dit| dit.destination_type_ref.name } }
104
+ .to_set
105
+ end
106
+
107
+ private
108
+
109
+ def after_initialize
110
+ # Record that we are now generating results so that caching can kick in.
111
+ state.user_definition_complete = true
112
+ end
113
+
114
+ def json_schema_with_metadata_merger
115
+ @json_schema_with_metadata_merger ||= Indexing::JSONSchemaWithMetadata::Merger.new(self)
116
+ end
117
+
118
+ def generate_datastore_config
119
+ # We need to check this before generating our datastore configuration.
120
+ # We can't generate a mapping from a recursively defined schema type.
121
+ check_for_circular_dependencies!
122
+
123
+ index_templates, indices = state.object_types_by_name.values
124
+ .flat_map(&:indices)
125
+ .sort_by(&:name)
126
+ .partition(&:rollover_config)
127
+
128
+ datastore_scripts = (build_dynamic_scripts + STATIC_SCRIPT_REPO.scripts)
129
+
130
+ {
131
+ "index_templates" => index_templates.to_h { |i| [i.name, i.to_index_template_config] },
132
+ "indices" => indices.to_h { |i| [i.name, i.to_index_config] },
133
+ "scripts" => datastore_scripts.to_h { |s| [s.id, s.to_artifact_payload] }
134
+ }
135
+ end
136
+
137
+ def build_dynamic_scripts
138
+ state.object_types_by_name.values
139
+ .flat_map(&:derived_indexed_types)
140
+ .map(&:painless_script)
141
+ end
142
+
143
+ def build_runtime_metadata
144
+ extra_update_targets_by_object_type_name = identify_extra_update_targets_by_object_type_name
145
+
146
+ object_types_by_name = all_types_except_root_query_type
147
+ .select { |t| t.respond_to?(:graphql_fields_by_name) }
148
+ .to_h { |type| [type.name, (_ = type).runtime_metadata(extra_update_targets_by_object_type_name.fetch(type.name) { [] })] }
149
+
150
+ scalar_types_by_name = state.scalar_types_by_name.transform_values(&:runtime_metadata)
151
+
152
+ enum_generator = state.factory.new_enums_for_indexed_types
153
+
154
+ indexed_enum_types_by_name = state.object_types_by_name.values
155
+ .select(&:indexed?)
156
+ .filter_map { |type| enum_generator.sort_order_enum_for(_ = type) }
157
+ .to_h { |enum_type| [(_ = enum_type).name, (_ = enum_type).runtime_metadata] }
158
+
159
+ enum_types_by_name = all_types_except_root_query_type
160
+ .grep(SchemaElements::EnumType) # : ::Array[SchemaElements::EnumType]
161
+ .to_h { |t| [t.name, t.runtime_metadata] }
162
+ .merge(indexed_enum_types_by_name)
163
+
164
+ index_definitions_by_name = state.object_types_by_name.values.flat_map(&:indices).to_h do |index|
165
+ [index.name, index.runtime_metadata]
166
+ end
167
+
168
+ SchemaArtifacts::RuntimeMetadata::Schema.new(
169
+ object_types_by_name: object_types_by_name,
170
+ scalar_types_by_name: scalar_types_by_name,
171
+ enum_types_by_name: enum_types_by_name,
172
+ index_definitions_by_name: index_definitions_by_name,
173
+ schema_element_names: state.schema_elements,
174
+ graphql_extension_modules: state.graphql_extension_modules,
175
+ static_script_ids_by_scoped_name: STATIC_SCRIPT_REPO.script_ids_by_scoped_name
176
+ )
177
+ end
178
+
179
+ # Builds a map, keyed by object type name, of extra `update_targets` that have been generated
180
+ # from any fields that use `sourced_from` on other types.
181
+ def identify_extra_update_targets_by_object_type_name
182
+ # The field_path_resolver memoizes some calculations, and we want the same instance to be
183
+ # used by all UpdateTargetBuilders to maximize its effectiveness.
184
+ field_path_resolver = SchemaElements::FieldPath::Resolver.new
185
+ sourced_field_errors = [] # : ::Array[::String]
186
+ relationship_errors = [] # : ::Array[::String]
187
+
188
+ state.object_types_by_name.values.each_with_object(::Hash.new { |h, k| h[k] = [] }) do |object_type, accum|
189
+ fields_with_sources_by_relationship_name =
190
+ if object_type.indices.empty?
191
+ # only indexed types can have `sourced_from` fields, and resolving `fields_with_sources` on an unindexed union type
192
+ # such as `_Entity` when we are using apollo can lead to exceptions when multiple entity types have the same field name
193
+ # that use different mapping types.
194
+ {} # : ::Hash[::String, ::Array[SchemaElements::Field]]
195
+ else
196
+ object_type
197
+ .fields_with_sources
198
+ .group_by { |f| (_ = f.source).relationship_name }
199
+ end
200
+
201
+ defined_relationships = object_type
202
+ .graphql_fields_by_name.values
203
+ .select(&:relationship)
204
+ .map(&:name)
205
+
206
+ (defined_relationships | fields_with_sources_by_relationship_name.keys).each do |relationship_name|
207
+ sourced_fields = fields_with_sources_by_relationship_name.fetch(relationship_name) { [] }
208
+ relationship_resolver = Indexing::RelationshipResolver.new(
209
+ schema_def_state: state,
210
+ object_type: object_type,
211
+ relationship_name: relationship_name,
212
+ sourced_fields: sourced_fields,
213
+ field_path_resolver: field_path_resolver
214
+ )
215
+
216
+ resolved_relationship, relationship_error = relationship_resolver.resolve
217
+ relationship_errors << relationship_error if relationship_error
218
+
219
+ if object_type.indices.any? && resolved_relationship && sourced_fields.any?
220
+ update_target_resolver = Indexing::UpdateTargetResolver.new(
221
+ object_type: object_type,
222
+ resolved_relationship: resolved_relationship,
223
+ sourced_fields: sourced_fields,
224
+ field_path_resolver: field_path_resolver
225
+ )
226
+
227
+ update_target, errors = update_target_resolver.resolve
228
+ accum[resolved_relationship.related_type.name] << update_target if update_target
229
+ sourced_field_errors.concat(errors)
230
+ end
231
+ end
232
+ end.tap do
233
+ full_errors = [] # : ::Array[::String]
234
+
235
+ if sourced_field_errors.any?
236
+ full_errors << "Schema had #{sourced_field_errors.size} error(s) related to `sourced_from` fields:\n\n#{sourced_field_errors.map.with_index(1) { |e, i| "#{i}. #{e}" }.join("\n\n")}"
237
+ end
238
+
239
+ if relationship_errors.any?
240
+ full_errors << "Schema had #{relationship_errors.size} error(s) related to relationship fields:\n\n#{relationship_errors.map.with_index(1) { |e, i| "#{i}. #{e}" }.join("\n\n")}"
241
+ end
242
+
243
+ unless full_errors.empty?
244
+ raise SchemaError, full_errors.join("\n\n")
245
+ end
246
+ end
247
+ end
248
+
249
+ # Generates the SDL defined by your schema. Intended to be called only once
250
+ # at the very end (after evaluating the "main" template). `Evaluator` calls this
251
+ # automatically at the end.
252
+ def generate_sdl
253
+ check_for_circular_dependencies!
254
+ state.object_types_by_name.values.each(&:verify_graphql_correctness!)
255
+
256
+ type_defs = state.factory
257
+ .new_graphql_sdl_enumerator(all_types_except_root_query_type)
258
+ .map { |sdl| strip_trailing_whitespace(sdl) }
259
+
260
+ [type_defs + state.sdl_parts].join("\n\n")
261
+ end
262
+
263
+ def build_public_json_schema
264
+ json_schema_version = state.json_schema_version
265
+ if json_schema_version.nil?
266
+ raise SchemaError, "`json_schema_version` must be specified in the schema. To resolve, add `schema.json_schema_version 1` in a schema definition block."
267
+ end
268
+
269
+ indexed_type_names = state.object_types_by_name.values
270
+ .select { |type| type.indexed? && !type.abstract? }
271
+ .reject { |type| derived_indexing_type_names.include?(type.name) }
272
+ .map(&:name)
273
+
274
+ definitions_by_name = json_schema_indexing_field_types_by_name
275
+ .transform_values(&:to_json_schema)
276
+ .compact
277
+
278
+ {
279
+ "$schema" => JSON_META_SCHEMA,
280
+ JSON_SCHEMA_VERSION_KEY => json_schema_version,
281
+ "$defs" => {
282
+ "ElasticGraphEventEnvelope" => Indexing::EventEnvelope.json_schema(indexed_type_names, json_schema_version)
283
+ }.merge(definitions_by_name)
284
+ }
285
+ end
286
+
287
+ def json_schema_indexing_field_types_by_name
288
+ @json_schema_indexing_field_types_by_name ||= state
289
+ .types_by_name.values
290
+ .reject do |t|
291
+ derived_indexing_type_names.include?(t.name) ||
292
+ # Skip graphql framework types
293
+ t.graphql_only?
294
+ end
295
+ .sort_by(&:name)
296
+ .to_h { |type| [type.name, type.to_indexing_field_type] }
297
+ end
298
+
299
+ def strip_trailing_whitespace(string)
300
+ string.gsub(/ +$/, "")
301
+ end
302
+
303
+ def check_for_circular_dependencies!
304
+ return if @no_circular_dependencies
305
+
306
+ referenced_types_by_source_type = state.types_by_name
307
+ .reject { |_, type| type.graphql_only? }
308
+ .each_with_object(::Hash.new { |h, k| h[k] = ::Set.new }) do |(type_name, _), cache|
309
+ recursively_add_referenced_types_to(state.type_ref(type_name), cache)
310
+ end
311
+
312
+ circular_reference_sets = referenced_types_by_source_type
313
+ .select { |source_type, referenced_types| referenced_types.include?(source_type) }
314
+ .values
315
+ .uniq
316
+
317
+ if circular_reference_sets.any?
318
+ descriptions = circular_reference_sets.map do |set|
319
+ "- The set of #{set.to_a} forms a circular reference chain."
320
+ end
321
+
322
+ raise SchemaError, "Your schema has self-referential types, which are not allowed, since " \
323
+ "it prevents the datastore mapping and GraphQL schema generation from terminating:\n" \
324
+ "#{descriptions.join("\n")}"
325
+ end
326
+
327
+ @no_circular_dependencies = true
328
+ end
329
+
330
+ def recursively_add_referenced_types_to(source_type_ref, references_cache)
331
+ return unless (source_type = source_type_ref.as_object_type)
332
+ references_set = references_cache[source_type_ref.name]
333
+
334
+ # Recursive references are allowed only when its a relation, so skip that case.
335
+ source_type.graphql_fields_by_name.values.reject { |f| f.relationship }.each do |field|
336
+ field_type = field.type.fully_unwrapped
337
+
338
+ if field_type.object? && references_set.add?(field_type.name)
339
+ recursively_add_referenced_types_to(field_type, references_cache)
340
+ end
341
+
342
+ references_set.merge(references_cache[field_type.name])
343
+ end
344
+ end
345
+
346
+ def all_types_except_root_query_type
347
+ @all_types_except_root_query_type ||= state.types_by_name.values.flat_map do |registered_type|
348
+ related_types = [registered_type] + registered_type.derived_graphql_types
349
+ apply_customizations_to(related_types, registered_type)
350
+ related_types
351
+ end
352
+ end
353
+
354
+ def apply_customizations_to(types, registered_type)
355
+ built_in_customizers = state.built_in_types_customization_blocks
356
+ if built_in_customizers.any? && state.initially_registered_built_in_types.include?(registered_type.name)
357
+ types.each do |type|
358
+ built_in_customizers.each do |customization_block|
359
+ customization_block.call(type)
360
+ end
361
+ end
362
+ end
363
+
364
+ unless (unknown_type_names = registered_type.derived_type_customizations_by_name.keys - types.map(&:name)).empty?
365
+ raise SchemaError,
366
+ "`customize_derived_types` was called on `#{registered_type.name}` with some unrecognized type names " \
367
+ "(#{unknown_type_names.join(", ")}). Maybe some of the derived GraphQL types are misspelled?"
368
+ end
369
+
370
+ unless (unknown_type_names = registered_type.derived_field_customizations_by_type_and_field_name.keys - types.map(&:name)).empty?
371
+ raise SchemaError,
372
+ "`customize_derived_type_fields` was called on `#{registered_type.name}` with some unrecognized type names " \
373
+ "(#{unknown_type_names.join(", ")}). Maybe some of the derived GraphQL types are misspelled?"
374
+ end
375
+
376
+ unknown_field_names = (types - [registered_type]).flat_map do |type|
377
+ registered_type.derived_type_customizations_for_type(type).each { |b| b.call(type) }
378
+ field_customizations_by_name = registered_type.derived_field_customizations_by_name_for_type(type)
379
+
380
+ if field_customizations_by_name.any? && !type.respond_to?(:graphql_fields_by_name)
381
+ raise SchemaError,
382
+ "`customize_derived_type_fields` was called on `#{registered_type.name}` with a type that can " \
383
+ "never have fields: `#{type.name}`."
384
+ end
385
+
386
+ field_customizations_by_name.filter_map do |field_name, customization_blocks|
387
+ if (field = (_ = type).graphql_fields_by_name[field_name])
388
+ customization_blocks.each { |b| b.call(field) }
389
+ nil
390
+ else
391
+ "#{type.name}.#{field_name}"
392
+ end
393
+ end
394
+ end
395
+
396
+ unless unknown_field_names.empty?
397
+ raise SchemaError,
398
+ "`customize_derived_type_fields` was called on `#{registered_type.name}` with some unrecognized field names " \
399
+ "(#{unknown_field_names.join(", ")}). Maybe one of the field names was misspelled?"
400
+ end
401
+ end
402
+ end
403
+ end
404
+ end