diff --git a/AGENTS.md b/AGENTS.md index f4f5bef06..ea25d3bfd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -123,6 +123,10 @@ When adding filtering predicates or aggregation functions: 3. **Query Translation**: Implement GraphQL → datastore query translation with unit, integration, and acceptance tests 4. **Documentation**: Add user-facing docs with working examples to `config/site/` +### Verification + +After completing a chunk of work, run `script/run_specs` and `script/type_check` to verify correctness across all gems and type signatures. + ### Test Strategy Three layers of testing: @@ -186,6 +190,10 @@ Custom gems can be added via `Gemfile-custom` (see `Gemfile-custom.example`), th ## Important Patterns +### Schema Definition + +- When referencing derived type names (e.g. filter input types), never hardcode names like `"StringFilterInput"`. Always use `schema_def_state.type_ref("String").as_filter_input.name` (or similar `as_*` methods on type references). Hardcoded names break when schema element names are customized (e.g. camelCase schemas). + ### Schema Artifacts After schema definition changes, always run: ```bash diff --git a/config/schema/artifacts/runtime_metadata.yaml b/config/schema/artifacts/runtime_metadata.yaml index 46005bf2f..ac8ea8f25 100644 --- a/config/schema/artifacts/runtime_metadata.yaml +++ b/config/schema/artifacts/runtime_metadata.yaml @@ -3820,6 +3820,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + DistributionChannelFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename DistributionChannelGroupedBy: graphql_fields_by_name: active: @@ -4182,6 +4186,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + InventorFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename InventorGroupedBy: graphql_fields_by_name: name: @@ -5098,6 +5106,8 @@ object_types_by_name: name: object_without_lookahead NamedEntityFilterInput: graphql_fields_by_name: + _typename: + name_in_index: __typename amount_cents2: name_in_index: amount_cents created_at2: @@ -5422,6 +5432,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + NamedInventorFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename NamedInventorGroupedBy: graphql_fields_by_name: name: @@ -5630,6 +5644,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + PartFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename PartGroupedBy: graphql_fields_by_name: created_at: @@ -6195,6 +6213,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + RetailFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename RetailGroupedBy: graphql_fields_by_name: active: @@ -6506,6 +6528,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + StoreFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename StoreGroupedBy: graphql_fields_by_name: active: @@ -7540,6 +7566,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + WholesaleFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename WholesaleGroupedBy: graphql_fields_by_name: active: @@ -8869,6 +8899,8 @@ object_types_by_name: name: object_without_lookahead WidgetOrAddressFilterInput: graphql_fields_by_name: + _typename: + name_in_index: __typename amount_cents2: name_in_index: amount_cents created_at2: diff --git a/config/schema/artifacts/schema.graphql b/config/schema/artifacts/schema.graphql index 024e43ebe..c34a4db96 100644 --- a/config/schema/artifacts/schema.graphql +++ b/config/schema/artifacts/schema.graphql @@ -2694,6 +2694,14 @@ Input type used to specify filters on `DistributionChannel` fields. Will match all documents if passed as an empty object (or as `null`). """ input DistributionChannelFilterInput { + """ + Filters `DistributionChannel` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Used to filter on the `active` field. @@ -3968,6 +3976,14 @@ Input type used to specify filters on `Inventor` fields. Will match all documents if passed as an empty object (or as `null`). """ input InventorFilterInput { + """ + Filters `Inventor` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Matches records where all of the provided sub-filters evaluate to true. This works just like an AND operator in SQL. @@ -6255,6 +6271,14 @@ Input type used to specify filters on `NamedEntity` fields. Will match all documents if passed as an empty object (or as `null`). """ input NamedEntityFilterInput { + """ + Filters `NamedEntity` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Matches records where all of the provided sub-filters evaluate to true. This works just like an AND operator in SQL. @@ -7636,6 +7660,14 @@ Input type used to specify filters on `NamedInventor` fields. Will match all documents if passed as an empty object (or as `null`). """ input NamedInventorFilterInput { + """ + Filters `NamedInventor` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Matches records where all of the provided sub-filters evaluate to true. This works just like an AND operator in SQL. @@ -7994,6 +8026,14 @@ Input type used to specify filters on `Part` fields. Will match all documents if passed as an empty object (or as `null`). """ input PartFilterInput { + """ + Filters `Part` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Matches records where all of the provided sub-filters evaluate to true. This works just like an AND operator in SQL. @@ -11397,6 +11437,14 @@ Input type used to specify filters on `Retail` fields. Will match all documents if passed as an empty object (or as `null`). """ input RetailFilterInput { + """ + Filters `Retail` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Used to filter on the `active` field. @@ -12494,6 +12542,14 @@ Input type used to specify filters on `Store` fields. Will match all documents if passed as an empty object (or as `null`). """ input StoreFilterInput { + """ + Filters `Store` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Used to filter on the `active` field. @@ -15388,6 +15444,14 @@ Input type used to specify filters on `Wholesale` fields. Will match all documents if passed as an empty object (or as `null`). """ input WholesaleFilterInput { + """ + Filters `Wholesale` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Used to filter on the `active` field. @@ -17708,6 +17772,14 @@ Input type used to specify filters on `WidgetOrAddress` fields. Will match all documents if passed as an empty object (or as `null`). """ input WidgetOrAddressFilterInput { + """ + Filters `WidgetOrAddress` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Matches records where all of the provided sub-filters evaluate to true. This works just like an AND operator in SQL. diff --git a/config/schema/artifacts_with_apollo/runtime_metadata.yaml b/config/schema/artifacts_with_apollo/runtime_metadata.yaml index dfb191604..458a3f6b3 100644 --- a/config/schema/artifacts_with_apollo/runtime_metadata.yaml +++ b/config/schema/artifacts_with_apollo/runtime_metadata.yaml @@ -3922,6 +3922,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + DistributionChannelFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename DistributionChannelGroupedBy: graphql_fields_by_name: active: @@ -4284,6 +4288,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + InventorFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename InventorGroupedBy: graphql_fields_by_name: name: @@ -5221,6 +5229,8 @@ object_types_by_name: name: object_without_lookahead NamedEntityFilterInput: graphql_fields_by_name: + _typename: + name_in_index: __typename amount_cents2: name_in_index: amount_cents created_at2: @@ -5545,6 +5555,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + NamedInventorFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename NamedInventorGroupedBy: graphql_fields_by_name: name: @@ -5753,6 +5767,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + PartFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename PartGroupedBy: graphql_fields_by_name: created_at: @@ -6324,6 +6342,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + RetailFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename RetailGroupedBy: graphql_fields_by_name: active: @@ -6635,6 +6657,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + StoreFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename StoreGroupedBy: graphql_fields_by_name: active: @@ -7669,6 +7695,10 @@ object_types_by_name: node: resolver: name: object_without_lookahead + WholesaleFilterInput: + graphql_fields_by_name: + _typename: + name_in_index: __typename WholesaleGroupedBy: graphql_fields_by_name: active: @@ -8998,6 +9028,8 @@ object_types_by_name: name: object_without_lookahead WidgetOrAddressFilterInput: graphql_fields_by_name: + _typename: + name_in_index: __typename amount_cents2: name_in_index: amount_cents created_at2: diff --git a/config/schema/artifacts_with_apollo/schema.graphql b/config/schema/artifacts_with_apollo/schema.graphql index 1418fd751..0a24a785f 100644 --- a/config/schema/artifacts_with_apollo/schema.graphql +++ b/config/schema/artifacts_with_apollo/schema.graphql @@ -2961,6 +2961,14 @@ Input type used to specify filters on `DistributionChannel` fields. Will match all documents if passed as an empty object (or as `null`). """ input DistributionChannelFilterInput { + """ + Filters `DistributionChannel` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Used to filter on the `active` field. @@ -4250,6 +4258,14 @@ Input type used to specify filters on `Inventor` fields. Will match all documents if passed as an empty object (or as `null`). """ input InventorFilterInput { + """ + Filters `Inventor` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Matches records where all of the provided sub-filters evaluate to true. This works just like an AND operator in SQL. @@ -6537,6 +6553,14 @@ Input type used to specify filters on `NamedEntity` fields. Will match all documents if passed as an empty object (or as `null`). """ input NamedEntityFilterInput { + """ + Filters `NamedEntity` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Matches records where all of the provided sub-filters evaluate to true. This works just like an AND operator in SQL. @@ -7918,6 +7942,14 @@ Input type used to specify filters on `NamedInventor` fields. Will match all documents if passed as an empty object (or as `null`). """ input NamedInventorFilterInput { + """ + Filters `NamedInventor` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Matches records where all of the provided sub-filters evaluate to true. This works just like an AND operator in SQL. @@ -8276,6 +8308,14 @@ Input type used to specify filters on `Part` fields. Will match all documents if passed as an empty object (or as `null`). """ input PartFilterInput { + """ + Filters `Part` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Matches records where all of the provided sub-filters evaluate to true. This works just like an AND operator in SQL. @@ -11720,6 +11760,14 @@ Input type used to specify filters on `Retail` fields. Will match all documents if passed as an empty object (or as `null`). """ input RetailFilterInput { + """ + Filters `Retail` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Used to filter on the `active` field. @@ -12817,6 +12865,14 @@ Input type used to specify filters on `Store` fields. Will match all documents if passed as an empty object (or as `null`). """ input StoreFilterInput { + """ + Filters `Store` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Used to filter on the `active` field. @@ -15711,6 +15767,14 @@ Input type used to specify filters on `Wholesale` fields. Will match all documents if passed as an empty object (or as `null`). """ input WholesaleFilterInput { + """ + Filters `Wholesale` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Used to filter on the `active` field. @@ -18031,6 +18095,14 @@ Input type used to specify filters on `WidgetOrAddress` fields. Will match all documents if passed as an empty object (or as `null`). """ input WidgetOrAddressFilterInput { + """ + Filters `WidgetOrAddress` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + """ + _typename: StringFilterInput + """ Matches records where all of the provided sub-filters evaluate to true. This works just like an AND operator in SQL. diff --git a/elasticgraph-apollo/spec/unit/elastic_graph/apollo/schema_definition_spec.rb b/elasticgraph-apollo/spec/unit/elastic_graph/apollo/schema_definition_spec.rb index e5b1f8171..558f1cb7a 100644 --- a/elasticgraph-apollo/spec/unit/elastic_graph/apollo/schema_definition_spec.rb +++ b/elasticgraph-apollo/spec/unit/elastic_graph/apollo/schema_definition_spec.rb @@ -1357,6 +1357,7 @@ def expect_identifiable_type_tagging_of_token(&type_def_for) # the tagging of those source fields. That's why `name`, `options1`, etc are tagged with `public` below. expect(type_def_for.call("IdentifiableFilterInput")).to eq(<<~EOS.strip) input IdentifiableFilterInput { + #{schema_elements._typename}: StringFilterInput #{schema_elements.all_of}: [IdentifiableFilterInput!] #{schema_elements.any_of}: [IdentifiableFilterInput!] id: IDFilterInput diff --git a/elasticgraph-graphql/spec/acceptance/search_spec.rb b/elasticgraph-graphql/spec/acceptance/search_spec.rb index 910471b7b..7c19ab980 100644 --- a/elasticgraph-graphql/spec/acceptance/search_spec.rb +++ b/elasticgraph-graphql/spec/acceptance/search_spec.rb @@ -799,6 +799,38 @@ module ElasticGraph .dig("data", case_correctly("store_aggregations"), "nodes", 0, case_correctly("count")) expect(store_agg_count).to eq(expected_store_typenames.size) + # `_typename` filter allows querying by concrete subtype across multiple indexes and + # branches of the type hierarchy. `DirectWholesaler` is in the shared `distribution_channels` + # index; `PhysicalStore` has a dedicated index using `constant_keyword` for `__typename`. + typename_key = case_correctly("_typename").to_sym + wholesaler_or_physical = list_distribution_channels_with( + *all_channel_fragments, + filter: {typename_key => {equal_to_any_of: ["DirectWholesaler", "PhysicalStore"]}} + ) + expect(wholesaler_or_physical.map { |c| c["__typename"] }).to contain_exactly("DirectWholesaler", "PhysicalStore", "PhysicalStore") + + # `_typename` filter interacts correctly with automatic `__typename` scoping at a sub-interface level. + # Filtering `retailers` to `OnlineStore OR PhysicalStore` returns all retailers (the full set), + # confirming both the shared-index and dedicated-index subtypes are matched correctly. + all_retailers = list_retailers_with( + *store_fragments, + filter: {typename_key => {equal_to_any_of: ["OnlineStore", "PhysicalStore"]}} + ) + expect(all_retailers.map { |r| r["__typename"] }).to contain_exactly(*expected_store_typenames) + + # `_typename` filter also works on aggregations, including across indexes. + wholesaler_or_physical_agg_count = call_graphql_query(<<~QUERY) + query { + #{case_correctly("distribution_channel_aggregations")}(filter: { + #{typename_key}: { #{case_correctly("equal_to_any_of")}: ["DirectWholesaler", "PhysicalStore"] } + }) { + nodes { #{case_correctly("count")} } + } + } + QUERY + .dig("data", case_correctly("distribution_channel_aggregations"), "nodes", 0, case_correctly("count")) + expect(wholesaler_or_physical_agg_count).to eq(wholesaler_or_physical.size) + # all_highlights resolves against the concrete type (OnlineStore), not the abstract root # (DistributionChannel). OnlineStore.name is absent from DistributionChannel — without # __typename-aware type resolution the highlight would be silently dropped. @@ -1401,6 +1433,16 @@ def expect_error_from(filter, *error_snippets) filter: {inventor: {stock_ticker_key => {not: {equal_to_any_of: [widget2.fetch(:inventor).fetch(stock_ticker_key)]}}}} )).to contain_exactly(expected_widget1) + typename_key = case_correctly("_typename").to_sym + # ...or on the subfield's _typename (single underscore, since `__` prefix is reserved by the GraphQL spec) + expect(list_widgets_with_options_and_inventor( + filter: {inventor: {typename_key => {equal_to_any_of: ["Company"]}}} + )).to contain_exactly(expected_widget2) + + expect(list_widgets_with_options_and_inventor( + filter: {inventor: {typename_key => {equal_to_any_of: ["Person"]}}} + )).to contain_exactly(expected_widget1) + # On interfaces you can filter on a subfield that is present on all subtypes... expect(list_widgets_with_options_and_inventor( filter: {named_inventor: {name: {equal_to_any_of: [widget1.fetch(:inventor).fetch(:name)]}}} @@ -1420,12 +1462,14 @@ def expect_error_from(filter, *error_snippets) filter: {named_inventor: {stock_ticker_key => {not: {equal_to_any_of: [widget2.fetch(:inventor).fetch(stock_ticker_key)]}}}} )).to contain_exactly(expected_widget1) - # ...or on `__typename`. Well, you could if the GraphQL spec allowed input fields - # named `__typename`, but it does not (see http://spec.graphql.org/June2018/#sec-Input-Objects) - # so we do not yet support it. - # expect(list_widgets_with_options_and_inventor( - # filter: { inventor: { __typename: { equal_to_any_of: ["Company"] } } } - # )).to contain_exactly(expected_widget2) + # ...or on the subfield's _typename (single underscore, since `__` prefix is reserved by the GraphQL spec) + expect(list_widgets_with_options_and_inventor( + filter: {named_inventor: {typename_key => {equal_to_any_of: ["Company"]}}} + )).to contain_exactly(expected_widget2) + + expect(list_widgets_with_options_and_inventor( + filter: {named_inventor: {typename_key => {equal_to_any_of: ["Person"]}}} + )).to contain_exactly(expected_widget1) end end diff --git a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/query_adapter/filters_spec.rb b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/query_adapter/filters_spec.rb index 4b095961b..dd752083c 100644 --- a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/query_adapter/filters_spec.rb +++ b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/query_adapter/filters_spec.rb @@ -234,6 +234,25 @@ class QueryAdapter expect(query.internal_filters).to be_empty end + it "translates `_typename` filter field on an abstract type to `__typename` in the datastore query" do + query = datastore_query_for(:Query, :widgets, <<~QUERY) + query { + widgets(filter: {inventor: {_typename: {equal_to_any_of: ["Company"]}}}) { + edges { + node { + id + } + } + } + } + QUERY + + expect(query.client_filters).to contain_exactly({ + "inventor" => {"__typename" => {"equal_to_any_of" => ["Company"]}} + }) + expect(query.internal_filters).to be_empty + end + it "translates `count` on a list field to `#{LIST_COUNTS_FIELD}` while leaving a `count` schema field unchanged" do query = datastore_query_for(:Query, :widgets, <<~QUERY) query { diff --git a/elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names.rb b/elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names.rb index 07932f95d..316da3382 100644 --- a/elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names.rb +++ b/elasticgraph-schema_artifacts/lib/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names.rb @@ -128,7 +128,7 @@ module CamelCaseConverter extend self def normalize_case(name) - name.gsub(/_(\w)/) { $1.upcase } + name.gsub(/(?<=\w)_(\w)/) { $1.upcase } end end @@ -141,7 +141,7 @@ def normalize_case(name) # @private SchemaElementNames = SchemaElementNamesDefinition.new( # Filter arg and operation names: - :filter, + :filter, :_typename, :equal_to_any_of, :gt, :gte, :lt, :lte, :matches_phrase, :matches_query, :matches_query_with_prefix, :any_of, :all_of, :not, :time_of_day, :any_satisfy, :contains, :starts_with, :all_substrings_of, :any_substring_of, :ignore_case, :any_prefix_of, # Directives diff --git a/elasticgraph-schema_artifacts/sig/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names.rbs b/elasticgraph-schema_artifacts/sig/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names.rbs index d12b861b7..255e2d8c0 100644 --- a/elasticgraph-schema_artifacts/sig/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names.rbs +++ b/elasticgraph-schema_artifacts/sig/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names.rbs @@ -17,6 +17,7 @@ module ElasticGraph def canonical_name_for: (::String | ::Symbol) -> ::Symbol attr_reader filter: ::String + attr_reader _typename: ::String attr_reader equal_to_any_of: ::String attr_reader gt: ::String attr_reader gte: ::String diff --git a/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names_spec.rb b/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names_spec.rb index e7277743b..b719ecc50 100644 --- a/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names_spec.rb +++ b/elasticgraph-schema_artifacts/spec/unit/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names_spec.rb @@ -12,12 +12,12 @@ module ElasticGraph module SchemaArtifacts module RuntimeMetadata ExampleElementNames = SchemaElementNamesDefinition.new( - :foo, :multi_word_snake, :multiWordCamel + :foo, :multi_word_snake, :multiWordCamel, :_leading_underscore ) RSpec.describe SchemaElementNamesDefinition do it "exposes the set of element names via an `ELEMENT_NAMES` constant" do - expect(ExampleElementNames::ELEMENT_NAMES).to eq [:foo, :multi_word_snake, :multiWordCamel] + expect(ExampleElementNames::ELEMENT_NAMES).to eq [:foo, :multi_word_snake, :multiWordCamel, :_leading_underscore] end it "exposes camelCase element names when so configured, via snake case attributes" do @@ -26,7 +26,8 @@ module RuntimeMetadata expect(names).to have_attributes( foo: "foo", multi_word_snake: "multiWordSnake", - multi_word_camel: "multiWordCamel" + multi_word_camel: "multiWordCamel", + _leading_underscore: "_leadingUnderscore" ) end @@ -36,7 +37,8 @@ module RuntimeMetadata expect(names).to have_attributes( foo: "foo", multi_word_snake: "multi_word_snake", - multi_word_camel: "multi_word_camel" + multi_word_camel: "multi_word_camel", + _leading_underscore: "_leading_underscore" ) end @@ -54,6 +56,7 @@ module RuntimeMetadata expect(names.normalize_case("foo_bar")).to eq "fooBar" expect(names.normalize_case("fooBar")).to eq "fooBar" expect(names.normalize_case("FooBar")).to eq "FooBar" + expect(names.normalize_case("_typename")).to eq "_typename" end it "allows overrides" do diff --git a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb index a438b6fe9..5a67084fe 100644 --- a/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +++ b/elasticgraph-schema_definition/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb @@ -87,6 +87,17 @@ def to_input_filters return [] if does_not_support?(&:filterable?) schema_def_state.factory.build_standard_filter_input_types_for_index_object_type(name) do |t| + if abstract? + t.field schema_def_state.schema_elements._typename, schema_def_state.type_ref("String").as_filter_input.name, name_in_index: "__typename" do |f| + f.documentation <<~EOS + Filters `#{name}` records by concrete type. Only concrete type names are valid values — + filtering on an abstract type name will match nothing, since records only have concrete + type names for their `__typename` value. + Analogous to the `__typename` return field. + EOS + end + end + graphql_fields_by_name.values.each do |field| if field.filterable? t.graphql_fields_by_name[field.name] = field.to_filter_field(parent_type: t) diff --git a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/filters_spec.rb b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/filters_spec.rb index 0997a7ed2..d3181745e 100644 --- a/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/filters_spec.rb +++ b/elasticgraph-schema_definition/spec/unit/elastic_graph/schema_definition/graphql_schema/filters_spec.rb @@ -1148,15 +1148,14 @@ module SchemaDefinition end end - # Note: we would like to support filtering on `__typename` but this is invalid according to the - # GraphQL Spec: http://spec.graphql.org/June2018/#sec-Input-Objects - # > For each input field of an Input Object type: - # > 2. The input field must not have a name which begins with the characters "__" (two underscores). + # Uses `_typename` (single underscore) since the GraphQL spec prohibits `__` prefix on input fields: + # http://spec.graphql.org/June2018/#sec-Input-Objects expect(filter_type_from(result, "Inventor")).to eq(<<~EOS.strip) input InventorFilterInput { #{schema_elements.any_of}: [InventorFilterInput!] #{schema_elements.all_of}: [InventorFilterInput!] #{schema_elements.not}: InventorFilterInput + #{schema_elements._typename}: StringFilterInput name: StringFilterInput nationality: StringFilterInput stock_ticker: StringFilterInput @@ -1222,6 +1221,7 @@ module SchemaDefinition #{schema_elements.any_of}: [ClothingItemFilterInput!] #{schema_elements.all_of}: [ClothingItemFilterInput!] #{schema_elements.not}: ClothingItemFilterInput + #{schema_elements._typename}: StringFilterInput size: SizeFilterInput shirt_color: StringFilterInput pants_color: StringFilterInput @@ -1279,6 +1279,7 @@ module SchemaDefinition #{schema_elements.any_of}: [InventorFilterInput!] #{schema_elements.all_of}: [InventorFilterInput!] #{schema_elements.not}: InventorFilterInput + #{schema_elements._typename}: StringFilterInput name: StringFilterInput stock_ticker: StringFilterInput } @@ -1340,6 +1341,7 @@ def link_supertype_to_subtypes(interface_type, *subtype_names) #{schema_elements.any_of}: [InventorFilterInput!] #{schema_elements.all_of}: [InventorFilterInput!] #{schema_elements.not}: InventorFilterInput + #{schema_elements._typename}: StringFilterInput name: StringFilterInput nationality: StringFilterInput stock_ticker: StringFilterInput