diff --git a/elasticgraph-json_ingestion/spec/integration/elastic_graph/json_ingestion/schema_definition/schema_artifact_manager_extension_spec.rb b/elasticgraph-json_ingestion/spec/integration/elastic_graph/json_ingestion/schema_definition/schema_artifact_manager_extension_spec.rb new file mode 100644 index 000000000..7b34bc2ce --- /dev/null +++ b/elasticgraph-json_ingestion/spec/integration/elastic_graph/json_ingestion/schema_definition/schema_artifact_manager_extension_spec.rb @@ -0,0 +1,342 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +require "elastic_graph/constants" +require "elastic_graph/json_ingestion/schema_definition/api_extension" +require "elastic_graph/schema_definition/rake_tasks" +require "fileutils" +require "yaml" + +module ElasticGraph + module JSONIngestion + module SchemaDefinition + RSpec.describe SchemaArtifactManagerExtension, :in_temp_dir, :rake_task do + after do + Thread.current[:eg_schema_load_count] = nil + end + + it "dumps public JSON schemas and private versioned JSON schemas with ElasticGraph metadata" do + write_schema(json_schema_version: 1) + output = run_rake("schema_artifacts:dump") + + expect(output.lines).to include( + a_string_including("Dumped", JSON_SCHEMAS_FILE), + a_string_including("Dumped", versioned_json_schema_file(1)) + ) + + public_id_schema = read_yaml_artifact(JSON_SCHEMAS_FILE).dig("$defs", "Widget", "properties", "id") + versioned_id_schema = read_yaml_artifact(versioned_json_schema_file(1)).dig("$defs", "Widget", "properties", "id") + + expect(public_id_schema).to eq(json_schema_for_keyword_type("ID")) + expect(versioned_id_schema).to eq(json_schema_for_keyword_type("ID", { + "ElasticGraph" => { + "type" => "ID!", + "nameInIndex" => "id" + } + })) + + expect(run_rake("schema_artifacts:dump")).to include("is already up to date", JSON_SCHEMAS_FILE) + end + + it "requires JSON schema version bumps unless enforcement is disabled" do + write_schema(json_schema_version: 1) + run_rake("schema_artifacts:dump") + + write_schema(json_schema_version: 2) + expect { + run_rake("schema_artifacts:dump") + }.to change { read_artifact(JSON_SCHEMAS_FILE) } + .from(a_string_including("\njson_schema_version: 1\n")) + .to(a_string_including("\njson_schema_version: 2\n")) + + write_schema(json_schema_version: 2, extra_widget_body: "t.field 'color', 'String!'") + expect { + run_rake("schema_artifacts:dump") + }.to abort_with a_string_including( + "A change has been attempted to `json_schemas.yaml`", + "`schema.json_schema_version 3`" + ).and matching(/line \d+ at `(\S*\/?)schema\.rb`/) + + write_schema( + json_schema_version: 2, + extra_widget_body: "t.field 'color', 'String!'", + enforce_json_schema_version: false + ) + + expect(run_rake("schema_artifacts:dump")).to include( + "WARNING: the `json_schemas.yaml` artifact is being updated without the `json_schema_version` being correspondingly incremented" + ) + end + + it "keeps field metadata up to date on every versioned JSON schema" do + write_schema(json_schema_version: 1) + run_rake("schema_artifacts:dump") + + write_schema(json_schema_version: 2, extra_widget_body: "t.field 'color', 'String!'") + run_rake("schema_artifacts:dump") + + write_schema( + json_schema_version: 2, + name_field_suffix: ", name_in_index: 'name2'", + extra_widget_body: "t.field 'color', 'String!'" + ) + run_rake("schema_artifacts:dump") + + loaded_v1 = read_yaml_artifact(versioned_json_schema_file(1)) + loaded_v2 = read_yaml_artifact(versioned_json_schema_file(2)) + + expect(loaded_v1.dig("$defs", "Widget", "properties", "name")).to eq( + json_schema_for_keyword_type("String", { + "ElasticGraph" => { + "type" => "String!", + "nameInIndex" => "name2" + } + }) + ) + expect(loaded_v1.dig("$defs", "Widget", "properties", "color")).to eq(nil) + + expect(loaded_v2.dig("$defs", "Widget", "properties", "name")).to eq( + json_schema_for_keyword_type("String", { + "ElasticGraph" => { + "type" => "String!", + "nameInIndex" => "name2" + } + }) + ) + expect(loaded_v2.dig("$defs", "Widget", "properties", "color")).to eq( + json_schema_for_keyword_type("String", { + "ElasticGraph" => { + "type" => "String!", + "nameInIndex" => "color" + } + }) + ) + end + + it "gives clear errors for old schema versions with missing fields or types" do + write_schema(json_schema_version: 8) + run_rake("schema_artifacts:dump") + write_schema(json_schema_version: 9, omit_widget_name_field: true) + expect { run_rake("schema_artifacts:dump") }.to abort_with a_string_including( + "The `Widget.name` field (which existed in JSON schema version 8) no longer exists", + "at this old version", + "delete its file from `json_schemas_by_version`" + ) + + write_schema(json_schema_version: 9) + run_rake("schema_artifacts:dump") + write_schema(json_schema_version: 10, omit_widget_name_field: true) + expect { run_rake("schema_artifacts:dump") }.to abort_with a_string_including( + "The `Widget.name` field (which existed in JSON schema versions 8 and 9) no longer exists", + "at these old versions", + "delete their files from `json_schemas_by_version`" + ) + + write_schema(json_schema_version: 10) + run_rake("schema_artifacts:dump") + write_schema(json_schema_version: 11, omit_widget_name_field: true) + expect { run_rake("schema_artifacts:dump") }.to abort_with a_string_including( + "The `Widget.name` field (which existed in JSON schema versions 8, 9, and 10) no longer exists" + ) + + write_schema(json_schema_version: 11, omit_widget_name_field: true, extra_widget_body: "t.field('full_name', 'String') { |f| f.renamed_from 'name' }") + run_rake("schema_artifacts:dump") + + delete_artifact(JSON_SCHEMAS_FILE) + write_schema(json_schema_version: 11, omit_widget_name_field: true, extra_widget_body: "t.deleted_field 'name'") + run_rake("schema_artifacts:dump") + + delete_artifacts + write_schema(json_schema_version: 1) + run_rake("schema_artifacts:dump") + write_schema(json_schema_version: 2, widget_type_name: "Widget2") + expect { run_rake("schema_artifacts:dump") }.to abort_with a_string_including( + "The `Widget` type (which existed in JSON schema version 1) no longer exists", + "If the `Widget` type has been renamed" + ) + end + + it "reports deprecated schema element warnings, conflicts, and missing necessary fields" do + ::File.write("schema.rb", <<~EOS) + ElasticGraph.define_schema do |schema| + schema.json_schema_version 1 + schema.deleted_type "SomeType" + + schema.object_type "Widget" do |t| + t.renamed_from "OldWidget" + t.deleted_field "old_name" + t.field "id", "ID!" + t.field "name", "String" do |f| + f.renamed_from "old_name" + end + t.index "widgets" + end + end + EOS + + expect(run_rake("schema_artifacts:dump")).to include( + "The schema definition has 4 unneeded reference(s)", + "`schema.deleted_type \"SomeType\"`", + "`type.renamed_from \"OldWidget\"`", + "`type.deleted_field \"old_name\"`", + "`field.renamed_from \"old_name\"`" + ) + + delete_artifacts + ::File.write("schema.rb", <<~EOS) + ElasticGraph.define_schema do |schema| + schema.json_schema_version 1 + schema.deleted_type "Widget" + + schema.object_type "Widget" do |t| + t.field "id", "ID!" + t.index "widgets" + + t.field "token", "ID" do |f| + f.renamed_from "id" + end + t.deleted_field "id" + end + end + EOS + + expect { + run_rake("schema_artifacts:dump") + }.to abort_with a_string_including( + "The schema definition of `Widget` has conflicts", + "The schema definition of `Widget.id` has conflicts" + ) + + delete_artifacts + ::File.write("schema.rb", <<~EOS) + ElasticGraph.define_schema do |schema| + schema.json_schema_version 1 + + schema.object_type "Embedded" do |t| + t.field "workspace_id", "ID" + t.field "created_at", "DateTime" + end + + schema.object_type "Widget" do |t| + t.field "id", "ID" + t.field "embedded", "Embedded" + t.index "widgets" do |i| + i.route_with "embedded.workspace_id" + i.rollover :yearly, "embedded.created_at" + end + end + end + EOS + + run_rake("schema_artifacts:dump") + + ::File.write("schema.rb", <<~EOS) + ElasticGraph.define_schema do |schema| + schema.json_schema_version 2 + + schema.object_type "Embedded" do |t| + t.field "workspace_id2", "ID", name_in_index: "workspace_id" + t.deleted_field "workspace_id" + + t.field "created_at2", "DateTime", name_in_index: "created_at" + t.deleted_field "created_at" + end + + schema.object_type "Widget" do |t| + t.field "id", "ID" + t.field "embedded", "Embedded" + t.index "widgets" do |i| + i.route_with "embedded.workspace_id2" + i.rollover :yearly, "embedded.created_at2" + end + end + end + EOS + + expect { + run_rake("schema_artifacts:dump") + }.to abort_with a_string_including( + "JSON schema version 1 has no field that maps to the routing field path of `Widget.embedded.workspace_id`", + "JSON schema version 1 has no field that maps to the rollover field path of `Widget.embedded.created_at`" + ) + end + + def write_schema( + json_schema_version:, + enforce_json_schema_version: true, + widget_type_name: "Widget", + name_field_suffix: "", + extra_widget_body: "", + omit_widget_name_field: false + ) + ::File.write("schema.rb", <<~EOS) + Thread.current[:eg_schema_load_count] = (Thread.current[:eg_schema_load_count] || 0) + 1 + raise "Schema file was loaded more than once!" if Thread.current[:eg_schema_load_count] > 1 + + ElasticGraph.define_schema do |schema| + schema.json_schema_version #{json_schema_version} + #{"schema.enforce_json_schema_version false" unless enforce_json_schema_version} + + schema.object_type "#{widget_type_name}" do |t| + t.field "id", "ID!" + #{%(t.field "name", "String!"#{name_field_suffix}) unless omit_widget_name_field} + #{extra_widget_body} + t.index "widgets" + end + end + EOS + end + + def run_rake(*args) + Thread.current[:eg_schema_load_count] = nil + + super(*args) do |output| + ::ElasticGraph::SchemaDefinition::RakeTasks.new( + schema_element_name_form: :snake_case, + index_document_sizes: true, + path_to_schema: "schema.rb", + schema_artifacts_directory: "config/schema/artifacts", + extension_modules: [APIExtension], + output: output + ) + end + end + + def read_artifact(*name_parts) + path = ::File.join("config", "schema", "artifacts", *name_parts) + ::File.exist?(path) && ::File.read(path) + end + + def read_yaml_artifact(*name_parts) + ::YAML.safe_load(read_artifact(*name_parts)) + end + + def delete_artifact(*name_parts) + ::File.delete(::File.join("config", "schema", "artifacts", *name_parts)) + end + + def delete_artifacts + ::FileUtils.rm_rf(::File.join("config", "schema", "artifacts")) + end + + def versioned_json_schema_file(version) + ::File.join(JSON_SCHEMAS_BY_VERSION_DIRECTORY, "v#{version}.yaml") + end + + def json_schema_for_keyword_type(type, extras = {}) + { + "allOf" => [ + {"$ref" => "#/$defs/#{type}"}, + {"maxLength" => DEFAULT_MAX_KEYWORD_LENGTH} + ] + }.merge(extras) + end + end + end + end +end diff --git a/elasticgraph-json_ingestion/spec/spec_helper.rb b/elasticgraph-json_ingestion/spec/spec_helper.rb new file mode 100644 index 000000000..138d7e9ca --- /dev/null +++ b/elasticgraph-json_ingestion/spec/spec_helper.rb @@ -0,0 +1,10 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +# This file contains RSpec configuration for `elasticgraph-json_ingestion`. +# It is loaded by the shared spec helper at `spec_support/spec_helper.rb`. diff --git a/elasticgraph-schema_definition/spec/support/json_schema_matcher.rb b/elasticgraph-json_ingestion/spec/support/json_schema_matcher.rb similarity index 100% rename from elasticgraph-schema_definition/spec/support/json_schema_matcher.rb rename to elasticgraph-json_ingestion/spec/support/json_schema_matcher.rb diff --git a/elasticgraph-schema_definition/spec/support/json_schema_matcher_spec.rb b/elasticgraph-json_ingestion/spec/support/json_schema_matcher_spec.rb similarity index 100% rename from elasticgraph-schema_definition/spec/support/json_schema_matcher_spec.rb rename to elasticgraph-json_ingestion/spec/support/json_schema_matcher_spec.rb diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/indexing/json_schema_with_metadata_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/json_schema_with_metadata_spec.rb similarity index 99% rename from elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/indexing/json_schema_with_metadata_spec.rb rename to elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/json_schema_with_metadata_spec.rb index 2e87e57ce..021d71793 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/indexing/json_schema_with_metadata_spec.rb +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/json_schema_with_metadata_spec.rb @@ -10,9 +10,9 @@ require "elastic_graph/spec_support/schema_definition_helpers" module ElasticGraph - module SchemaDefinition + module JSONIngestion::SchemaDefinition module Indexing - ::RSpec.describe JSONIngestion::SchemaDefinition::Indexing::JSONSchemaWithMetadata do + ::RSpec.describe JSONSchemaWithMetadata do include_context "SchemaDefinitionHelpers" it "ignores derived indexed types that do not show up in the JSON schema" do @@ -1056,7 +1056,7 @@ def metadata_for(json_schema, type, field) def define_schema(&schema_definition) super( schema_element_name_form: "snake_case", - extension_modules: [JSONIngestion::SchemaDefinition::APIExtension], + extension_modules: [APIExtension], &schema_definition ) end diff --git a/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/wrappers_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/wrappers_spec.rb new file mode 100644 index 000000000..4dd5414ec --- /dev/null +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/indexing/wrappers_spec.rb @@ -0,0 +1,134 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +require "elastic_graph/json_ingestion/schema_definition/indexing/field" +require "elastic_graph/json_ingestion/schema_definition/indexing/field_reference" +require "elastic_graph/json_ingestion/schema_definition/indexing/field_type/object" +require "elastic_graph/spec_support/schema_definition_helpers" + +module ElasticGraph + module JSONIngestion + module SchemaDefinition + module Indexing + RSpec.describe "JSON schema indexing wrappers" do + include_context "SchemaDefinitionHelpers" + + it "compares field references using both the wrapped reference and JSON schema options" do + reference = widget_schema_field("name").to_indexing_field_reference + matching_reference = FieldReference.new( + reference.__getobj__, + json_schema_layers: reference.json_schema_layers, + json_schema_customizations: reference.json_schema_customizations + ) + different_reference = FieldReference.new( + reference.__getobj__, + json_schema_layers: reference.json_schema_layers, + json_schema_customizations: {maxLength: 10} + ) + + expect(reference).to eq(matching_reference) + expect(reference.eql?(matching_reference)).to eq(true) + expect(reference.hash).to eq(matching_reference.hash) + expect(reference).not_to eq(different_reference) + expect(reference == reference.__getobj__).to eq(true) + end + + it "returns nil when resolving a field reference whose type is unresolved" do + reference = FieldReference.new( + ::ElasticGraph::SchemaDefinition::Indexing::FieldReference.new( + name: "missing_type", + name_in_index: "missing_type", + type: unresolved_type_ref, + mapping_options: {}, + accuracy_confidence: nil, + source: nil, + runtime_field_script: nil, + doc_comment: nil + ), + json_schema_layers: [], + json_schema_customizations: {} + ) + + expect(reference.resolve).to eq(nil) + end + + it "compares fields using both the wrapped field and JSON schema options" do + field = widget_indexing_field("name") + matching_field = Field.new( + field.__getobj__, + json_schema_layers: field.json_schema_layers, + json_schema_customizations: field.json_schema_customizations + ) + different_field = Field.new( + field.__getobj__, + json_schema_layers: field.json_schema_layers, + json_schema_customizations: {maxLength: 10} + ) + + expect(field).to eq(matching_field) + expect(field.eql?(matching_field)).to eq(true) + expect(field.hash).to eq(matching_field.hash) + expect(field).not_to eq(different_field) + expect(field == field.__getobj__).to eq(true) + end + + it "compares object field types using both the wrapped field type and JSON schema options" do + object_field_type = widget_field_type + matching_object_field_type = FieldType::Object.new(object_field_type.__getobj__).tap do |field_type| + field_type.json_schema_options = object_field_type.json_schema_options + end + different_object_field_type = FieldType::Object.new(object_field_type.__getobj__).tap do |field_type| + field_type.json_schema_options = {type: "object"} + end + + expect(object_field_type).to eq(matching_object_field_type) + expect(object_field_type.eql?(matching_object_field_type)).to eq(true) + expect(object_field_type.hash).to eq(matching_object_field_type.hash) + expect(object_field_type).not_to eq(different_object_field_type) + expect(object_field_type == object_field_type.__getobj__).to eq(true) + end + + def widget_schema_field(name) + widget_type.indexing_fields_by_name_in_index.fetch(name) + end + + def widget_indexing_field(name) + widget_field_type.subfields.find { |field| field.name == name } + end + + def widget_field_type + widget_type.to_indexing_field_type + end + + def widget_type + define_schema(schema_element_name_form: "snake_case") do |schema| + schema.object_type "Widget" do |type| + type.field "id", "ID!" + type.field "name", "String" do |field| + field.json_schema minLength: 1 + end + end + end.state.object_types_by_name.fetch("Widget") + end + + def unresolved_type_ref + Class.new do + def fully_unwrapped + self + end + + def resolved + nil + end + end.new + end + end + end + end + end +end diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_field_metadata_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_field_metadata_spec.rb similarity index 95% rename from elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_field_metadata_spec.rb rename to elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_field_metadata_spec.rb index 450ae447d..0ebfeafe0 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_field_metadata_spec.rb +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_field_metadata_spec.rb @@ -10,7 +10,7 @@ require "elastic_graph/spec_support/schema_definition_helpers" module ElasticGraph - module SchemaDefinition + module JSONIngestion::SchemaDefinition ::RSpec.describe "JSON schema field metadata generation" do include_context "SchemaDefinitionHelpers" @@ -143,13 +143,13 @@ def dump_metadata(&schema_definition) def define_schema(&schema_definition) super( schema_element_name_form: "snake_case", - extension_modules: [JSONIngestion::SchemaDefinition::APIExtension], + extension_modules: [APIExtension], &schema_definition ) end def field_meta_of(type, name_in_index) - JSONIngestion::SchemaDefinition::Indexing::JSONSchemaFieldMetadata.new(type: type, name_in_index: name_in_index) + Indexing::JSONSchemaFieldMetadata.new(type: type, name_in_index: name_in_index) end end end diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_pruner_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_pruner_spec.rb similarity index 97% rename from elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_pruner_spec.rb rename to elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_pruner_spec.rb index e9c01c9f2..c8327661f 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_pruner_spec.rb +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_pruner_spec.rb @@ -11,8 +11,8 @@ require "elastic_graph/spec_support/schema_definition_helpers" module ElasticGraph - module SchemaDefinition - RSpec.describe JSONIngestion::SchemaDefinition::JSONSchemaPruner do + module JSONIngestion::SchemaDefinition + RSpec.describe JSONSchemaPruner do include_context "SchemaDefinitionHelpers" describe ".prune" do diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_spec.rb similarity index 99% rename from elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_spec.rb rename to elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_spec.rb index 0ca98186c..7b4093806 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/json_schema_spec.rb +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/json_schema_spec.rb @@ -13,7 +13,7 @@ require "support/json_schema_matcher" module ElasticGraph - module SchemaDefinition + module JSONIngestion::SchemaDefinition ::RSpec.describe "JSON schema generation" do include_context "SchemaDefinitionHelpers" json_schema_id = {"allOf" => [{"$ref" => "#/$defs/ID"}, {"maxLength" => DEFAULT_MAX_KEYWORD_LENGTH}]} diff --git a/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb new file mode 100644 index 000000000..5e3f16424 --- /dev/null +++ b/elasticgraph-json_ingestion/spec/unit/elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension_spec.rb @@ -0,0 +1,102 @@ +# Copyright 2024 - 2026 Block, Inc. +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +require "elastic_graph/constants" +require "elastic_graph/errors" +require "elastic_graph/json_ingestion/schema_definition/schema_elements/scalar_type_extension" +require "elastic_graph/spec_support/schema_definition_helpers" + +module ElasticGraph + module JSONIngestion + module SchemaDefinition + module SchemaElements + RSpec.describe ScalarTypeExtension do + include_context "SchemaDefinitionHelpers" + + it "requires custom scalar types to declare their JSON schema representation" do + expect { + define_schema(schema_element_name_form: "snake_case") do |schema| + schema.scalar_type "BigInt" do |type| + type.mapping type: "long" + end + end + }.to raise_error Errors::SchemaError, a_string_including("BigInt", "lacks `json_schema`") + end + + it "extends schema elements created without customization blocks" do + api = build_api + api.enum_type "EmptyEnum" + api.interface_type "EmptyInterface" + direct_type_with_subfields = api.factory.new_type_with_subfields( + :object, + "DirectObject", + wrapping_type: nil, + field_factory: api.factory.method(:new_field) + ) + + expect(api.state.enum_types_by_name.fetch("EmptyEnum").singleton_class.ancestors).to include(EnumTypeExtension) + expect(api.state.object_types_by_name.fetch("EmptyInterface").__getobj__.singleton_class.ancestors).to include(TypeWithSubfieldsExtension) + expect(direct_type_with_subfields.singleton_class.ancestors).to include(TypeWithSubfieldsExtension) + + expect { + build_api.scalar_type "BigInt" + }.to raise_error Errors::SchemaError, a_string_including("BigInt", "lacks `json_schema`") + end + + it "infers a numeric missing-value placeholder for JSON-safe unsigned_long scalars with custom coercion" do + grouping_missing_value_placeholder = grouping_missing_value_placeholder_for( + "unsigned_long", + type: "integer", + maximum: JSON_SAFE_LONG_MAX + ) do |type| + type.coerce_with "ExampleScalarCoercionAdapter", defined_at: scalar_coercion_adapter_path + end + + expect(grouping_missing_value_placeholder).to eq(MISSING_NUMERIC_PLACEHOLDER) + end + + it "does not infer a numeric missing-value placeholder for unsigned_long scalars outside the JSON-safe range" do + grouping_missing_value_placeholder = grouping_missing_value_placeholder_for( + "unsigned_long", + type: "integer", + maximum: JSON_SAFE_LONG_MAX + 1 + ) do |type| + type.coerce_with "ExampleScalarCoercionAdapter", defined_at: scalar_coercion_adapter_path + end + + expect(grouping_missing_value_placeholder).to eq(nil) + end + + def grouping_missing_value_placeholder_for(mapping_type, **json_schema_options) + define_schema(schema_element_name_form: "snake_case") do |schema| + schema.scalar_type "CustomScalar" do |type| + type.mapping type: mapping_type + type.json_schema(**json_schema_options) + yield type + end + end.runtime_metadata.scalar_types_by_name.fetch("CustomScalar").grouping_missing_value_placeholder + end + + def scalar_coercion_adapter_path + ::File.join(CommonSpecHelpers::REPO_ROOT, "elasticgraph-schema_definition/spec/support/example_extensions/scalar_coercion_adapter") + end + + def build_api + schema_elements = ::ElasticGraph::SchemaArtifacts::RuntimeMetadata::SchemaElementNames.new(form: "snake_case") + ::ElasticGraph::SchemaDefinition::API.new( + schema_elements, + true, + extension_modules: [APIExtension], + output: log_device + ) + end + end + end + end + end +end diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/define_schema_spec.rb b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/define_schema_spec.rb index b0850973a..98ec529ac 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/define_schema_spec.rb +++ b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/define_schema_spec.rb @@ -82,6 +82,31 @@ module SchemaDefinition ) end + it "rejects type names reserved by schema definition extensions" do + expect { + define_schema do |schema| + schema.object_type EVENT_ENVELOPE_JSON_SCHEMA_NAME + end + }.to raise_error Errors::SchemaError, a_string_including( + "`#{EVENT_ENVELOPE_JSON_SCHEMA_NAME}` cannot be used as a schema type", + "reserved name" + ) + end + + it "allows test schemas to skip JSON schema version setup" do + result = define_schema(json_schema_version: nil) do |schema| + schema.object_type("Widget") do |t| + t.field "id", "ID" + end + end + + expect(type_def_from(result, "Widget")).to eq(<<~EOS.strip) + type Widget { + id: ID + } + EOS + end + it "produces the same GraphQL output, regardless of the order the types are defined in" do object_type_definitions = { "Component" => lambda do |t| diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/index_definitions_by_name_spec.rb b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/index_definitions_by_name_spec.rb index 1835088ba..3be37da65 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/index_definitions_by_name_spec.rb +++ b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/runtime_metadata/index_definitions_by_name_spec.rb @@ -41,6 +41,28 @@ module SchemaDefinition end end + it "rejects `route_with` fields that resolve to a non-leaf type" do + expect { + index_definition_metadata_for("widgets") do |i| + i.route_with "nested_fields_gql" + end + }.to raise_error Errors::SchemaError, a_string_including( + "shard routing field `MyType.nested_fields_gql", + "is not a leaf field" + ) + end + + it "includes nested field-path guidance for unresolved nested `route_with` fields" do + expect { + index_definition_metadata_for("widgets") do |i| + i.route_with "nested_fields_gql.missing" + end + }.to raise_error Errors::SchemaError, a_string_including( + "Field `MyType.nested_fields_gql.missing` cannot be resolved", + "Verify that all fields and types referenced by `nested_fields_gql.missing` are defined." + ) + end + it "defaults `route_with` to `id` because that's the default routing the datastore uses" do components = index_definition_metadata_for("components") expect(components.route_with).to eq "id" @@ -59,6 +81,28 @@ module SchemaDefinition expect(components.rollover).to eq nil end + it "rejects rollover fields that are not date or datetime fields" do + expect { + index_definition_metadata_for("widgets") do |i| + i.rollover :monthly, "group_id" + end + }.to raise_error Errors::SchemaError, a_string_including( + "rollover field `MyType.group_id", + "is not a `Date` or `DateTime` field" + ) + end + + it "rejects rollover fields that are lists" do + expect { + index_definition_metadata_for("widgets", on_my_type: ->(t) { t.field "timestamps", "[DateTime!]!" }) do |i| + i.rollover :monthly, "timestamps" + end + }.to raise_error Errors::SchemaError, a_string_including( + "rollover field `MyType.timestamps", + "is a list field" + ) + end + it "dumps the `rollover` timestamp field's `name_in_index`" do widgets = index_definition_metadata_for("widgets") do |i| i.rollover :monthly, "created_at_gql" diff --git a/spec_support/lib/elastic_graph/spec_support/enable_simplecov.rb b/spec_support/lib/elastic_graph/spec_support/enable_simplecov.rb index a9769863a..0d87095f3 100644 --- a/spec_support/lib/elastic_graph/spec_support/enable_simplecov.rb +++ b/spec_support/lib/elastic_graph/spec_support/enable_simplecov.rb @@ -103,9 +103,6 @@ def wait_for_other_processes # status if we're not running it's test suite. add_filter "/elasticgraph-local/" unless spec_files_to_run.any? { |f| f.include?("/elasticgraph-local/") } - # The JSON ingestion gem is being introduced by extracting implementation first and moving its tests later. - add_filter "/elasticgraph-json_ingestion/" - # This version file is loaded from our gemspecs, which can get loaded by bundler before we get here. # SimpleCov is only able to track coverage of files loaded after it starts, so we need to filter them out if # their constant is already defined. They don't contain any branching statements or anything so it's ok to