diff --git a/service-course-api/src/main/java/servicecourse/repo/BikeBrandEntity.java b/service-course-api/src/main/java/servicecourse/repo/BikeBrandEntity.java index 378ef91..b7d8caf 100644 --- a/service-course-api/src/main/java/servicecourse/repo/BikeBrandEntity.java +++ b/service-course-api/src/main/java/servicecourse/repo/BikeBrandEntity.java @@ -3,18 +3,20 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; import servicecourse.generated.types.BikeBrand; @Entity @Table(name = "bike_brands") @EqualsAndHashCode -@RequiredArgsConstructor +@AllArgsConstructor @NoArgsConstructor @ToString public class BikeBrandEntity { @Id - @NonNull private String name; public static BikeBrandEntity ofName(String name) { diff --git a/service-course-api/src/main/java/servicecourse/repo/ModelEntitySpecification.java b/service-course-api/src/main/java/servicecourse/repo/ModelEntitySpecification.java index 7dcb9b7..95a0268 100644 --- a/service-course-api/src/main/java/servicecourse/repo/ModelEntitySpecification.java +++ b/service-course-api/src/main/java/servicecourse/repo/ModelEntitySpecification.java @@ -1,8 +1,10 @@ package servicecourse.repo; import org.springframework.data.jpa.domain.Specification; +import servicecourse.generated.types.IntegerFilterInput; import servicecourse.generated.types.ModelFilterInput; import servicecourse.generated.types.StringFilterInput; +import servicecourse.repo.common.IntegerFilterSpecification; import servicecourse.repo.common.SpecificationUtils; import servicecourse.repo.common.StringFilterSpecification; @@ -20,7 +22,11 @@ public class ModelEntitySpecification { public static Specification from(ModelFilterInput input) { List> specifications = Stream.of( Optional.ofNullable(input.getName()) - .map(ModelEntitySpecification::name)) + .map(ModelEntitySpecification::name), + Optional.ofNullable(input.getModelYear()) + .map(ModelEntitySpecification::modelYear), + Optional.ofNullable(input.getBrandName()) + .map(ModelEntitySpecification::brandName)) .flatMap(Optional::stream) .collect(Collectors.toList()); @@ -33,4 +39,16 @@ private static Specification name(StringFilterInput input) { .from(input, ModelEntity_.name) .toPredicate(root, query, cb); } + + private static Specification modelYear(IntegerFilterInput input) { + return (root, query, cb) -> IntegerFilterSpecification + .from(input, ModelEntity_.modelYear) + .toPredicate(root, query, cb); + } + + private static Specification brandName(StringFilterInput input) { + return (root, query, cb) -> StringFilterSpecification + .from(input, ModelEntity_.brandName) + .toPredicate(root, query, cb); + } } diff --git a/service-course-api/src/main/java/servicecourse/repo/common/IntegerFilterSpecification.java b/service-course-api/src/main/java/servicecourse/repo/common/IntegerFilterSpecification.java new file mode 100644 index 0000000..ae9264d --- /dev/null +++ b/service-course-api/src/main/java/servicecourse/repo/common/IntegerFilterSpecification.java @@ -0,0 +1,51 @@ +package servicecourse.repo.common; + +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.metamodel.SingularAttribute; +import lombok.NonNull; +import org.springframework.data.jpa.domain.Specification; +import servicecourse.generated.types.IntegerFilterInput; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class IntegerFilterSpecification { + /** + * @param input the details of the integer filter to apply to field + * @param fieldPath the path from the root entity (of type {@code T}) to the {@code Integer} + * attribute to apply the filter on + * @param the entity for which {@code fieldPath} is an attribute + * @return a specification ready to apply to entities of type {@code T}, if the input is empty + * the specification will be equivalent to "match all" + * @throws NullPointerException if {@code input} is null + */ + public static Specification from(@NonNull IntegerFilterInput input, + SingularAttribute fieldPath) { + return (root, query, cb) -> { + Expression fieldExpression = root.get(fieldPath); + + List> specifications = Stream.>>of( + equalsSpecification(input.getEquals(), fieldExpression), + inSpecification(input.getIn(), fieldExpression)) + .flatMap(Optional::stream) + .collect(Collectors.toList()); + + return specifications.isEmpty() ? SpecificationUtils.alwaysTruePredicate(cb) + : Specification.anyOf(specifications).toPredicate(root, query, cb); + }; + } + + private static Optional> equalsSpecification(Integer equals, + Expression fieldExpression) { + return Optional.ofNullable(equals) + .map(e -> ((root, query, cb) -> cb.equal(fieldExpression, equals))); + } + + private static Optional> inSpecification(List in, + Expression fieldExpression) { + return Optional.ofNullable(in) + .map(i -> ((root, query, cb) -> fieldExpression.in(in))); + } +} diff --git a/service-course-api/src/main/resources/schema/schema.graphqls b/service-course-api/src/main/resources/schema/schema.graphqls index 378aefb..81305ce 100644 --- a/service-course-api/src/main/resources/schema/schema.graphqls +++ b/service-course-api/src/main/resources/schema/schema.graphqls @@ -15,7 +15,7 @@ scalar Date # Utils -enum Op { +enum Operator { OR AND } @@ -26,9 +26,8 @@ input DateRangeFilterInput { } """ -Fields are combined with OR logic. - -Must specify a minimum of one field, otherwise an error will be raised. +If multiple fields are specified, they will be combined with OR logic. If no fields are specified, +the filter will be treated as "match all". """ input StringFilterInput { """ @@ -39,12 +38,13 @@ input StringFilterInput { equals: String } +""" +If multiple fields are specified, they will be combined with OR logic. If no fields are specified, +the filter will be treated as "match all". +""" input IntegerFilterInput { - lessThanOrEqualTo: Int - greaterThanOrEqualTo: Int equals: Int in: [Int!] - operator: Op } # Queries and mutations @@ -104,12 +104,19 @@ input CreateModelInput { } """ -Fields are combined with AND logic +If multiple fields are specified, they will be combined with AND logic. If no fields are +specified, the filter will be treated as "match all". """ input ModelFilterInput { + """ + If specified, return only models with a name that matches the string filter + """ name: StringFilterInput modelYear: IntegerFilterInput - brandName: String + """ + If specified, return only models with a brand name that matches the string filter + """ + brandName: StringFilterInput } # Bikes @@ -122,30 +129,37 @@ type Bike { heroImageUrl: Url } +""" +If multiple fields are specified, they will be combined with AND logic. If no fields are +specified, the filter will be treated as "match all". +""" input BikesFilterInput { """ - Return only bikes that are available in the provided date range + If specified, return only bikes that are available in the provided date range """ availableDateRange: DateRangeFilterInput """ - Return only bikes whose model matches the criteria + If specified, return only bikes whose model matches the criteria """ model: ModelFilterInput """ - Return only bikes with a groupset matching the criteria + If specified, return only bikes with a groupset matching the criteria """ groupset: GroupsetFilterInput """ - Return only bikes with a size that matches the input + If specified, return only bikes with a size that matches the string filter """ size: StringFilterInput } input CreateBikeInput { + """ + A model with this id must exist otherwise the mutation will fail and an error will be raised + """ modelId: ID! """ - There must be a groupset with this name already saved otherwise the mutation will fail and an - error will be raised. + A groupset with this name must exist otherwise the mutation will fail and an error will be + raised """ groupsetName: String! size: String! @@ -156,7 +170,7 @@ input UpdateBikeInput { bikeId: ID! """ If specified, there must be a groupset with this name already saved otherwise the mutation will - fail and an error will be raised. + fail and an error will be raised """ groupsetName: String heroImageUrl: Url @@ -176,11 +190,18 @@ type Groupset { isElectronic: Boolean! } +""" +If multiple fields are specified, they will be combined with AND logic. If no fields are +specified, the filter will be treated as "match all". +""" input GroupsetFilterInput { """ - Return only groupsets with a name that matches the input + If specified, return only groupsets with a name that matches the string filter """ name: StringFilterInput + """ + If specified, return only groupsets that belong to the provided brand + """ brand: GroupsetBrand isElectronic: Boolean }