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