From 0c8746560969d84c7c6eedc5883cb620fee172c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hornych?= Date: Mon, 11 Mar 2024 14:07:03 +0100 Subject: [PATCH] fix(#486): fix behaviour of `multiplePricesForSaleAvailable`, leaving out duplicate prices --- .../allPricesForSaleField.graphql.json.md | 200 +++++++++--------- .../user/en/query/requirements/fetching.md | 33 ++- .../CatalogDataApiGraphQLSchemaBuilder.java | 2 + .../model/GetEntityHeaderDescriptor.java | 11 +- .../dataFetcher/GetEntityDataFetcher.java | 9 +- .../ListUnknownEntitiesDataFetcher.java | 3 +- ...iplePricesForSaleAvailableDataFetcher.java | 48 ++++- .../endpoint/QueryEntitiesHandler.java | 8 +- .../GenericEntityJsonSerializer.java | 15 +- .../DeleteEntitiesByQueryHandler.java | 8 +- .../resolver/endpoint/EntityHandler.java | 8 +- .../endpoint/ListEntitiesHandler.java | 8 +- .../endpoint/ListUnknownEntitiesHandler.java | 8 +- .../endpoint/QueryEntitiesHandler.java | 8 +- .../serializer/EntityJsonSerializer.java | 106 +++++++--- .../EntitySerializationContext.java | 44 ++++ .../ExtraResultsJsonSerializer.java | 4 +- 17 files changed, 345 insertions(+), 178 deletions(-) create mode 100644 evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/serializer/EntitySerializationContext.java diff --git a/documentation/user/en/query/requirements/examples/fetching/allPricesForSaleField.graphql.json.md b/documentation/user/en/query/requirements/examples/fetching/allPricesForSaleField.graphql.json.md index 8b5921b3d..06f88bc06 100644 --- a/documentation/user/en/query/requirements/examples/fetching/allPricesForSaleField.graphql.json.md +++ b/documentation/user/en/query/requirements/examples/fetching/allPricesForSaleField.graphql.json.md @@ -1,106 +1,106 @@ ```json { "data": { - "queryProduct": { - "recordPage": { - "data": [ - { - "primaryKey": 103911, - "attributes": { - "code": "apple-ipad-10-2-10th-generation-2022" - }, - "priceForSale": { - "priceWithoutTax": "395.87", - "priceWithTax": "479.0", - "taxRate": "21.0" - }, - "allPricesForSale": [ - { - "priceWithoutTax": "412.4", - "priceWithTax": "499.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "412.4", - "priceWithTax": "499.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "436.36", - "priceWithTax": "528.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "436.36", - "priceWithTax": "528.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "436.36", - "priceWithTax": "528.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "412.4", - "priceWithTax": "499.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "412.4", - "priceWithTax": "499.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "395.87", - "priceWithTax": "479.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "436.36", - "priceWithTax": "528.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "395.87", - "priceWithTax": "479.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "412.4", - "priceWithTax": "499.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "395.87", - "priceWithTax": "479.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "412.4", - "priceWithTax": "499.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "395.87", - "priceWithTax": "479.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "412.4", - "priceWithTax": "499.0", - "taxRate": "21.0" - }, - { - "priceWithoutTax": "412.4", - "priceWithTax": "499.0", - "taxRate": "21.0" - } - ], - "multiplePricesForSaleAvailable": true - } - ] - } - } + "queryProduct": { + "recordPage": { + "data": [ + { + "primaryKey": 103911, + "attributes": { + "code": "apple-ipad-10-2-10th-generation-2022" + }, + "priceForSale": { + "priceWithoutTax": "395.87", + "priceWithTax": "479.0", + "taxRate": "21.0" + }, + "allPricesForSale": [ + { + "priceWithoutTax": "395.87", + "priceWithTax": "479.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "395.87", + "priceWithTax": "479.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "395.87", + "priceWithTax": "479.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "395.87", + "priceWithTax": "479.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "412.4", + "priceWithTax": "499.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "412.4", + "priceWithTax": "499.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "412.4", + "priceWithTax": "499.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "412.4", + "priceWithTax": "499.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "412.4", + "priceWithTax": "499.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "412.4", + "priceWithTax": "499.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "412.4", + "priceWithTax": "499.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "412.4", + "priceWithTax": "499.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "436.36", + "priceWithTax": "528.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "436.36", + "priceWithTax": "528.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "436.36", + "priceWithTax": "528.0", + "taxRate": "21.0" + }, + { + "priceWithoutTax": "436.36", + "priceWithTax": "528.0", + "taxRate": "21.0" + } + ], + "multiplePricesForSaleAvailable": true + } + ] + } + } } } ``` \ No newline at end of file diff --git a/documentation/user/en/query/requirements/fetching.md b/documentation/user/en/query/requirements/fetching.md index 75ef68950..f6f72f94f 100644 --- a/documentation/user/en/query/requirements/fetching.md +++ b/documentation/user/en/query/requirements/fetching.md @@ -944,9 +944,13 @@ still want to fetch them to display in the UI for the user. -For entities that have either `FIRST_OCCURENCE` or `SUM` inner record handling with prices -for multiple referenced entities grouped by the `innerRecordId`, the `multiplePricesForSaleAvailable` field will be returned -indicating whether there are multiple prices for sale available or just one. +For entities that have either `FIRST_OCCURENCE` or `SUM` inner record handling, the `multiplePricesForSaleAvailable` +property is returned, indicating whether there are multiple _unique_ prices for sale (grouped by the `innerRecordId`). +It is important to note that it doesn't simply return the count of all prices for sale. +Instead, it uses the [`priceType`](../requirements/price.md#price-type) constraint to determine the uniqueness of each +price value. This means that even if there are, say, 3 prices for sale, but they all have the same value, this property +will return `false`. This is especially useful for the +UI to determine whether to display a price range or just a single price without having to fetch all prices for sale. @@ -1152,13 +1156,22 @@ As you can see, the price for sale matching the custom arguments is returned. -Similarly, you can use `allPricesForSale`, which is almost the same as `priceForSale`, but returns all prices for the -sale of the entity. This makes sense if an entity has either `FIRST_OCCURENCE` or `SUM` inner record handling with prices -for multiple referenced entities grouped by the `innerRecordId`. In the case of the `priceForSale` field, only the lowest -price for sale would be returned, but this may not be sufficient if you want to display more complex pricing for the entity. -You can use the `allPricesForSale` field to get all these prices and display more complex information. You can also -use the simpler `multiplePricesForSaleAvailable` field to simply check if there are multiple prices for sale available or -just one without fetching the actual price objects. +Similarly, you can use `allPricesForSale`, which is almost the same as `priceForSale`, but returns all prices for sale +of the entity grouped by the `innerRecordId`. This usually only makes sense for master products with variants +(i.e. `FIRST_OCCURENCE` inner record handling) where the master product has prices for all of its variants, where you may +want to know (and display) prices for sale for each variant (or some kind of range). For the `NONE` inner record handling, +this will always return at most the actual single price for sale. For the `SUM` inner record handling, this will return prices for sale +for each `innerRecordId` in the same way as for the `FIRST_OCCURNCE`, but the use cases are limited. + +The returned list of prices is sorted by the price value from the lowest to the highest depending on the [`priceType`](../requirements/price.md#price-type) +constraint used. + +There is also the simpler `multiplePricesForSaleAvailable` field, which returns a boolean indicating whether there are +multiple _unique_ prices for sale. It is important to note that it doesn't simply return the count of `allPricesForSale`. +Instead, it uses the [`priceType`](../requirements/price.md#price-type) constraint to determine the uniqueness of each +price value. This means that even if there are, say, 3 prices for sale, but they all have the same value, this field +will return `false` (as opposed to `allPricesForSale`, which would return all prices). This is especially useful for the +UI to determine whether to display a price range or just a single price without having to fetch all prices for sale. diff --git a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogDataApiGraphQLSchemaBuilder.java b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogDataApiGraphQLSchemaBuilder.java index 24c87ecdc..e9c0f08fd 100644 --- a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogDataApiGraphQLSchemaBuilder.java +++ b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/CatalogDataApiGraphQLSchemaBuilder.java @@ -392,6 +392,8 @@ private BuiltFieldDescriptor buildGetEntityField(@Nonnull CollectionGraphQLSchem .argument(GetEntityHeaderDescriptor.PRICE_VALID_IN .to(argumentBuilderTransformer)) .argument(GetEntityHeaderDescriptor.PRICE_VALID_NOW + .to(argumentBuilderTransformer)) + .argument(GetEntityHeaderDescriptor.PRICE_TYPE .to(argumentBuilderTransformer)); } diff --git a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/model/GetEntityHeaderDescriptor.java b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/model/GetEntityHeaderDescriptor.java index 6ece690a9..b7ef0761e 100644 --- a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/model/GetEntityHeaderDescriptor.java +++ b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/model/GetEntityHeaderDescriptor.java @@ -6,7 +6,7 @@ * | __/\ V /| | || (_| | |_| | |_) | * \___| \_/ |_|\__\__,_|____/|____/ * - * Copyright (c) 2023 + * Copyright (c) 2023-2024 * * Licensed under the Business Source License, Version 1.1 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ package io.evitadb.externalApi.graphql.api.catalog.dataApi.model; +import io.evitadb.api.query.require.QueryPriceMode; import io.evitadb.externalApi.api.catalog.dataApi.model.CatalogDataApiRootDescriptor; import io.evitadb.externalApi.api.model.PropertyDescriptor; @@ -82,4 +83,12 @@ public interface GetEntityHeaderDescriptor { """) .type(nullable(Boolean.class)) .build(); + PropertyDescriptor PRICE_TYPE = PropertyDescriptor.builder() + .name("priceType") + .description(""" + Parameter specifying which price type (with/without tax) will be used for handling filtering and sorting constraints. + By default, the "with tax" price type is used. + """) + .type(nullable(QueryPriceMode.class)) + .build(); } diff --git a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/GetEntityDataFetcher.java b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/GetEntityDataFetcher.java index 34c195f83..b3cde53c5 100644 --- a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/GetEntityDataFetcher.java +++ b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/GetEntityDataFetcher.java @@ -6,7 +6,7 @@ * | __/\ V /| | || (_| | |_| | |_) | * \___| \_/ |_|\__\__,_|____/|____/ * - * Copyright (c) 2023 + * Copyright (c) 2023-2024 * * Licensed under the Business Source License, Version 1.1 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import io.evitadb.api.query.Query; import io.evitadb.api.query.filter.FilterBy; import io.evitadb.api.query.require.EntityFetch; +import io.evitadb.api.query.require.QueryPriceMode; import io.evitadb.api.query.require.Require; import io.evitadb.api.requestResponse.data.EntityClassifier; import io.evitadb.api.requestResponse.data.structure.EntityDecorator; @@ -166,7 +167,7 @@ private Require buildRequire(@Nonnull DataFetchingEnvironment environment, @Nonn ) .orElse(null); - return require(entityFetch); + return require(entityFetch, priceType(arguments.priceType())); } @Nonnull @@ -189,6 +190,7 @@ private record Arguments(@Nullable Integer primaryKey, @Nullable String[] priceInPriceLists, @Nullable OffsetDateTime priceValidIn, boolean priceValidInNow, + @Nonnull QueryPriceMode priceType, @Nonnull Map uniqueAttributes) { private static Arguments from(@Nonnull DataFetchingEnvironment environment, @Nonnull EntitySchemaContract entitySchema) { @@ -205,6 +207,8 @@ private static Arguments from(@Nonnull DataFetchingEnvironment environment, @Non final OffsetDateTime priceValidIn = (OffsetDateTime) arguments.remove(GetEntityHeaderDescriptor.PRICE_VALID_IN.name()); final boolean priceValidInNow = (boolean) Optional.ofNullable(arguments.remove(GetEntityHeaderDescriptor.PRICE_VALID_NOW.name())) .orElse(false); + final QueryPriceMode priceType = (QueryPriceMode) Optional.ofNullable(arguments.remove(GetEntityHeaderDescriptor.PRICE_TYPE.name())) + .orElse(QueryPriceMode.WITH_TAX); // left over arguments are unique attribute filters as defined by schema final Map uniqueAttributes = extractUniqueAttributesFromArguments(arguments, entitySchema); @@ -223,6 +227,7 @@ private static Arguments from(@Nonnull DataFetchingEnvironment environment, @Non (priceInPriceLists != null ? priceInPriceLists.toArray(String[]::new) : null), priceValidIn, priceValidInNow, + priceType, uniqueAttributes ); } diff --git a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/ListUnknownEntitiesDataFetcher.java b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/ListUnknownEntitiesDataFetcher.java index 8b77ac481..0507db157 100644 --- a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/ListUnknownEntitiesDataFetcher.java +++ b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/ListUnknownEntitiesDataFetcher.java @@ -6,7 +6,7 @@ * | __/\ V /| | || (_| | |_| | |_) | * \___| \_/ |_|\__\__,_|____/|____/ * - * Copyright (c) 2023 + * Copyright (c) 2023-2024 * * Licensed under the Business Source License, Version 1.1 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,6 @@ import io.evitadb.api.query.require.EntityContentRequire; import io.evitadb.api.query.require.EntityFetch; import io.evitadb.api.query.require.Require; -import io.evitadb.api.requestResponse.data.EntityClassifier; import io.evitadb.api.requestResponse.data.SealedEntity; import io.evitadb.api.requestResponse.data.structure.EntityDecorator; import io.evitadb.api.requestResponse.data.structure.EntityReference; diff --git a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/entity/MultiplePricesForSaleAvailableDataFetcher.java b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/entity/MultiplePricesForSaleAvailableDataFetcher.java index fad545237..7ffc3e0c1 100644 --- a/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/entity/MultiplePricesForSaleAvailableDataFetcher.java +++ b/evita_external_api/evita_external_api_graphql/src/main/java/io/evitadb/externalApi/graphql/api/catalog/dataApi/resolver/dataFetcher/entity/MultiplePricesForSaleAvailableDataFetcher.java @@ -23,29 +23,63 @@ package io.evitadb.externalApi.graphql.api.catalog.dataApi.resolver.dataFetcher.entity; -import graphql.execution.DataFetcherResult; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import io.evitadb.api.query.require.QueryPriceMode; import io.evitadb.api.requestResponse.data.PriceContract; +import io.evitadb.api.requestResponse.data.PriceInnerRecordHandling; import io.evitadb.api.requestResponse.data.structure.EntityDecorator; +import io.evitadb.externalApi.graphql.api.catalog.dataApi.resolver.dataFetcher.EntityQueryContext; +import io.evitadb.externalApi.graphql.exception.GraphQLInternalError; import javax.annotation.Nonnull; import java.util.List; /** - * Finds out whether there are multiple prices which the entity could be sold for. + * Finds out whether there are multiple unique prices which the entity could be sold for. * * @author Lukáš Hornych, FG Forrest a.s. (c) 2022 */ -public class MultiplePricesForSaleAvailableDataFetcher implements DataFetcher> { +public class MultiplePricesForSaleAvailableDataFetcher implements DataFetcher { @Nonnull @Override - public DataFetcherResult get(@Nonnull DataFetchingEnvironment environment) throws Exception { + public Boolean get(@Nonnull DataFetchingEnvironment environment) throws Exception { final EntityDecorator entity = environment.getSource(); + final EntityQueryContext context = environment.getLocalContext(); + final List allPricesForSale = entity.getAllPricesForSale(); - return DataFetcherResult.newResult() - .data(allPricesForSale.size() > 1) - .build(); + if (allPricesForSale.size() <= 1) { + return false; + } + + final boolean hasMultiplePricesForSale; + final PriceInnerRecordHandling priceInnerRecordHandling = entity.getPriceInnerRecordHandling(); + if (priceInnerRecordHandling.equals(PriceInnerRecordHandling.FIRST_OCCURRENCE)) { + if (allPricesForSale.size() <= 1) { + return false; + } + + final QueryPriceMode desiredPriceType = entity.getPricePredicate().getQueryPriceMode(); + final long uniquePriceValuesCount = allPricesForSale.stream() + .map(price -> { + if (desiredPriceType.equals(QueryPriceMode.WITH_TAX)) { + return price.priceWithTax(); + } else if (desiredPriceType.equals(QueryPriceMode.WITHOUT_TAX)) { + return price.priceWithoutTax(); + } else { + throw new GraphQLInternalError("Unsupported price type `" + desiredPriceType + "`"); + } + }) + .distinct() + .count(); + hasMultiplePricesForSale = uniquePriceValuesCount > 1; + } else if (priceInnerRecordHandling.equals(PriceInnerRecordHandling.SUM)) { + hasMultiplePricesForSale = allPricesForSale.size() > 1; + } else { + hasMultiplePricesForSale = false; + } + + return hasMultiplePricesForSale; } } diff --git a/evita_external_api/evita_external_api_lab/src/main/java/io/evitadb/externalApi/lab/api/resolver/endpoint/QueryEntitiesHandler.java b/evita_external_api/evita_external_api_lab/src/main/java/io/evitadb/externalApi/lab/api/resolver/endpoint/QueryEntitiesHandler.java index 3ac5cfc0d..04b9650f4 100644 --- a/evita_external_api/evita_external_api_lab/src/main/java/io/evitadb/externalApi/lab/api/resolver/endpoint/QueryEntitiesHandler.java +++ b/evita_external_api/evita_external_api_lab/src/main/java/io/evitadb/externalApi/lab/api/resolver/endpoint/QueryEntitiesHandler.java @@ -6,7 +6,7 @@ * | __/\ V /| | || (_| | |_| | |_) | * \___| \_/ |_|\__\__,_|____/|____/ * - * Copyright (c) 2023 + * Copyright (c) 2023-2024 * * Licensed under the Business Source License, Version 1.1 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ import io.evitadb.externalApi.rest.api.catalog.dataApi.dto.QueryResponse; import io.evitadb.externalApi.rest.api.catalog.dataApi.dto.QueryResponse.QueryResponseBuilder; import io.evitadb.externalApi.rest.api.catalog.dataApi.dto.StripListDto; +import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntitySerializationContext; import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.ExtraResultsJsonSerializer; import io.evitadb.externalApi.rest.exception.RestInternalError; import io.evitadb.externalApi.rest.io.JsonRestHandler; @@ -156,15 +157,16 @@ protected Object convertResultIntoSerializableObject(@Nonnull RestEndpointExchan private DataChunkDto serializeRecordPage(@Nonnull RestEndpointExchange exchange, @Nonnull EvitaResponse response) { final DataChunk recordPage = response.getRecordPage(); + final EntitySerializationContext serializationContext = new EntitySerializationContext(exchange.session().getCatalogSchema()); if (recordPage instanceof PaginatedList paginatedList) { return new PaginatedListDto( paginatedList, - entityJsonSerializer.serialize(paginatedList.getData(), exchange.session().getCatalogSchema()) + entityJsonSerializer.serialize(serializationContext, paginatedList.getData()) ); } else if (recordPage instanceof StripList stripList) { return new StripListDto( stripList, - entityJsonSerializer.serialize(stripList.getData(), exchange.session().getCatalogSchema()) + entityJsonSerializer.serialize(serializationContext, stripList.getData()) ); } else { throw new RestInternalError("Unsupported data chunk type `" + recordPage.getClass().getName() + "`."); diff --git a/evita_external_api/evita_external_api_lab/src/main/java/io/evitadb/externalApi/lab/api/resolver/serializer/GenericEntityJsonSerializer.java b/evita_external_api/evita_external_api_lab/src/main/java/io/evitadb/externalApi/lab/api/resolver/serializer/GenericEntityJsonSerializer.java index f5b9206fd..c7b1338d5 100644 --- a/evita_external_api/evita_external_api_lab/src/main/java/io/evitadb/externalApi/lab/api/resolver/serializer/GenericEntityJsonSerializer.java +++ b/evita_external_api/evita_external_api_lab/src/main/java/io/evitadb/externalApi/lab/api/resolver/serializer/GenericEntityJsonSerializer.java @@ -6,7 +6,7 @@ * | __/\ V /| | || (_| | |_| | |_) | * \___| \_/ |_|\__\__,_|____/|____/ * - * Copyright (c) 2023 + * Copyright (c) 2023-2024 * * Licensed under the Business Source License, Version 1.1 (the "License"); * you may not use this file except in compliance with the License. @@ -25,13 +25,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import io.evitadb.api.requestResponse.data.EntityContract; import io.evitadb.api.requestResponse.data.ReferenceContract; -import io.evitadb.api.requestResponse.schema.CatalogSchemaContract; +import io.evitadb.api.requestResponse.data.structure.EntityDecorator; import io.evitadb.api.requestResponse.schema.EntitySchemaContract; import io.evitadb.externalApi.lab.api.model.entity.GenericEntityDescriptor; import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntityJsonSerializer; -import io.evitadb.externalApi.rest.io.RestHandlingContext; +import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntitySerializationContext; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nonnull; @@ -51,9 +50,9 @@ public GenericEntityJsonSerializer(@Nonnull ObjectMapper objectMapper) { } @Override - protected void serializeReferences(@Nonnull ObjectNode rootNode, - @Nonnull EntityContract entity, - @Nonnull CatalogSchemaContract catalogSchema, + protected void serializeReferences(@Nonnull EntitySerializationContext ctx, + @Nonnull ObjectNode rootNode, + @Nonnull EntityDecorator entity, @Nonnull EntitySchemaContract entitySchema) { if (entity.referencesAvailable() && !entity.getReferences().isEmpty()) { final ObjectNode referencesObject = objectJsonSerializer.objectNode(); @@ -62,7 +61,7 @@ protected void serializeReferences(@Nonnull ObjectNode rootNode, .stream() .map(ReferenceContract::getReferenceName) .collect(Collectors.toCollection(TreeSet::new)) - .forEach(it -> serializeReferencesWithSameName(referencesObject, entity, it, catalogSchema, entitySchema)); + .forEach(it -> serializeReferencesWithSameName(ctx, referencesObject, entity, it, entitySchema)); rootNode.putIfAbsent(GenericEntityDescriptor.REFERENCES.name(), referencesObject); } diff --git a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/DeleteEntitiesByQueryHandler.java b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/DeleteEntitiesByQueryHandler.java index 66cce6534..32368be5c 100644 --- a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/DeleteEntitiesByQueryHandler.java +++ b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/DeleteEntitiesByQueryHandler.java @@ -6,7 +6,7 @@ * | __/\ V /| | || (_| | |_| | |_) | * \___| \_/ |_|\__\__,_|____/|____/ * - * Copyright (c) 2023 + * Copyright (c) 2023-2024 * * Licensed under the Business Source License, Version 1.1 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import io.evitadb.externalApi.http.EndpointResponse; import io.evitadb.externalApi.http.SuccessEndpointResponse; import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntityJsonSerializer; +import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntitySerializationContext; import io.evitadb.externalApi.rest.exception.RestInternalError; import io.evitadb.externalApi.rest.io.RestEndpointExchange; import io.evitadb.utils.ArrayUtils; @@ -104,6 +105,9 @@ protected Object convertResultIntoSerializableObject(@Nonnull RestEndpointExchan deletedEntities instanceof SealedEntity[], () -> new RestInternalError("Expected SealedEntity[], but got `" + deletedEntities.getClass().getName() + "`.") ); - return entityJsonSerializer.serialize((SealedEntity[]) deletedEntities, restHandlingContext.getCatalogSchema()); + return entityJsonSerializer.serialize( + new EntitySerializationContext(restHandlingContext.getCatalogSchema()), + (SealedEntity[]) deletedEntities + ); } } diff --git a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/EntityHandler.java b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/EntityHandler.java index 41d731af7..cc9f3dde8 100644 --- a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/EntityHandler.java +++ b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/EntityHandler.java @@ -6,7 +6,7 @@ * | __/\ V /| | || (_| | |_| | |_) | * \___| \_/ |_|\__\__,_|____/|____/ * - * Copyright (c) 2023 + * Copyright (c) 2023-2024 * * Licensed under the Business Source License, Version 1.1 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import io.evitadb.api.requestResponse.data.EntityClassifier; import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntityJsonSerializer; +import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntitySerializationContext; import io.evitadb.externalApi.rest.api.catalog.resolver.endpoint.CatalogRestHandlingContext; import io.evitadb.externalApi.rest.exception.RestInternalError; import io.evitadb.externalApi.rest.io.JsonRestHandler; @@ -64,6 +65,9 @@ protected Object convertResultIntoSerializableObject(@Nonnull RestEndpointExchan deletedEntity instanceof EntityClassifier, () -> new RestInternalError("Entity must be instance of EntityClassifier, but got `" + deletedEntity.getClass().getName() + "`.") ); - return entityJsonSerializer.serialize((EntityClassifier) deletedEntity, restHandlingContext.getCatalogSchema()); + return entityJsonSerializer.serialize( + new EntitySerializationContext(restHandlingContext.getCatalogSchema()), + (EntityClassifier) deletedEntity + ); } } diff --git a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/ListEntitiesHandler.java b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/ListEntitiesHandler.java index ab670062f..f6a1e2d74 100644 --- a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/ListEntitiesHandler.java +++ b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/ListEntitiesHandler.java @@ -6,7 +6,7 @@ * | __/\ V /| | || (_| | |_| | |_) | * \___| \_/ |_|\__\__,_|____/|____/ * - * Copyright (c) 2023 + * Copyright (c) 2023-2024 * * Licensed under the Business Source License, Version 1.1 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import io.evitadb.externalApi.http.EndpointResponse; import io.evitadb.externalApi.http.SuccessEndpointResponse; import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntityJsonSerializer; +import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntitySerializationContext; import io.evitadb.externalApi.rest.exception.RestInternalError; import io.evitadb.externalApi.rest.io.RestEndpointExchange; import io.evitadb.utils.Assert; @@ -71,6 +72,9 @@ protected Object convertResultIntoSerializableObject(@Nonnull RestEndpointExchan () -> new RestInternalError("Expected list of entities, but got `" + entities.getClass().getName() + "`.") ); //noinspection unchecked - return entityJsonSerializer.serialize((List) entities, restHandlingContext.getCatalogSchema()); + return entityJsonSerializer.serialize( + new EntitySerializationContext(restHandlingContext.getCatalogSchema()), + (List) entities + ); } } diff --git a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/ListUnknownEntitiesHandler.java b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/ListUnknownEntitiesHandler.java index 917309bf7..486f11076 100644 --- a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/ListUnknownEntitiesHandler.java +++ b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/ListUnknownEntitiesHandler.java @@ -6,7 +6,7 @@ * | __/\ V /| | || (_| | |_| | |_) | * \___| \_/ |_|\__\__,_|____/|____/ * - * Copyright (c) 2023 + * Copyright (c) 2023-2024 * * Licensed under the Business Source License, Version 1.1 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.constraint.FilterByConstraintFromRequestQueryBuilder; import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.constraint.RequireConstraintFromRequestQueryBuilder; import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntityJsonSerializer; +import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntitySerializationContext; import io.evitadb.externalApi.rest.api.catalog.resolver.endpoint.CatalogRestHandlingContext; import io.evitadb.externalApi.rest.exception.RestInternalError; import io.evitadb.externalApi.rest.io.JsonRestHandler; @@ -96,6 +97,9 @@ protected Object convertResultIntoSerializableObject(@Nonnull RestEndpointExchan () -> new RestInternalError("Expected list of entities, but got `" + entities.getClass().getName() + "`.") ); //noinspection unchecked - return entityJsonSerializer.serialize((List) entities, restHandlingContext.getCatalogSchema()); + return entityJsonSerializer.serialize( + new EntitySerializationContext(restHandlingContext.getCatalogSchema()), + (List) entities + ); } } diff --git a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/QueryEntitiesHandler.java b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/QueryEntitiesHandler.java index f02605f16..78663723b 100644 --- a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/QueryEntitiesHandler.java +++ b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/endpoint/QueryEntitiesHandler.java @@ -6,7 +6,7 @@ * | __/\ V /| | || (_| | |_| | |_) | * \___| \_/ |_|\__\__,_|____/|____/ * - * Copyright (c) 2023 + * Copyright (c) 2023-2024 * * Licensed under the Business Source License, Version 1.1 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ import io.evitadb.externalApi.rest.api.catalog.dataApi.dto.QueryResponse.QueryResponseBuilder; import io.evitadb.externalApi.rest.api.catalog.dataApi.dto.StripListDto; import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntityJsonSerializer; +import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.EntitySerializationContext; import io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer.ExtraResultsJsonSerializer; import io.evitadb.externalApi.rest.exception.RestInternalError; import io.evitadb.externalApi.rest.io.RestEndpointExchange; @@ -108,15 +109,16 @@ protected Object convertResultIntoSerializableObject(@Nonnull RestEndpointExchan private DataChunkDto serializeRecordPage(@Nonnull EvitaResponse response) { final DataChunk recordPage = response.getRecordPage(); + final EntitySerializationContext serializationContext = new EntitySerializationContext(restHandlingContext.getCatalogSchema()); if (recordPage instanceof PaginatedList paginatedList) { return new PaginatedListDto( paginatedList, - entityJsonSerializer.serialize(paginatedList.getData(), restHandlingContext.getCatalogSchema()) + entityJsonSerializer.serialize(serializationContext, paginatedList.getData()) ); } else if (recordPage instanceof StripList stripList) { return new StripListDto( stripList, - entityJsonSerializer.serialize(stripList.getData(), restHandlingContext.getCatalogSchema()) + entityJsonSerializer.serialize(serializationContext, stripList.getData()) ); } else { throw new RestInternalError("Unsupported data chunk type `" + recordPage.getClass().getName() + "`."); diff --git a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/serializer/EntityJsonSerializer.java b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/serializer/EntityJsonSerializer.java index dc33e9769..ed0976c27 100644 --- a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/serializer/EntityJsonSerializer.java +++ b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/serializer/EntityJsonSerializer.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.evitadb.api.query.require.QueryPriceMode; import io.evitadb.api.requestResponse.data.AssociatedDataContract; import io.evitadb.api.requestResponse.data.AssociatedDataContract.AssociatedDataKey; import io.evitadb.api.requestResponse.data.AssociatedDataContract.AssociatedDataValue; @@ -35,13 +36,12 @@ import io.evitadb.api.requestResponse.data.AttributesContract.AttributeValue; import io.evitadb.api.requestResponse.data.EntityClassifier; import io.evitadb.api.requestResponse.data.EntityClassifierWithParent; -import io.evitadb.api.requestResponse.data.EntityContract; import io.evitadb.api.requestResponse.data.PriceContract; import io.evitadb.api.requestResponse.data.PriceInnerRecordHandling; import io.evitadb.api.requestResponse.data.ReferenceContract; +import io.evitadb.api.requestResponse.data.structure.EntityDecorator; import io.evitadb.api.requestResponse.schema.AttributeSchemaProvider; import io.evitadb.api.requestResponse.schema.Cardinality; -import io.evitadb.api.requestResponse.schema.CatalogSchemaContract; import io.evitadb.api.requestResponse.schema.EntitySchemaContract; import io.evitadb.api.requestResponse.schema.NamedSchemaContract; import io.evitadb.api.requestResponse.schema.ReferenceSchemaContract; @@ -50,6 +50,7 @@ import io.evitadb.externalApi.rest.api.catalog.dataApi.model.entity.SectionedAssociatedDataDescriptor; import io.evitadb.externalApi.rest.api.catalog.dataApi.model.entity.SectionedAttributesDescriptor; import io.evitadb.externalApi.rest.api.resolver.serializer.ObjectJsonSerializer; +import io.evitadb.externalApi.rest.exception.RestInternalError; import io.evitadb.externalApi.rest.exception.RestQueryResolvingInternalError; import io.evitadb.utils.Assert; import lombok.extern.slf4j.Slf4j; @@ -87,8 +88,8 @@ public EntityJsonSerializer(boolean localized, @Nonnull ObjectMapper objectMappe * * @return serialized entity or list of entities */ - public JsonNode serialize(@Nonnull EntityClassifier entityClassifier, @Nonnull CatalogSchemaContract catalogSchema) { - return serializeSingleEntity(entityClassifier, catalogSchema); + public JsonNode serialize(@Nonnull EntitySerializationContext ctx, @Nonnull EntityClassifier entityClassifier) { + return serializeSingleEntity(ctx, entityClassifier); } /** @@ -96,10 +97,10 @@ public JsonNode serialize(@Nonnull EntityClassifier entityClassifier, @Nonnull C * * @return serialized entity or list of entities */ - public JsonNode serialize(@Nonnull List entityClassifiers, @Nonnull CatalogSchemaContract catalogSchema) { + public JsonNode serialize(@Nonnull EntitySerializationContext ctx, @Nonnull List entityClassifiers) { final ArrayNode arrayNode = objectJsonSerializer.arrayNode(); for (EntityClassifier classifier : entityClassifiers) { - arrayNode.add(serializeSingleEntity(classifier, catalogSchema)); + arrayNode.add(serializeSingleEntity(ctx, classifier)); } return arrayNode; } @@ -109,23 +110,23 @@ public JsonNode serialize(@Nonnull List entityClassifiers, @No * * @return serialized entity or list of entities */ - public JsonNode serialize(@Nonnull EntityClassifier[] entityClassifiers, @Nonnull CatalogSchemaContract catalogSchema) { - return serialize(Arrays.asList(entityClassifiers), catalogSchema); + public JsonNode serialize(@Nonnull EntitySerializationContext ctx, @Nonnull EntityClassifier[] entityClassifiers) { + return serialize(ctx, Arrays.asList(entityClassifiers)); } @Nonnull - private ObjectNode serializeSingleEntity(@Nonnull EntityClassifier entityClassifier, @Nonnull CatalogSchemaContract catalogSchema) { + private ObjectNode serializeSingleEntity(@Nonnull EntitySerializationContext ctx, @Nonnull EntityClassifier entityClassifier) { final ObjectNode rootNode = serializeEntityClassifier(entityClassifier); - if (entityClassifier instanceof EntityContract entity) { - final EntitySchemaContract entitySchema = catalogSchema.getEntitySchemaOrThrowException(entity.getType()); - serializeEntityBody(rootNode, entity, catalogSchema); + if (entityClassifier instanceof EntityDecorator entity) { + final EntitySchemaContract entitySchema = ctx.getCatalogSchema().getEntitySchemaOrThrowException(entity.getType()); + serializeEntityBody(ctx, rootNode, entity); serializeAttributes(rootNode, entity.getLocales(), entity, entitySchema, entitySchema); serializeAssociatedData(rootNode, entity.getLocales(), entity, entitySchema); - serializePrices(rootNode, entity); - serializeReferences(rootNode, entity, catalogSchema, entitySchema); + serializePrices(ctx, rootNode, entity); + serializeReferences(ctx, rootNode, entity, entitySchema); } else if (entityClassifier instanceof EntityClassifierWithParent entity) { entity.getParentEntity().ifPresent(parent -> - rootNode.putIfAbsent(RestEntityDescriptor.PARENT_ENTITY.name(), serializeSingleEntity(parent, catalogSchema))); + rootNode.putIfAbsent(RestEntityDescriptor.PARENT_ENTITY.name(), serializeSingleEntity(ctx, parent))); } return rootNode; } @@ -141,11 +142,13 @@ private ObjectNode serializeEntityClassifier(@Nonnull EntityClassifier entity) { /** * Serialize body of entity */ - private void serializeEntityBody(@Nonnull ObjectNode rootNode, @Nonnull EntityContract entity, @Nonnull CatalogSchemaContract catalogSchema) { + private void serializeEntityBody(@Nonnull EntitySerializationContext ctx, + @Nonnull ObjectNode rootNode, + @Nonnull EntityDecorator entity) { rootNode.put(RestEntityDescriptor.VERSION.name(), entity.version()); if (entity.parentAvailable()) { - entity.getParentEntity().ifPresent(parent -> rootNode.putIfAbsent(RestEntityDescriptor.PARENT_ENTITY.name(), serializeSingleEntity(parent, catalogSchema))); + entity.getParentEntity().ifPresent(parent -> rootNode.putIfAbsent(RestEntityDescriptor.PARENT_ENTITY.name(), serializeSingleEntity(ctx, parent))); } if (!entity.getLocales().isEmpty()) { @@ -242,26 +245,26 @@ private void serializeAssociatedData(@Nonnull ObjectNode rootNode, /** * Serialize references */ - protected void serializeReferences(@Nonnull ObjectNode rootNode, - @Nonnull EntityContract entity, - @Nonnull CatalogSchemaContract catalogSchema, + protected void serializeReferences(@Nonnull EntitySerializationContext ctx, + @Nonnull ObjectNode rootNode, + @Nonnull EntityDecorator entity, @Nonnull EntitySchemaContract entitySchema) { if (entity.referencesAvailable() && !entity.getReferences().isEmpty()) { entity.getReferences() .stream() .map(ReferenceContract::getReferenceName) .collect(Collectors.toCollection(TreeSet::new)) - .forEach(it -> serializeReferencesWithSameName(rootNode, entity, it, catalogSchema, entitySchema)); + .forEach(it -> serializeReferencesWithSameName(ctx, rootNode, entity, it, entitySchema)); } } /** * Serialize references of same name */ - protected void serializeReferencesWithSameName(@Nonnull ObjectNode rootNode, - @Nonnull EntityContract entity, + protected void serializeReferencesWithSameName(@Nonnull EntitySerializationContext ctx, + @Nonnull ObjectNode rootNode, + @Nonnull EntityDecorator entity, @Nonnull String referenceName, - @Nonnull CatalogSchemaContract catalogSchema, @Nonnull EntitySchemaContract entitySchema) { final Collection groupedReferences = entity.getReferences(referenceName); final Optional anyReferenceFound = groupedReferences.stream().findFirst(); @@ -277,13 +280,13 @@ protected void serializeReferencesWithSameName(@Nonnull ObjectNode rootNode, firstReference.getReferenceCardinality() + " but found " + groupedReferences.size() + " references with same name: " + referenceName); - rootNode.putIfAbsent(nodeReferenceName, serializeSingleReference(entity.getLocales(), firstReference, catalogSchema, entitySchema)); + rootNode.putIfAbsent(nodeReferenceName, serializeSingleReference(ctx, entity.getLocales(), firstReference, entitySchema)); } else { final ArrayNode referencesNode = objectJsonSerializer.arrayNode(); rootNode.putIfAbsent(nodeReferenceName, referencesNode); for (ReferenceContract groupedReference : groupedReferences) { - referencesNode.add(serializeSingleReference(entity.getLocales(), groupedReference, catalogSchema, entitySchema)); + referencesNode.add(serializeSingleReference(ctx, entity.getLocales(), groupedReference, entitySchema)); } } } @@ -293,22 +296,22 @@ protected void serializeReferencesWithSameName(@Nonnull ObjectNode rootNode, * Serializes single reference */ @Nonnull - private ObjectNode serializeSingleReference(@Nonnull Set locales, + private ObjectNode serializeSingleReference(@Nonnull EntitySerializationContext ctx, + @Nonnull Set locales, @Nonnull ReferenceContract reference, - @Nonnull CatalogSchemaContract catalogSchema, @Nonnull EntitySchemaContract entitySchema) { final ObjectNode referenceNode = objectJsonSerializer.objectNode(); referenceNode.putIfAbsent(ReferenceDescriptor.REFERENCED_PRIMARY_KEY.name(), objectJsonSerializer.serializeObject(reference.getReferencedPrimaryKey())); reference.getReferencedEntity().ifPresent(sealedEntity -> - referenceNode.putIfAbsent(ReferenceDescriptor.REFERENCED_ENTITY.name(), serializeSingleEntity(sealedEntity, catalogSchema))); + referenceNode.putIfAbsent(ReferenceDescriptor.REFERENCED_ENTITY.name(), serializeSingleEntity(ctx, sealedEntity))); reference.getGroupEntity() .map(it -> (EntityClassifier) it) .or(reference::getGroup) .ifPresent(groupEntity -> { - referenceNode.putIfAbsent(ReferenceDescriptor.GROUP_ENTITY.name(), serializeSingleEntity(groupEntity, catalogSchema)); + referenceNode.putIfAbsent(ReferenceDescriptor.GROUP_ENTITY.name(), serializeSingleEntity(ctx,groupEntity)); }); final ReferenceSchemaContract referenceSchema = reference.getReferenceSchema() @@ -321,7 +324,7 @@ private ObjectNode serializeSingleReference(@Nonnull Set locales, /** * Serialize prices */ - private void serializePrices(@Nonnull ObjectNode rootNode, @Nonnull EntityContract entity) { + private void serializePrices(@Nonnull EntitySerializationContext ctx, @Nonnull ObjectNode rootNode, @Nonnull EntityDecorator entity) { if (entity.pricesAvailable()) { final Collection prices = entity.getPrices(); final ArrayNode pricesNode = objectJsonSerializer.arrayNode(); @@ -335,13 +338,52 @@ private void serializePrices(@Nonnull ObjectNode rootNode, @Nonnull EntityContra rootNode.putIfAbsent(RestEntityDescriptor.PRICE_FOR_SALE.name(), objectJsonSerializer.serializeObject(it)); if (!entity.getPriceInnerRecordHandling().equals(PriceInnerRecordHandling.NONE)) { - final boolean multiplePricesForSale = entity.getAllPricesForSale().size() > 1; + final boolean multiplePricesForSale = hasMultiplePricesForSaleAvailable(ctx, entity); rootNode.putIfAbsent(RestEntityDescriptor.MULTIPLE_PRICES_FOR_SALE_AVAILABLE.name(), objectJsonSerializer.serializeObject(multiplePricesForSale)); } }); } } + /** + * Resolves whether there are multiple unique prices which the entity could be sold for. + */ + private boolean hasMultiplePricesForSaleAvailable(@Nonnull EntitySerializationContext ctx, @Nonnull EntityDecorator entity) { + final List allPricesForSale = entity.getAllPricesForSale(); + if (allPricesForSale.size() <= 1) { + return false; + } + + final boolean hasMultiplePricesForSale; + final PriceInnerRecordHandling priceInnerRecordHandling = entity.getPriceInnerRecordHandling(); + if (priceInnerRecordHandling.equals(PriceInnerRecordHandling.FIRST_OCCURRENCE)) { + if (allPricesForSale.size() <= 1) { + return false; + } + + final QueryPriceMode desiredPriceType = entity.getPricePredicate().getQueryPriceMode(); + final long uniquePriceValuesCount = allPricesForSale.stream() + .map(price -> { + if (desiredPriceType.equals(QueryPriceMode.WITH_TAX)) { + return price.priceWithTax(); + } else if (desiredPriceType.equals(QueryPriceMode.WITHOUT_TAX)) { + return price.priceWithoutTax(); + } else { + throw new RestInternalError("Unsupported price type `" + desiredPriceType + "`"); + } + }) + .distinct() + .count(); + hasMultiplePricesForSale = uniquePriceValuesCount > 1; + } else if (priceInnerRecordHandling.equals(PriceInnerRecordHandling.SUM)) { + hasMultiplePricesForSale = allPricesForSale.size() > 1; + } else { + hasMultiplePricesForSale = false; + } + + return hasMultiplePricesForSale; + } + private void writeAttributesIntoNode(@Nonnull ObjectNode attributesNode, @Nonnull Collection attributeKeys, @Nonnull AttributesContract attributes, diff --git a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/serializer/EntitySerializationContext.java b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/serializer/EntitySerializationContext.java new file mode 100644 index 000000000..a5110c769 --- /dev/null +++ b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/serializer/EntitySerializationContext.java @@ -0,0 +1,44 @@ +/* + * + * _ _ ____ ____ + * _____ _(_) |_ __ _| _ \| __ ) + * / _ \ \ / / | __/ _` | | | | _ \ + * | __/\ V /| | || (_| | |_| | |_) | + * \___| \_/ |_|\__\__,_|____/|____/ + * + * Copyright (c) 2024 + * + * Licensed under the Business Source License, Version 1.1 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/FgForrest/evitaDB/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.evitadb.externalApi.rest.api.catalog.dataApi.resolver.serializer; + +import io.evitadb.api.requestResponse.schema.CatalogSchemaContract; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import javax.annotation.Nonnull; + +/** + * Context holding shared information for entity serialization. + * + * @author Lukáš Hornych, FG Forrest a.s. (c) 2024 + */ +@Builder(toBuilder = true) +@Data +@RequiredArgsConstructor +public class EntitySerializationContext { + + @Nonnull private final CatalogSchemaContract catalogSchema; +} diff --git a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/serializer/ExtraResultsJsonSerializer.java b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/serializer/ExtraResultsJsonSerializer.java index 457b9f273..ec44f9db3 100644 --- a/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/serializer/ExtraResultsJsonSerializer.java +++ b/evita_external_api/evita_external_api_rest/src/main/java/io/evitadb/externalApi/rest/api/catalog/dataApi/resolver/serializer/ExtraResultsJsonSerializer.java @@ -6,7 +6,7 @@ * | __/\ V /| | || (_| | |_| | |_) | * \___| \_/ |_|\__\__,_|____/|____/ * - * Copyright (c) 2023 + * Copyright (c) 2023-2024 * * Licensed under the Business Source License, Version 1.1 (the "License"); * you may not use this file except in compliance with the License. @@ -251,7 +251,7 @@ private JsonNode serializeLevelInfos(@Nonnull List levelInfos, @Nonnu @Nonnull private JsonNode serializeEntity(@Nonnull EntityClassifier entityDecorator, @Nonnull CatalogSchemaContract catalogSchema) { - return entityJsonSerializer.serialize(entityDecorator, catalogSchema); + return entityJsonSerializer.serialize(new EntitySerializationContext(catalogSchema), entityDecorator); } @Nonnull