Skip to content

Commit

Permalink
improve schema docs, add integer filter specification
Browse files Browse the repository at this point in the history
  • Loading branch information
bthreader committed Dec 9, 2023
1 parent 1296890 commit 013f410
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -20,7 +22,11 @@ public class ModelEntitySpecification {
public static Specification<ModelEntity> from(ModelFilterInput input) {
List<Specification<ModelEntity>> 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());

Expand All @@ -33,4 +39,16 @@ private static Specification<ModelEntity> name(StringFilterInput input) {
.from(input, ModelEntity_.name)
.toPredicate(root, query, cb);
}

private static Specification<ModelEntity> modelYear(IntegerFilterInput input) {
return (root, query, cb) -> IntegerFilterSpecification
.from(input, ModelEntity_.modelYear)
.toPredicate(root, query, cb);
}

private static Specification<ModelEntity> brandName(StringFilterInput input) {
return (root, query, cb) -> StringFilterSpecification
.from(input, ModelEntity_.brandName)
.toPredicate(root, query, cb);
}
}
Original file line number Diff line number Diff line change
@@ -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 <T> 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 <T> Specification<T> from(@NonNull IntegerFilterInput input,
SingularAttribute<T, Integer> fieldPath) {
return (root, query, cb) -> {
Expression<Integer> fieldExpression = root.get(fieldPath);

List<Specification<T>> specifications = Stream.<Optional<Specification<T>>>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 <T> Optional<Specification<T>> equalsSpecification(Integer equals,
Expression<Integer> fieldExpression) {
return Optional.ofNullable(equals)
.map(e -> ((root, query, cb) -> cb.equal(fieldExpression, equals)));
}

private static <T> Optional<Specification<T>> inSpecification(List<Integer> in,
Expression<Integer> fieldExpression) {
return Optional.ofNullable(in)
.map(i -> ((root, query, cb) -> fieldExpression.in(in)));
}
}
55 changes: 38 additions & 17 deletions service-course-api/src/main/resources/schema/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ scalar Date

# Utils

enum Op {
enum Operator {
OR
AND
}
Expand All @@ -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 {
"""
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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!
Expand All @@ -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
Expand All @@ -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
}

0 comments on commit 013f410

Please sign in to comment.