diff --git a/config/schema/artifacts/json_schemas.yaml b/config/schema/artifacts/json_schemas.yaml index 7f23fd764..64c6f7129 100644 --- a/config/schema/artifacts/json_schemas.yaml +++ b/config/schema/artifacts/json_schemas.yaml @@ -23,8 +23,10 @@ json_schema_version: 1 type: string enum: - Address + - BrokerWholesaler - Company - Component + - DirectWholesaler - ElectricalPart - Manufacturer - MechanicalPart @@ -33,7 +35,6 @@ json_schema_version: 1 - PhysicalStore - Sponsor - Team - - ThirdPartyWholesale - Widget - WidgetWorkspace id: @@ -160,6 +161,24 @@ json_schema_version: 1 - sponsorships_object Boolean: type: boolean + BrokerWholesaler: + type: object + properties: + id: + allOf: + - "$ref": "#/$defs/ID" + - maxLength: 8191 + active: + anyOf: + - "$ref": "#/$defs/Boolean" + - type: 'null' + __typename: + type: string + const: BrokerWholesaler + default: BrokerWholesaler + required: + - id + - active Color: type: string enum: @@ -253,6 +272,24 @@ json_schema_version: 1 DateTime: type: string format: date-time + DirectWholesaler: + type: object + properties: + id: + allOf: + - "$ref": "#/$defs/ID" + - maxLength: 8191 + active: + anyOf: + - "$ref": "#/$defs/Boolean" + - type: 'null' + __typename: + type: string + const: DirectWholesaler + default: DirectWholesaler + required: + - id + - active ElectricalPart: type: object properties: @@ -875,24 +912,6 @@ json_schema_version: 1 - was_shortened - players_nested - players_object - ThirdPartyWholesale: - type: object - properties: - id: - allOf: - - "$ref": "#/$defs/ID" - - maxLength: 8191 - active: - anyOf: - - "$ref": "#/$defs/Boolean" - - type: 'null' - __typename: - type: string - const: ThirdPartyWholesale - default: ThirdPartyWholesale - required: - - id - - active Untyped: type: - array diff --git a/config/schema/artifacts/json_schemas_by_version/v1.yaml b/config/schema/artifacts/json_schemas_by_version/v1.yaml index 3b8798aee..ad09ed3e1 100644 --- a/config/schema/artifacts/json_schemas_by_version/v1.yaml +++ b/config/schema/artifacts/json_schemas_by_version/v1.yaml @@ -23,8 +23,10 @@ json_schema_version: 1 type: string enum: - Address + - BrokerWholesaler - Company - Component + - DirectWholesaler - ElectricalPart - Manufacturer - MechanicalPart @@ -33,7 +35,6 @@ json_schema_version: 1 - PhysicalStore - Sponsor - Team - - ThirdPartyWholesale - Widget - WidgetWorkspace id: @@ -187,6 +188,30 @@ json_schema_version: 1 - sponsorships_object Boolean: type: boolean + BrokerWholesaler: + type: object + properties: + id: + allOf: + - "$ref": "#/$defs/ID" + - maxLength: 8191 + ElasticGraph: + type: ID! + nameInIndex: id + active: + anyOf: + - "$ref": "#/$defs/Boolean" + - type: 'null' + ElasticGraph: + type: Boolean + nameInIndex: active + __typename: + type: string + const: BrokerWholesaler + default: BrokerWholesaler + required: + - id + - active Color: type: string enum: @@ -313,6 +338,30 @@ json_schema_version: 1 DateTime: type: string format: date-time + DirectWholesaler: + type: object + properties: + id: + allOf: + - "$ref": "#/$defs/ID" + - maxLength: 8191 + ElasticGraph: + type: ID! + nameInIndex: id + active: + anyOf: + - "$ref": "#/$defs/Boolean" + - type: 'null' + ElasticGraph: + type: Boolean + nameInIndex: active + __typename: + type: string + const: DirectWholesaler + default: DirectWholesaler + required: + - id + - active ElectricalPart: type: object properties: @@ -1175,30 +1224,6 @@ json_schema_version: 1 - was_shortened - players_nested - players_object - ThirdPartyWholesale: - type: object - properties: - id: - allOf: - - "$ref": "#/$defs/ID" - - maxLength: 8191 - ElasticGraph: - type: ID! - nameInIndex: id - active: - anyOf: - - "$ref": "#/$defs/Boolean" - - type: 'null' - ElasticGraph: - type: Boolean - nameInIndex: active - __typename: - type: string - const: ThirdPartyWholesale - default: ThirdPartyWholesale - required: - - id - - active Untyped: type: - array diff --git a/config/schema/artifacts/runtime_metadata.yaml b/config/schema/artifacts/runtime_metadata.yaml index 9dedd741e..52750db69 100644 --- a/config/schema/artifacts/runtime_metadata.yaml +++ b/config/schema/artifacts/runtime_metadata.yaml @@ -1061,6 +1061,16 @@ enum_types_by_name: sort_field: direction: desc field_path: league + WholesaleSortOrderInput: + values_by_name: + id_ASC: + sort_field: + direction: asc + field_path: id + id_DESC: + sort_field: + direction: desc + field_path: id WidgetCurrencySortOrderInput: values_by_name: details_symbol_ASC: @@ -3296,6 +3306,38 @@ object_types_by_name: graphql_fields_by_name: count: name_in_index: __counts + BrokerWholesaler: + graphql_fields_by_name: + active: + resolver: + name: get_record_field_value + id: + resolver: + name: get_record_field_value + index_definition_names: + - distribution_channels + update_targets: + - data_params: + __typename: + cardinality: one + active: + cardinality: one + id_source: id + metadata_params: + relationship: + value: __self + sourceId: + cardinality: one + source_path: id + sourceType: + cardinality: one + source_path: type + version: + cardinality: one + relationship: __self + routing_value_source: id + script_id: update_index_data_1fdfaf1c9261c96019decc89b515bd9a + type: BrokerWholesaler ColorListFilterInput: graphql_fields_by_name: count: @@ -3805,6 +3847,38 @@ object_types_by_name: graphql_fields_by_name: count: name_in_index: __counts + DirectWholesaler: + graphql_fields_by_name: + active: + resolver: + name: get_record_field_value + id: + resolver: + name: get_record_field_value + index_definition_names: + - distribution_channels + update_targets: + - data_params: + __typename: + cardinality: one + active: + cardinality: one + id_source: id + metadata_params: + relationship: + value: __self + sourceId: + cardinality: one + source_path: id + sourceType: + cardinality: one + source_path: type + version: + cardinality: one + relationship: __self + routing_value_source: id + script_id: update_index_data_1fdfaf1c9261c96019decc89b515bd9a + type: DirectWholesaler DistributionChannel: graphql_fields_by_name: active: @@ -6222,6 +6296,12 @@ object_types_by_name: teams: resolver: name: list_records + wholesale_aggregations: + resolver: + name: list_records + wholesalers: + resolver: + name: list_records widget_aggregations: resolver: name: list_records @@ -7603,7 +7683,7 @@ object_types_by_name: players_object: resolver: name: object_with_lookahead - ThirdPartyWholesale: + Wholesale: graphql_fields_by_name: active: resolver: @@ -7613,28 +7693,88 @@ object_types_by_name: name: get_record_field_value index_definition_names: - distribution_channels - update_targets: - - data_params: - __typename: - cardinality: one - active: - cardinality: one - id_source: id - metadata_params: - relationship: - value: __self - sourceId: - cardinality: one - source_path: id - sourceType: - cardinality: one - source_path: type - version: - cardinality: one - relationship: __self - routing_value_source: id - script_id: update_index_data_1fdfaf1c9261c96019decc89b515bd9a - type: ThirdPartyWholesale + WholesaleAggregatedValues: + graphql_fields_by_name: + active: + resolver: + name: object_with_lookahead + id: + resolver: + name: object_with_lookahead + WholesaleAggregation: + elasticgraph_category: indexed_aggregation + graphql_fields_by_name: + aggregated_values: + resolver: + name: object_without_lookahead + count: + resolver: + name: object_without_lookahead + grouped_by: + resolver: + name: object_without_lookahead + source_type: Wholesale + WholesaleAggregationConnection: + elasticgraph_category: relay_connection + graphql_fields_by_name: + edges: + resolver: + name: object_without_lookahead + nodes: + resolver: + name: object_without_lookahead + page_info: + resolver: + name: object_without_lookahead + WholesaleAggregationEdge: + elasticgraph_category: relay_edge + graphql_fields_by_name: + cursor: + resolver: + name: object_without_lookahead + node: + resolver: + name: object_without_lookahead + WholesaleConnection: + elasticgraph_category: relay_connection + graphql_fields_by_name: + edges: + resolver: + name: object_without_lookahead + nodes: + resolver: + name: object_without_lookahead + page_info: + resolver: + name: object_without_lookahead + total_edge_count: + resolver: + name: object_without_lookahead + WholesaleEdge: + elasticgraph_category: relay_edge + graphql_fields_by_name: + all_highlights: + resolver: + name: object_without_lookahead + cursor: + resolver: + name: object_without_lookahead + highlights: + resolver: + name: object_without_lookahead + node: + resolver: + name: object_without_lookahead + WholesaleGroupedBy: + graphql_fields_by_name: + active: + resolver: + name: object_with_lookahead + WholesaleHighlights: + graphql_fields_by_name: + id: + resolver: + name: get_record_field_value Widget: graphql_fields_by_name: amount_cents: diff --git a/config/schema/artifacts/schema.graphql b/config/schema/artifacts/schema.graphql index 4b46e92c0..11b6f3586 100644 --- a/config/schema/artifacts/schema.graphql +++ b/config/schema/artifacts/schema.graphql @@ -668,6 +668,11 @@ input BooleanListFilterInput { not: BooleanListFilterInput } +type BrokerWholesaler implements DistributionChannel & Wholesale { + active: Boolean + id: ID! +} + enum Color { BLUE GREEN @@ -2744,6 +2749,11 @@ input DayOfWeekGroupingOffsetInput { unit: DateTimeUnitInput! } +type DirectWholesaler implements DistributionChannel & Wholesale { + active: Boolean + id: ID! +} + """ Enumerates the supported distance units. """ @@ -11338,6 +11348,109 @@ type Query { order_by: [TeamSortOrderInput!] ): TeamConnection + """ + Aggregations over the `wholesalers` data: + + > Fetches `Wholesale`s based on the provided arguments. + """ + wholesale_aggregations( + """ + Used to forward-paginate through the `wholesale_aggregations`. When provided, the next page after the + provided cursor will be returned. + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + after: Cursor + + """ + Used to backward-paginate through the `wholesale_aggregations`. When provided, the previous page before the + provided cursor will be returned. + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + before: Cursor + + """ + Used to filter the `Wholesale` documents that get aggregated over based on the provided criteria. + """ + filter: WholesaleFilterInput + + """ + Used in conjunction with the `after` argument to forward-paginate through the `wholesale_aggregations`. + When provided, limits the number of returned results to the first `n` after the provided + `after` cursor (or from the start of the `wholesale_aggregations`, if no `after` cursor is provided). + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + first: Int + + """ + Used in conjunction with the `before` argument to backward-paginate through the `wholesale_aggregations`. + When provided, limits the number of returned results to the last `n` before the provided + `before` cursor (or from the end of the `wholesale_aggregations`, if no `before` cursor is provided). + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + last: Int + ): WholesaleAggregationConnection + + """ + Fetches `Wholesale`s based on the provided arguments. + """ + wholesalers( + """ + Used to forward-paginate through the `wholesalers`. When provided, the next page after the + provided cursor will be returned. + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + after: Cursor + + """ + Used to backward-paginate through the `wholesalers`. When provided, the previous page before the + provided cursor will be returned. + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + before: Cursor + + """ + Used to filter the returned `wholesalers` based on the provided criteria. + """ + filter: WholesaleFilterInput + + """ + Used in conjunction with the `after` argument to forward-paginate through the `wholesalers`. + When provided, limits the number of returned results to the first `n` after the provided + `after` cursor (or from the start of the `wholesalers`, if no `after` cursor is provided). + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + first: Int + + """ + Used in conjunction with the `before` argument to backward-paginate through the `wholesalers`. + When provided, limits the number of returned results to the last `n` before the provided + `before` cursor (or from the end of the `wholesalers`, if no `before` cursor is provided). + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + last: Int + + """ + Used to specify how the returned `wholesalers` should be sorted. + """ + order_by: [WholesaleSortOrderInput!] + ): WholesaleConnection + """ Aggregations over the `widgets` data: @@ -15691,11 +15804,6 @@ input TextFilterInput { not: TextFilterInput } -type ThirdPartyWholesale implements DistributionChannel { - active: Boolean - id: ID! -} - """ An [IANA time zone identifier](https://www.iana.org/time-zones), such as `America/Los_Angeles` or `UTC`. @@ -15763,6 +15871,232 @@ input UntypedFilterInput { not: UntypedFilterInput } +interface Wholesale implements DistributionChannel { + active: Boolean + id: ID! +} + +""" +Type used to perform aggregation computations on `Wholesale` fields. +""" +type WholesaleAggregatedValues { + """ + Computed aggregate values for the `active` field. + """ + active: NonNumericAggregatedValues + + """ + Computed aggregate values for the `id` field. + """ + id: NonNumericAggregatedValues +} + +""" +Return type representing a bucket of `Wholesale` documents for an aggregations query. +""" +type WholesaleAggregation { + """ + Provides computed aggregated values over all `Wholesale` documents in an aggregation bucket. + """ + aggregated_values: WholesaleAggregatedValues + + """ + The count of `Wholesale` documents in an aggregation bucket. + """ + count: JsonSafeLong! + + """ + Used to specify the `Wholesale` fields to group by. The returned values identify each aggregation bucket. + """ + grouped_by: WholesaleGroupedBy +} + +""" +Represents a paginated collection of `WholesaleAggregation` results. + +See the [Relay GraphQL Cursor Connections +Specification](https://relay.dev/graphql/connections.htm#sec-Connection-Types) for more info. +""" +type WholesaleAggregationConnection { + """ + Wraps a specific `WholesaleAggregation` to pair it with its pagination cursor. + """ + edges: [WholesaleAggregationEdge!]! + + """ + The list of `WholesaleAggregation` results. + """ + nodes: [WholesaleAggregation!]! + + """ + Provides pagination-related information. + """ + page_info: PageInfo! +} + +""" +Represents a specific `WholesaleAggregation` in the context of a `WholesaleAggregationConnection`, +providing access to both the `WholesaleAggregation` and query-specific information such as the pagination `Cursor`. + +See the [Relay GraphQL Cursor Connections +Specification](https://relay.dev/graphql/connections.htm#sec-Edge-Types) for more info. +""" +type WholesaleAggregationEdge { + """ + The `Cursor` of this `WholesaleAggregation`. This can be passed in the next query as + a `before` or `after` argument to continue paginating from this `WholesaleAggregation`. + """ + cursor: Cursor + + """ + The `WholesaleAggregation` of this edge. + """ + node: WholesaleAggregation +} + +""" +Represents a paginated collection of `Wholesale` results. + +See the [Relay GraphQL Cursor Connections +Specification](https://relay.dev/graphql/connections.htm#sec-Connection-Types) for more info. +""" +type WholesaleConnection { + """ + Wraps a specific `Wholesale` to pair it with its pagination cursor. + """ + edges: [WholesaleEdge!]! + + """ + The list of `Wholesale` results. + """ + nodes: [Wholesale!]! + + """ + Provides pagination-related information. + """ + page_info: PageInfo! + + """ + The total number of edges available in this connection to paginate over. + """ + total_edge_count: JsonSafeLong! +} + +""" +Represents a specific `Wholesale` in the context of a `WholesaleConnection`, +providing access to both the `Wholesale` and query-specific information such as the pagination `Cursor`. + +See the [Relay GraphQL Cursor Connections +Specification](https://relay.dev/graphql/connections.htm#sec-Edge-Types) for more info. +""" +type WholesaleEdge { + """ + All search highlights for this `Wholesale`, indicating where in the indexed document the query matched. + """ + all_highlights: [SearchHighlight!]! + + """ + The `Cursor` of this `Wholesale`. This can be passed in the next query as + a `before` or `after` argument to continue paginating from this `Wholesale`. + """ + cursor: Cursor + + """ + Specific search highlights for this `Wholesale`, providing matching snippets for the requested fields. + """ + highlights: WholesaleHighlights + + """ + The `Wholesale` of this edge. + """ + node: Wholesale +} + +""" +Input type used to specify filters on `Wholesale` fields. + +Will match all documents if passed as an empty object (or as `null`). +""" +input WholesaleFilterInput { + """ + Used to filter on the `active` field. + + When `null` or an empty object is passed, matches all documents. + """ + active: BooleanFilterInput + + """ + Matches records where all of the provided sub-filters evaluate to true. This works just like an AND operator in SQL. + + Note: multiple filters are automatically ANDed together. This is only needed when you have multiple filters that can't + be provided on a single `WholesaleFilterInput` input because of collisions + between key names. For example, if you want to AND multiple + OR'd sub-filters (the equivalent of (A OR B) AND (C OR D)), you could do all_of: [{any_of: [...]}, {any_of: [...]}]. + + When `null` or an empty list is passed, matches all documents. + """ + all_of: [WholesaleFilterInput!] + + """ + Matches records where any of the provided sub-filters evaluate to true. + This works just like an OR operator in SQL. + + When `null` is passed, matches all documents. + When an empty list is passed, this part of the filter matches no documents. + """ + any_of: [WholesaleFilterInput!] + + """ + Used to filter on the `id` field. + + When `null` or an empty object is passed, matches all documents. + """ + id: IDFilterInput + + """ + Matches records where the provided sub-filter evaluates to false. + This works just like a NOT operator in SQL. + + When `null` or an empty object is passed, matches no documents. + """ + not: WholesaleFilterInput +} + +""" +Type used to specify the `Wholesale` fields to group by for aggregations. +""" +type WholesaleGroupedBy { + """ + The `active` field value for this group. + """ + active: Boolean +} + +""" +Type used to request desired `Wholesale` search highlight fields. +""" +type WholesaleHighlights { + """ + Search highlights for the `id`, providing snippets of the matching text. + """ + id: [String!]! +} + +""" +Enumerates the ways `Wholesale`s can be sorted. +""" +enum WholesaleSortOrderInput { + """ + Sorts ascending by the `id` field. + """ + id_ASC + + """ + Sorts descending by the `id` field. + """ + id_DESC +} + """ For more performant queries on this type, please filter on `workspace_id` if possible. """ diff --git a/config/schema/artifacts_with_apollo/json_schemas.yaml b/config/schema/artifacts_with_apollo/json_schemas.yaml index 7f23fd764..64c6f7129 100644 --- a/config/schema/artifacts_with_apollo/json_schemas.yaml +++ b/config/schema/artifacts_with_apollo/json_schemas.yaml @@ -23,8 +23,10 @@ json_schema_version: 1 type: string enum: - Address + - BrokerWholesaler - Company - Component + - DirectWholesaler - ElectricalPart - Manufacturer - MechanicalPart @@ -33,7 +35,6 @@ json_schema_version: 1 - PhysicalStore - Sponsor - Team - - ThirdPartyWholesale - Widget - WidgetWorkspace id: @@ -160,6 +161,24 @@ json_schema_version: 1 - sponsorships_object Boolean: type: boolean + BrokerWholesaler: + type: object + properties: + id: + allOf: + - "$ref": "#/$defs/ID" + - maxLength: 8191 + active: + anyOf: + - "$ref": "#/$defs/Boolean" + - type: 'null' + __typename: + type: string + const: BrokerWholesaler + default: BrokerWholesaler + required: + - id + - active Color: type: string enum: @@ -253,6 +272,24 @@ json_schema_version: 1 DateTime: type: string format: date-time + DirectWholesaler: + type: object + properties: + id: + allOf: + - "$ref": "#/$defs/ID" + - maxLength: 8191 + active: + anyOf: + - "$ref": "#/$defs/Boolean" + - type: 'null' + __typename: + type: string + const: DirectWholesaler + default: DirectWholesaler + required: + - id + - active ElectricalPart: type: object properties: @@ -875,24 +912,6 @@ json_schema_version: 1 - was_shortened - players_nested - players_object - ThirdPartyWholesale: - type: object - properties: - id: - allOf: - - "$ref": "#/$defs/ID" - - maxLength: 8191 - active: - anyOf: - - "$ref": "#/$defs/Boolean" - - type: 'null' - __typename: - type: string - const: ThirdPartyWholesale - default: ThirdPartyWholesale - required: - - id - - active Untyped: type: - array diff --git a/config/schema/artifacts_with_apollo/json_schemas_by_version/v1.yaml b/config/schema/artifacts_with_apollo/json_schemas_by_version/v1.yaml index 3b8798aee..ad09ed3e1 100644 --- a/config/schema/artifacts_with_apollo/json_schemas_by_version/v1.yaml +++ b/config/schema/artifacts_with_apollo/json_schemas_by_version/v1.yaml @@ -23,8 +23,10 @@ json_schema_version: 1 type: string enum: - Address + - BrokerWholesaler - Company - Component + - DirectWholesaler - ElectricalPart - Manufacturer - MechanicalPart @@ -33,7 +35,6 @@ json_schema_version: 1 - PhysicalStore - Sponsor - Team - - ThirdPartyWholesale - Widget - WidgetWorkspace id: @@ -187,6 +188,30 @@ json_schema_version: 1 - sponsorships_object Boolean: type: boolean + BrokerWholesaler: + type: object + properties: + id: + allOf: + - "$ref": "#/$defs/ID" + - maxLength: 8191 + ElasticGraph: + type: ID! + nameInIndex: id + active: + anyOf: + - "$ref": "#/$defs/Boolean" + - type: 'null' + ElasticGraph: + type: Boolean + nameInIndex: active + __typename: + type: string + const: BrokerWholesaler + default: BrokerWholesaler + required: + - id + - active Color: type: string enum: @@ -313,6 +338,30 @@ json_schema_version: 1 DateTime: type: string format: date-time + DirectWholesaler: + type: object + properties: + id: + allOf: + - "$ref": "#/$defs/ID" + - maxLength: 8191 + ElasticGraph: + type: ID! + nameInIndex: id + active: + anyOf: + - "$ref": "#/$defs/Boolean" + - type: 'null' + ElasticGraph: + type: Boolean + nameInIndex: active + __typename: + type: string + const: DirectWholesaler + default: DirectWholesaler + required: + - id + - active ElectricalPart: type: object properties: @@ -1175,30 +1224,6 @@ json_schema_version: 1 - was_shortened - players_nested - players_object - ThirdPartyWholesale: - type: object - properties: - id: - allOf: - - "$ref": "#/$defs/ID" - - maxLength: 8191 - ElasticGraph: - type: ID! - nameInIndex: id - active: - anyOf: - - "$ref": "#/$defs/Boolean" - - type: 'null' - ElasticGraph: - type: Boolean - nameInIndex: active - __typename: - type: string - const: ThirdPartyWholesale - default: ThirdPartyWholesale - required: - - id - - active Untyped: type: - array diff --git a/config/schema/artifacts_with_apollo/runtime_metadata.yaml b/config/schema/artifacts_with_apollo/runtime_metadata.yaml index 8850a029f..d63a77feb 100644 --- a/config/schema/artifacts_with_apollo/runtime_metadata.yaml +++ b/config/schema/artifacts_with_apollo/runtime_metadata.yaml @@ -1061,6 +1061,16 @@ enum_types_by_name: sort_field: direction: desc field_path: league + WholesaleSortOrderInput: + values_by_name: + id_ASC: + sort_field: + direction: asc + field_path: id + id_DESC: + sort_field: + direction: desc + field_path: id WidgetCurrencySortOrderInput: values_by_name: details_symbol_ASC: @@ -3325,6 +3335,38 @@ object_types_by_name: graphql_fields_by_name: count: name_in_index: __counts + BrokerWholesaler: + graphql_fields_by_name: + active: + resolver: + name: get_record_field_value + id: + resolver: + name: get_record_field_value + index_definition_names: + - distribution_channels + update_targets: + - data_params: + __typename: + cardinality: one + active: + cardinality: one + id_source: id + metadata_params: + relationship: + value: __self + sourceId: + cardinality: one + source_path: id + sourceType: + cardinality: one + source_path: type + version: + cardinality: one + relationship: __self + routing_value_source: id + script_id: update_index_data_1fdfaf1c9261c96019decc89b515bd9a + type: BrokerWholesaler ColorListFilterInput: graphql_fields_by_name: count: @@ -3907,6 +3949,38 @@ object_types_by_name: graphql_fields_by_name: count: name_in_index: __counts + DirectWholesaler: + graphql_fields_by_name: + active: + resolver: + name: get_record_field_value + id: + resolver: + name: get_record_field_value + index_definition_names: + - distribution_channels + update_targets: + - data_params: + __typename: + cardinality: one + active: + cardinality: one + id_source: id + metadata_params: + relationship: + value: __self + sourceId: + cardinality: one + source_path: id + sourceType: + cardinality: one + source_path: type + version: + cardinality: one + relationship: __self + routing_value_source: id + script_id: update_index_data_1fdfaf1c9261c96019decc89b515bd9a + type: DirectWholesaler DistributionChannel: graphql_fields_by_name: active: @@ -6351,6 +6425,12 @@ object_types_by_name: teams: resolver: name: list_records + wholesale_aggregations: + resolver: + name: list_records + wholesalers: + resolver: + name: list_records widget_aggregations: resolver: name: list_records @@ -7732,7 +7812,7 @@ object_types_by_name: players_object: resolver: name: object_with_lookahead - ThirdPartyWholesale: + Wholesale: graphql_fields_by_name: active: resolver: @@ -7742,28 +7822,88 @@ object_types_by_name: name: get_record_field_value index_definition_names: - distribution_channels - update_targets: - - data_params: - __typename: - cardinality: one - active: - cardinality: one - id_source: id - metadata_params: - relationship: - value: __self - sourceId: - cardinality: one - source_path: id - sourceType: - cardinality: one - source_path: type - version: - cardinality: one - relationship: __self - routing_value_source: id - script_id: update_index_data_1fdfaf1c9261c96019decc89b515bd9a - type: ThirdPartyWholesale + WholesaleAggregatedValues: + graphql_fields_by_name: + active: + resolver: + name: object_with_lookahead + id: + resolver: + name: object_with_lookahead + WholesaleAggregation: + elasticgraph_category: indexed_aggregation + graphql_fields_by_name: + aggregated_values: + resolver: + name: object_without_lookahead + count: + resolver: + name: object_without_lookahead + grouped_by: + resolver: + name: object_without_lookahead + source_type: Wholesale + WholesaleAggregationConnection: + elasticgraph_category: relay_connection + graphql_fields_by_name: + edges: + resolver: + name: object_without_lookahead + nodes: + resolver: + name: object_without_lookahead + page_info: + resolver: + name: object_without_lookahead + WholesaleAggregationEdge: + elasticgraph_category: relay_edge + graphql_fields_by_name: + cursor: + resolver: + name: object_without_lookahead + node: + resolver: + name: object_without_lookahead + WholesaleConnection: + elasticgraph_category: relay_connection + graphql_fields_by_name: + edges: + resolver: + name: object_without_lookahead + nodes: + resolver: + name: object_without_lookahead + page_info: + resolver: + name: object_without_lookahead + total_edge_count: + resolver: + name: object_without_lookahead + WholesaleEdge: + elasticgraph_category: relay_edge + graphql_fields_by_name: + all_highlights: + resolver: + name: object_without_lookahead + cursor: + resolver: + name: object_without_lookahead + highlights: + resolver: + name: object_without_lookahead + node: + resolver: + name: object_without_lookahead + WholesaleGroupedBy: + graphql_fields_by_name: + active: + resolver: + name: object_with_lookahead + WholesaleHighlights: + graphql_fields_by_name: + id: + resolver: + name: get_record_field_value Widget: graphql_fields_by_name: amount_cents: diff --git a/config/schema/artifacts_with_apollo/schema.graphql b/config/schema/artifacts_with_apollo/schema.graphql index d612486ba..fc033758c 100644 --- a/config/schema/artifacts_with_apollo/schema.graphql +++ b/config/schema/artifacts_with_apollo/schema.graphql @@ -701,6 +701,11 @@ input BooleanListFilterInput { not: BooleanListFilterInput } +type BrokerWholesaler implements DistributionChannel & Wholesale @key(fields: "id") { + active: Boolean + id: ID! +} + enum Color { BLUE GREEN @@ -3011,6 +3016,11 @@ input DayOfWeekGroupingOffsetInput { unit: DateTimeUnitInput! } +type DirectWholesaler implements DistributionChannel & Wholesale @key(fields: "id") { + active: Boolean + id: ID! +} + """ Enumerates the supported distance units. """ @@ -11661,6 +11671,109 @@ type Query { order_by: [TeamSortOrderInput!] ): TeamConnection + """ + Aggregations over the `wholesalers` data: + + > Fetches `Wholesale`s based on the provided arguments. + """ + wholesale_aggregations( + """ + Used to forward-paginate through the `wholesale_aggregations`. When provided, the next page after the + provided cursor will be returned. + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + after: Cursor + + """ + Used to backward-paginate through the `wholesale_aggregations`. When provided, the previous page before the + provided cursor will be returned. + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + before: Cursor + + """ + Used to filter the `Wholesale` documents that get aggregated over based on the provided criteria. + """ + filter: WholesaleFilterInput + + """ + Used in conjunction with the `after` argument to forward-paginate through the `wholesale_aggregations`. + When provided, limits the number of returned results to the first `n` after the provided + `after` cursor (or from the start of the `wholesale_aggregations`, if no `after` cursor is provided). + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + first: Int + + """ + Used in conjunction with the `before` argument to backward-paginate through the `wholesale_aggregations`. + When provided, limits the number of returned results to the last `n` before the provided + `before` cursor (or from the end of the `wholesale_aggregations`, if no `before` cursor is provided). + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + last: Int + ): WholesaleAggregationConnection + + """ + Fetches `Wholesale`s based on the provided arguments. + """ + wholesalers( + """ + Used to forward-paginate through the `wholesalers`. When provided, the next page after the + provided cursor will be returned. + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + after: Cursor + + """ + Used to backward-paginate through the `wholesalers`. When provided, the previous page before the + provided cursor will be returned. + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + before: Cursor + + """ + Used to filter the returned `wholesalers` based on the provided criteria. + """ + filter: WholesaleFilterInput + + """ + Used in conjunction with the `after` argument to forward-paginate through the `wholesalers`. + When provided, limits the number of returned results to the first `n` after the provided + `after` cursor (or from the start of the `wholesalers`, if no `after` cursor is provided). + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + first: Int + + """ + Used in conjunction with the `before` argument to backward-paginate through the `wholesalers`. + When provided, limits the number of returned results to the last `n` before the provided + `before` cursor (or from the end of the `wholesalers`, if no `before` cursor is provided). + + See the [Relay GraphQL Cursor Connections + Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info. + """ + last: Int + + """ + Used to specify how the returned `wholesalers` should be sorted. + """ + order_by: [WholesaleSortOrderInput!] + ): WholesaleConnection + """ Aggregations over the `widgets` data: @@ -16014,11 +16127,6 @@ input TextFilterInput { not: TextFilterInput } -type ThirdPartyWholesale implements DistributionChannel @key(fields: "id") { - active: Boolean - id: ID! -} - """ An [IANA time zone identifier](https://www.iana.org/time-zones), such as `America/Los_Angeles` or `UTC`. @@ -16086,6 +16194,232 @@ input UntypedFilterInput { not: UntypedFilterInput } +interface Wholesale implements DistributionChannel { + active: Boolean + id: ID! +} + +""" +Type used to perform aggregation computations on `Wholesale` fields. +""" +type WholesaleAggregatedValues { + """ + Computed aggregate values for the `active` field. + """ + active: NonNumericAggregatedValues + + """ + Computed aggregate values for the `id` field. + """ + id: NonNumericAggregatedValues +} + +""" +Return type representing a bucket of `Wholesale` documents for an aggregations query. +""" +type WholesaleAggregation { + """ + Provides computed aggregated values over all `Wholesale` documents in an aggregation bucket. + """ + aggregated_values: WholesaleAggregatedValues + + """ + The count of `Wholesale` documents in an aggregation bucket. + """ + count: JsonSafeLong! + + """ + Used to specify the `Wholesale` fields to group by. The returned values identify each aggregation bucket. + """ + grouped_by: WholesaleGroupedBy +} + +""" +Represents a paginated collection of `WholesaleAggregation` results. + +See the [Relay GraphQL Cursor Connections +Specification](https://relay.dev/graphql/connections.htm#sec-Connection-Types) for more info. +""" +type WholesaleAggregationConnection { + """ + Wraps a specific `WholesaleAggregation` to pair it with its pagination cursor. + """ + edges: [WholesaleAggregationEdge!]! + + """ + The list of `WholesaleAggregation` results. + """ + nodes: [WholesaleAggregation!]! + + """ + Provides pagination-related information. + """ + page_info: PageInfo! +} + +""" +Represents a specific `WholesaleAggregation` in the context of a `WholesaleAggregationConnection`, +providing access to both the `WholesaleAggregation` and query-specific information such as the pagination `Cursor`. + +See the [Relay GraphQL Cursor Connections +Specification](https://relay.dev/graphql/connections.htm#sec-Edge-Types) for more info. +""" +type WholesaleAggregationEdge { + """ + The `Cursor` of this `WholesaleAggregation`. This can be passed in the next query as + a `before` or `after` argument to continue paginating from this `WholesaleAggregation`. + """ + cursor: Cursor + + """ + The `WholesaleAggregation` of this edge. + """ + node: WholesaleAggregation +} + +""" +Represents a paginated collection of `Wholesale` results. + +See the [Relay GraphQL Cursor Connections +Specification](https://relay.dev/graphql/connections.htm#sec-Connection-Types) for more info. +""" +type WholesaleConnection { + """ + Wraps a specific `Wholesale` to pair it with its pagination cursor. + """ + edges: [WholesaleEdge!]! + + """ + The list of `Wholesale` results. + """ + nodes: [Wholesale!]! + + """ + Provides pagination-related information. + """ + page_info: PageInfo! + + """ + The total number of edges available in this connection to paginate over. + """ + total_edge_count: JsonSafeLong! +} + +""" +Represents a specific `Wholesale` in the context of a `WholesaleConnection`, +providing access to both the `Wholesale` and query-specific information such as the pagination `Cursor`. + +See the [Relay GraphQL Cursor Connections +Specification](https://relay.dev/graphql/connections.htm#sec-Edge-Types) for more info. +""" +type WholesaleEdge { + """ + All search highlights for this `Wholesale`, indicating where in the indexed document the query matched. + """ + all_highlights: [SearchHighlight!]! + + """ + The `Cursor` of this `Wholesale`. This can be passed in the next query as + a `before` or `after` argument to continue paginating from this `Wholesale`. + """ + cursor: Cursor + + """ + Specific search highlights for this `Wholesale`, providing matching snippets for the requested fields. + """ + highlights: WholesaleHighlights + + """ + The `Wholesale` of this edge. + """ + node: Wholesale +} + +""" +Input type used to specify filters on `Wholesale` fields. + +Will match all documents if passed as an empty object (or as `null`). +""" +input WholesaleFilterInput { + """ + Used to filter on the `active` field. + + When `null` or an empty object is passed, matches all documents. + """ + active: BooleanFilterInput + + """ + Matches records where all of the provided sub-filters evaluate to true. This works just like an AND operator in SQL. + + Note: multiple filters are automatically ANDed together. This is only needed when you have multiple filters that can't + be provided on a single `WholesaleFilterInput` input because of collisions + between key names. For example, if you want to AND multiple + OR'd sub-filters (the equivalent of (A OR B) AND (C OR D)), you could do all_of: [{any_of: [...]}, {any_of: [...]}]. + + When `null` or an empty list is passed, matches all documents. + """ + all_of: [WholesaleFilterInput!] + + """ + Matches records where any of the provided sub-filters evaluate to true. + This works just like an OR operator in SQL. + + When `null` is passed, matches all documents. + When an empty list is passed, this part of the filter matches no documents. + """ + any_of: [WholesaleFilterInput!] + + """ + Used to filter on the `id` field. + + When `null` or an empty object is passed, matches all documents. + """ + id: IDFilterInput + + """ + Matches records where the provided sub-filter evaluates to false. + This works just like a NOT operator in SQL. + + When `null` or an empty object is passed, matches no documents. + """ + not: WholesaleFilterInput +} + +""" +Type used to specify the `Wholesale` fields to group by for aggregations. +""" +type WholesaleGroupedBy { + """ + The `active` field value for this group. + """ + active: Boolean +} + +""" +Type used to request desired `Wholesale` search highlight fields. +""" +type WholesaleHighlights { + """ + Search highlights for the `id`, providing snippets of the matching text. + """ + id: [String!]! +} + +""" +Enumerates the ways `Wholesale`s can be sorted. +""" +enum WholesaleSortOrderInput { + """ + Sorts ascending by the `id` field. + """ + id_ASC + + """ + Sorts descending by the `id` field. + """ + id_DESC +} + """ For more performant queries on this type, please filter on `workspace_id` if possible. """ @@ -20160,7 +20494,7 @@ In an ElasticGraph schema, this is a union of all indexed types. Not intended for use by clients other than Apollo. """ -union _Entity = Company | Component | Country | ElectricalPart | Manufacturer | MechanicalPart | OnlineStore | Person | PhysicalStore | Sponsor | Team | ThirdPartyWholesale | Widget | WidgetCurrency | WidgetWorkspace +union _Entity = BrokerWholesaler | Company | Component | Country | DirectWholesaler | ElectricalPart | Manufacturer | MechanicalPart | OnlineStore | Person | PhysicalStore | Sponsor | Team | Widget | WidgetCurrency | WidgetWorkspace """ An object type required by the [Apollo Federation subgraph diff --git a/config/schema/widgets.rb b/config/schema/widgets.rb index ac15cace3..e50f4490e 100644 --- a/config/schema/widgets.rb +++ b/config/schema/widgets.rb @@ -367,9 +367,24 @@ t.field "established_on", "Date" end - # ThirdPartyWholesale - concrete type in parallel to Retail branch - schema.object_type "ThirdPartyWholesale" do |t| + # Wholesale is a sub-interface of DistributionChannel. Unlike Retail, all its concrete subtypes + # share the distribution_channels index (there are no dedicated indexes in this branch of the + # tree), so __typename filtering for Wholesale queries omits nil. + schema.interface_type "Wholesale" do |t| t.implements "DistributionChannel" + t.root_query_fields plural: "wholesalers" + t.field "id", "ID!" + t.field "active", "Boolean" + end + + schema.object_type "DirectWholesaler" do |t| + t.implements "Wholesale" + t.field "id", "ID!" + t.field "active", "Boolean" + end + + schema.object_type "BrokerWholesaler" do |t| + t.implements "Wholesale" t.field "id", "ID!" t.field "active", "Boolean" end diff --git a/elasticgraph-graphql/lib/elastic_graph/graphql.rb b/elasticgraph-graphql/lib/elastic_graph/graphql.rb index 2f21ab2ec..3700c2502 100644 --- a/elasticgraph-graphql/lib/elastic_graph/graphql.rb +++ b/elasticgraph-graphql/lib/elastic_graph/graphql.rb @@ -192,6 +192,7 @@ def named_graphql_resolvers def datastore_query_adapters @datastore_query_adapters ||= begin require "elastic_graph/graphql/aggregation/query_adapter" + require "elastic_graph/graphql/query_adapter/abstract_type_filter" require "elastic_graph/graphql/query_adapter/filters" require "elastic_graph/graphql/query_adapter/pagination" require "elastic_graph/graphql/query_adapter/sort" @@ -200,6 +201,7 @@ def datastore_query_adapters schema_element_names = runtime_metadata.schema_element_names [ + GraphQL::QueryAdapter::AbstractTypeFilter.new(schema_element_names), GraphQL::QueryAdapter::Pagination.new(schema_element_names: schema_element_names), GraphQL::QueryAdapter::Filters.new( schema_element_names: schema_element_names, diff --git a/elasticgraph-graphql/lib/elastic_graph/graphql/query_adapter/abstract_type_filter.rb b/elasticgraph-graphql/lib/elastic_graph/graphql/query_adapter/abstract_type_filter.rb new file mode 100644 index 000000000..1d8fdf475 --- /dev/null +++ b/elasticgraph-graphql/lib/elastic_graph/graphql/query_adapter/abstract_type_filter.rb @@ -0,0 +1,67 @@ +# 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 + +module ElasticGraph + class GraphQL + class QueryAdapter + # Query adapter that injects a `__typename` filter when querying an abstract type (interface + # or union) that shares an index with types that fall outside the set of its subtypes. Without + # this filter, documents belonging to those other types would incorrectly appear in results. + # + # For example, given this hierarchy: + # + # DistributionChannel (abstract interface, index: distribution_channels) + # ├── Wholesale (abstract interface, distribution_channels index) + # │ ├── DirectWholesaler (concrete, distribution_channels index) + # │ └── BrokerWholesaler (concrete, distribution_channels index) + # └── Retail (abstract interface, distribution_channels index) + # └── Store (abstract interface, distribution_channels index) + # ├── OnlineStore (concrete, distribution_channels index) + # └── PhysicalStore (concrete, physical_stores index — dedicated) + # + # A query for `retailers` (i.e. the `Retail` interface) searches both `distribution_channels` + # and `physical_stores`. Without a `__typename` filter, `DirectWholesaler` and + # `BrokerWholesaler` documents from `distribution_channels` would appear in results. + # So we inject: + # + # __typename: { equal_to_any_of: [nil, "OnlineStore", "PhysicalStore"] } + # + # `nil` is included because `PhysicalStore` has a dedicated index where documents lack + # `__typename` — the index itself identifies the type. We only include `nil` when at least + # one of the queried indexes stores only a single type (and thus lacks `__typename`). + class AbstractTypeFilter + def initialize(schema_element_names) + @equal_to_any_of = schema_element_names.equal_to_any_of + end + + def call(field:, query:, args:, lookahead:, context:) + type = field.type.unwrap_fully + + # For derived types (e.g. indexed aggregations), resolve the underlying document type so we can + # apply the same __typename scoping as we do for document queries. + doc_type = type.source_type || type + + return query unless doc_type.shares_index_with_non_subtypes? + + schema = context.fetch(:elastic_graph_schema) + subtypes = doc_type.subtypes # Note: subtypes returns all concrete subtypes at any depth + typename_values = subtypes.map(&:name) + # Only include nil when at least one queried index stores only a single type — those + # documents lack __typename (the index itself identifies the type), so nil is needed to + # allow them through. + if doc_type.search_index_definitions.any? { |idx| schema.document_types_stored_in(idx.name).size == 1 } + typename_values += [nil] + end + query.merge_with(internal_filters: [{ + "__typename" => {@equal_to_any_of => typename_values} + }]) + end + end + end + end +end diff --git a/elasticgraph-graphql/lib/elastic_graph/graphql/query_adapter/requested_fields.rb b/elasticgraph-graphql/lib/elastic_graph/graphql/query_adapter/requested_fields.rb index 7378616c5..3dc12a4d3 100644 --- a/elasticgraph-graphql/lib/elastic_graph/graphql/query_adapter/requested_fields.rb +++ b/elasticgraph-graphql/lib/elastic_graph/graphql/query_adapter/requested_fields.rb @@ -86,7 +86,10 @@ def requested_fields_under(node, index_field_paths, path_prefix: "") requested_fields_for(child, index_field_paths, path_prefix: path_prefix) end - fields << "#{path_prefix}__typename" if field_for(node.field)&.type&.abstract? + # For abstract types (unions/interfaces), we need __typename to resolve the concrete type. + # We must fully unwrap the type to check the innermost type, since the field type could be + # wrapped in non-null or list wrappers (e.g., `[NamedInventor!]!` on a `nodes` relay connection field). + fields << "#{path_prefix}__typename" if field_for(node.field)&.type&.unwrap_fully&.abstract? fields end diff --git a/elasticgraph-graphql/lib/elastic_graph/graphql/resolvers/graphql_adapter_builder.rb b/elasticgraph-graphql/lib/elastic_graph/graphql/resolvers/graphql_adapter_builder.rb index c167a1486..bbcc2f559 100644 --- a/elasticgraph-graphql/lib/elastic_graph/graphql/resolvers/graphql_adapter_builder.rb +++ b/elasticgraph-graphql/lib/elastic_graph/graphql/resolvers/graphql_adapter_builder.rb @@ -110,8 +110,9 @@ def object_type_hash # In order to support unions and interfaces, we must implement `resolve_type`. def resolve_type(supertype, object, context) schema = context.fetch(:elastic_graph_schema) - # If `__typename` is available, use that to resolve. It should be available on any embedded abstract types... - # (See `Inventor` in `config/schema.graphql` for an example of this kind of type union.) + # If `__typename` is available, use that to resolve. It will be present on embedded abstract + # types, and also on root documents indexed in a shared interface/union index. + # (See `Inventor` in `config/schema/widgets.rb` for an example of an embedded abstract type.) if (typename = object["__typename"]) schema .graphql_schema diff --git a/elasticgraph-graphql/lib/elastic_graph/graphql/schema/type.rb b/elasticgraph-graphql/lib/elastic_graph/graphql/schema/type.rb index f502dc850..4e021a605 100644 --- a/elasticgraph-graphql/lib/elastic_graph/graphql/schema/type.rb +++ b/elasticgraph-graphql/lib/elastic_graph/graphql/schema/type.rb @@ -77,8 +77,14 @@ def search_index_definitions # is NOT a union of `PersonAggregation` and `CompanyAggregation`, so we can't do the same thing on the # indexed aggregation types. Delegating to the source type solves this case. st.search_index_definitions + elsif abstract? + # For abstract types, derive search indexes purely from concrete subtypes. This correctly + # handles cases where subtypes override the abstract type's declared index with a dedicated + # one — only indexes that actually contain documents for this type are searched. + # Note: subtypes returns all concrete subtypes at any depth, so no explicit recursion is needed. + subtypes.flat_map(&:search_index_definitions).to_set else - @index_definitions.union(subtypes.flat_map(&:search_index_definitions)) + @index_definitions end end @@ -135,19 +141,17 @@ def source_type @source_type = @object_runtime_metadata&.source_type&.then { |st| @schema.type_named(st) } end - # Returns the set of concrete (non-abstract) indexed document types that share any of this - # type's search indexes but are not subtypes of this type. Used to determine whether a - # `__typename` filter is needed when querying an abstract type. - # - # Abstract types are excluded because documents in the datastore are always associated - # with a concrete `__typename`. When filtering by `__typename`, only concrete types are - # relevant. - def concrete_non_subtypes_in_shared_index - @concrete_non_subtypes_in_shared_index ||= - search_index_definitions - .flat_map { |index_def| @schema.document_types_stored_in(index_def.name).to_a } - .reject { |t| t == self || subtypes.include?(t) || t.abstract? } - .to_set + # Returns true if any of this type's search indexes contain any concrete document types + # that are not subtypes of this type. Used to determine whether a `__typename` filter is + # needed when querying an abstract type. + def shares_index_with_non_subtypes? + return @shares_index_with_non_subtypes if defined?(@shares_index_with_non_subtypes) + @shares_index_with_non_subtypes = + search_index_definitions.any? do |index_def| + @schema.document_types_stored_in(index_def.name).any? do |t| + t != self && !subtypes.include?(t) && !t.abstract? + end + end end def field_named(field_name) diff --git a/elasticgraph-graphql/sig/elastic_graph/graphql/query_adapter/abstract_type_filter.rbs b/elasticgraph-graphql/sig/elastic_graph/graphql/query_adapter/abstract_type_filter.rbs new file mode 100644 index 000000000..629141a41 --- /dev/null +++ b/elasticgraph-graphql/sig/elastic_graph/graphql/query_adapter/abstract_type_filter.rbs @@ -0,0 +1,13 @@ +module ElasticGraph + class GraphQL + class QueryAdapter + class AbstractTypeFilter + include _QueryAdapter + + def initialize: (SchemaArtifacts::RuntimeMetadata::SchemaElementNames) -> void + + @equal_to_any_of: ::String + end + end + end +end diff --git a/elasticgraph-graphql/sig/elastic_graph/graphql/schema/type.rbs b/elasticgraph-graphql/sig/elastic_graph/graphql/schema/type.rbs index 1436d393d..834874f5e 100644 --- a/elasticgraph-graphql/sig/elastic_graph/graphql/schema/type.rbs +++ b/elasticgraph-graphql/sig/elastic_graph/graphql/schema/type.rbs @@ -11,7 +11,7 @@ module ElasticGraph def search_index_definitions: () -> ::Array[DatastoreCore::_IndexDefinition] def source_type: () -> Type? def subtypes: () -> ::Set[Type] - def concrete_non_subtypes_in_shared_index: () -> ::Set[Type] + def shares_index_with_non_subtypes?: () -> bool def unwrap_fully: () -> Type def field_named: (::String) -> Field def fields_by_name_in_index: () -> ::Hash[::String, ::Array[Field]] diff --git a/elasticgraph-graphql/spec/acceptance/hidden_types_spec.rb b/elasticgraph-graphql/spec/acceptance/hidden_types_spec.rb index c630e2103..519bccab6 100644 --- a/elasticgraph-graphql/spec/acceptance/hidden_types_spec.rb +++ b/elasticgraph-graphql/spec/acceptance/hidden_types_spec.rb @@ -84,6 +84,7 @@ module ElasticGraph all_types_related_to("NamedInventor") + all_types_related_to("DistributionChannel") + all_types_related_to("Retail") + + all_types_related_to("Wholesale") + all_types_related_to("Store") + all_types_related_to("PhysicalStore") + relay_types_related_to("String", include_list_filter: true) - ["StringSortOrderInput"] + @@ -120,7 +121,7 @@ module ElasticGraph %w[ FloatAggregatedValues IntAggregatedValues JsonSafeLongAggregatedValues LongStringAggregatedValues NonNumericAggregatedValues DateAggregatedValues DateTimeAggregatedValues LocalTimeAggregatedValues - OnlineStore ThirdPartyWholesale + OnlineStore DirectWholesaler BrokerWholesaler Cursor PageInfo Query TextFilterInput GeoLocation DateTimeGroupingOffsetInput DateTimeUnitInput DateTimeTimeOfDayFilterInput DateGroupedBy DateGroupingOffsetInput DateGroupingTruncationUnitInput DateUnitInput diff --git a/elasticgraph-graphql/spec/acceptance/search_spec.rb b/elasticgraph-graphql/spec/acceptance/search_spec.rb index 3e1d9d16d..9f7474c36 100644 --- a/elasticgraph-graphql/spec/acceptance/search_spec.rb +++ b/elasticgraph-graphql/spec/acceptance/search_spec.rb @@ -706,7 +706,7 @@ module ElasticGraph index_records( online_store = build(:online_store, name: "Example Marketplace"), build(:online_store, name: "Other Store"), - build(:third_party_wholesale) + build(:direct_wholesaler) ) highlights_by_id = query_all_highlights("retailers", filter: { diff --git a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/query_adapter/abstract_type_filter_spec.rb b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/query_adapter/abstract_type_filter_spec.rb new file mode 100644 index 000000000..4f077ab3d --- /dev/null +++ b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/query_adapter/abstract_type_filter_spec.rb @@ -0,0 +1,69 @@ +# 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/graphql/query_adapter/abstract_type_filter" + +module ElasticGraph + class GraphQL + class QueryAdapter + RSpec.describe AbstractTypeFilter, :query_adapter do + context "when querying a concrete type" do + it "does not apply a __typename filter" do + query = datastore_query_for(:Query, :widgets, "query { widgets { edges { node { id } } } }") + + expect(typename_filter_from(query)).to be_nil + end + end + + context "when querying an abstract type with a shared index but all types in that index are subtypes" do + it "does not apply a __typename filter" do + query = datastore_query_for(:Query, :distribution_channels, "query { distribution_channels { edges { node { id } } } }") + + expect(typename_filter_from(query)).to be_nil + end + end + + context "when querying an abstract type that shares a search index with a non-subtype" do + it "applies a __typename filter scoped to the queried type's concrete subtypes, including nil for subtypes with dedicated indexes" do + query = datastore_query_for(:Query, :retailers, "query { retailers { edges { node { id } } } }") + + expect(typename_filter_from(query)).to contain_exactly(nil, "OnlineStore", "PhysicalStore") + end + + it "applies a __typename filter on aggregations of this kind of abstract type" do + query = datastore_query_for(:Query, :retail_aggregations, "query { retail_aggregations { nodes { count } } }") + + expect(typename_filter_from(query)).to contain_exactly(nil, "OnlineStore", "PhysicalStore") + end + + it "omits nil from the __typename filter when all queried indexes store multiple types" do + query = datastore_query_for(:Query, :wholesalers, "query { wholesalers { edges { node { id } } } }") + + expect(typename_filter_from(query)).to contain_exactly("DirectWholesaler", "BrokerWholesaler") + end + end + + private + + def datastore_query_for(type, field, graphql_query) + super( + schema_artifacts: stock_schema_artifacts, + graphql_query: graphql_query, + type: type, + field: field + ) + end + + def typename_filter_from(query) + filter = query.internal_filters.find { |f| f.key?("__typename") } + filter&.dig("__typename", "equal_to_any_of") + end + end + end + end +end diff --git a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/query_adapter/requested_fields_spec.rb b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/query_adapter/requested_fields_spec.rb index b94381d27..3615315b7 100644 --- a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/query_adapter/requested_fields_spec.rb +++ b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/query_adapter/requested_fields_spec.rb @@ -43,7 +43,30 @@ class QueryAdapter t.field "name", "String" end - # Indexed interfae type. + # Indexed interface type used solely as a root document type (not embedded anywhere). + # Author and Scientist inherit the `creators` index via index inheritance rather than + # declaring it themselves. + schema.interface_type "Creator" do |t| + t.field "id", "ID!" + t.field "name", "String" + t.index "creators" + end + + schema.object_type "Author" do |t| + t.implements "Creator" + t.field "id", "ID!" + t.field "name", "String" + t.field "genre", "String" + end + + schema.object_type "Scientist" do |t| + t.implements "Creator" + t.field "id", "ID!" + t.field "name", "String" + t.field "field_of_study", "String" + end + + # Indexed interface type where each subtype has its own dedicated index. schema.interface_type "NamedEntity" do |t| t.root_query_fields plural: "named_entities" t.field "id", "ID" @@ -85,6 +108,7 @@ class QueryAdapter t.index "electrical_parts" end + # Union type where each member has its own dedicated index. schema.union_type "Part" do |t| t.subtypes "MechanicalPart", "ElectricalPart" end @@ -369,6 +393,21 @@ class QueryAdapter expect(query.request_all_highlights).to be false end + it "requests __typename when using `nodes` on an abstract indexed type" do + query = datastore_query_for(:Query, :creators, <<~QUERY) + query { + creators { + nodes { + ... on Author { genre } + ... on Scientist { field_of_study } + } + } + } + QUERY + + expect(query.requested_fields).to contain_exactly("genre", "field_of_study", "__typename") + end + it "ignores relay connection sub-fields that are not directly under `edges.node` (e.g. `page_info`)" do query = datastore_query_for(:Query, :widgets, <<~QUERY) query { diff --git a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/type_spec.rb b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/type_spec.rb index 4a1209326..48dc10135 100644 --- a/elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/type_spec.rb +++ b/elasticgraph-graphql/spec/unit/elastic_graph/graphql/schema/type_spec.rb @@ -646,7 +646,7 @@ def type_for(field_name) expect(search_index_definitions.map(&:name)).to eq ["things"] end - it "includes the index definitions from the subtypes when it is a type union of indexed document types" do + it "excludes the declared index of an abstract type when all concrete subtypes have overridden it with dedicated indexes" do search_index_definitions = search_index_definitions_from do |schema, type| schema.object_type "T1" do |t| t.field "id", "ID!" @@ -674,7 +674,7 @@ def type_for(field_name) end end - expect(search_index_definitions.map(&:name)).to contain_exactly("t1", "t2", "t3", "t4", "union_index") + expect(search_index_definitions.map(&:name)).to contain_exactly("t1", "t2", "t3", "t4") end it "deduplicates the index definitions before returning them" do @@ -793,7 +793,7 @@ def search_index_definitions_from(type_name: "TheType") end end - describe "#concrete_non_subtypes_in_shared_index" do + describe "#shares_index_with_non_subtypes?" do attr_reader :schema before(:context) do @@ -835,24 +835,22 @@ def search_index_definitions_from(type_name: "TheType") end end - it "excludes the type itself and its subtypes, returning only concrete sibling types in the shared index" do - expect(schema.type_named("Store").concrete_non_subtypes_in_shared_index).to contain_exactly( - schema.type_named("Wholesaler"), - schema.type_named("Distributor") - ) + it "returns true when the type shares an index with concrete non-subtype types" do + expect(schema.type_named("Store").shares_index_with_non_subtypes?).to be true end - it "excludes the type itself even when the type is a concrete type in a shared index" do - # Wholesaler is a concrete type stored in the "channels" index, so it appears in - # document_types_stored_in("channels"). Without the `t == self` guard it would include itself. - expect(schema.type_named("Wholesaler").concrete_non_subtypes_in_shared_index).to contain_exactly( - schema.type_named("Distributor"), - schema.type_named("OnlineStore") - ) + it "returns true for a concrete type that shares an index with other concrete types" do + # Wholesaler is a concrete type stored in the "channels" index alongside Distributor and + # OnlineStore, which are not its subtypes. + expect(schema.type_named("Wholesaler").shares_index_with_non_subtypes?).to be true end - it "returns an empty set when all types sharing its indexes are subtypes" do - expect(schema.type_named("Channel").concrete_non_subtypes_in_shared_index).to be_empty + it "returns false when all types sharing its indexes are subtypes" do + expect(schema.type_named("Channel").shares_index_with_non_subtypes?).to be false + end + + it "returns false for a type with its own dedicated index" do + expect(schema.type_named("PhysicalStore").shares_index_with_non_subtypes?).to be false end end diff --git a/spec_support/lib/elastic_graph/spec_support/factories/widgets.rb b/spec_support/lib/elastic_graph/spec_support/factories/widgets.rb index 896dd6cb6..0c0032892 100644 --- a/spec_support/lib/elastic_graph/spec_support/factories/widgets.rb +++ b/spec_support/lib/elastic_graph/spec_support/factories/widgets.rb @@ -224,8 +224,13 @@ active { true } end - factory :third_party_wholesale, parent: :indexed_type do - __typename { "ThirdPartyWholesale" } + factory :direct_wholesaler, parent: :indexed_type do + __typename { "DirectWholesaler" } + active { true } + end + + factory :broker_wholesaler, parent: :indexed_type do + __typename { "BrokerWholesaler" } active { true } end end