diff --git a/spring-batch-notion/pom.xml b/spring-batch-notion/pom.xml index a896abdb..6cd7fa54 100644 --- a/spring-batch-notion/pom.xml +++ b/spring-batch-notion/pom.xml @@ -38,35 +38,20 @@ https://github.com/spring-projects/spring-batch-extensions/tree/main/spring-batch-notion - - 1.11.1 - - - - com.github.seratch - notion-sdk-jvm-core - ${notion-sdk-jvm.version} - - - com.github.seratch - notion-sdk-jvm-httpclient - ${notion-sdk-jvm.version} - - - com.github.seratch - notion-sdk-jvm-slf4j2 - ${notion-sdk-jvm.version} - org.springframework - spring-beans + spring-web org.springframework.batch spring-batch-infrastructure + + tools.jackson.core + jackson-databind + com.h2database diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/Filter.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/Filter.java index 3a298fc2..55b89ee5 100644 --- a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/Filter.java +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/Filter.java @@ -15,21 +15,28 @@ */ package org.springframework.batch.extensions.notion; -import notion.api.v1.model.databases.query.filter.CompoundFilterElement; -import notion.api.v1.model.databases.query.filter.QueryTopLevelFilter; -import notion.api.v1.model.databases.query.filter.condition.CheckboxFilter; -import notion.api.v1.model.databases.query.filter.condition.FilesFilter; -import notion.api.v1.model.databases.query.filter.condition.MultiSelectFilter; -import notion.api.v1.model.databases.query.filter.condition.NumberFilter; -import notion.api.v1.model.databases.query.filter.condition.SelectFilter; -import notion.api.v1.model.databases.query.filter.condition.StatusFilter; - +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonValue; +import org.jspecify.annotations.Nullable; +import org.springframework.batch.extensions.notion.Filter.FilterConditionBuilder.Condition; +import tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import tools.jackson.databind.annotation.JsonNaming; + +import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; -import java.util.function.BiConsumer; +import java.util.StringJoiner; import java.util.function.BiFunction; -import java.util.function.Consumer; /** * Filtering conditions to limit the entries returned from a database query. @@ -41,6 +48,8 @@ * * @author Stefano Cordio */ +@JsonTypeName("filter") +@JsonTypeInfo(use = Id.NAME, include = As.WRAPPER_OBJECT) public abstract sealed class Filter { /** @@ -63,10 +72,6 @@ public static TopLevelFilter where(Filter filter) { private Filter() { } - abstract QueryTopLevelFilter toQueryTopLevelFilter(); - - abstract CompoundFilterElement toCompoundFilterElement(); - /** * Base class for top level filters that support filters composition via the * {@link TopLevelFilter#and} and {@link TopLevelFilter#or} methods. @@ -86,7 +91,7 @@ private TopLevelFilter() { */ public FilterConditionBuilder and() { return new FilterConditionBuilder<>( - (property, customizer) -> new AndFilter(this, new PropertyFilter(property, customizer))); + (property, condition) -> new AndFilter(this, new PropertyFilter(property, condition))); } /** @@ -105,7 +110,7 @@ public AndFilter and(Filter filter) { */ public FilterConditionBuilder or() { return new FilterConditionBuilder<>( - (property, customizer) -> new OrFilter(this, new PropertyFilter(property, customizer))); + (property, condition) -> new OrFilter(this, new PropertyFilter(property, condition))); } /** @@ -121,97 +126,43 @@ public OrFilter or(Filter filter) { private static final class DelegateFilter extends TopLevelFilter { + @JsonValue private final Filter delegate; private DelegateFilter(Filter delegate) { - this.delegate = Objects.requireNonNull(delegate); - } - - @Override - QueryTopLevelFilter toQueryTopLevelFilter() { - return delegate.toQueryTopLevelFilter(); - } - - @Override - CompoundFilterElement toCompoundFilterElement() { - return delegate.toCompoundFilterElement(); + this.delegate = delegate; } } private static final class PropertyFilter extends TopLevelFilter { + @SuppressWarnings("unused") + @JsonProperty private final String property; - private final NotionPropertyFilterCustomizer customizer; + @SuppressWarnings("unused") + @JsonAnyGetter + private final Map> condition; - private PropertyFilter(String property, NotionPropertyFilterCustomizer customizer) { + private PropertyFilter(String property, Entry> condition) { this.property = property; - this.customizer = customizer; + this.condition = Map.ofEntries(condition); } @Override - QueryTopLevelFilter toQueryTopLevelFilter() { - return toNotionPropertyFilter(); - } - - @Override - CompoundFilterElement toCompoundFilterElement() { - return toNotionPropertyFilter(); - } - - private notion.api.v1.model.databases.query.filter.PropertyFilter toNotionPropertyFilter() { - var notionPropertyFilter = new notion.api.v1.model.databases.query.filter.PropertyFilter(property); - customizer.accept(notionPropertyFilter); - return notionPropertyFilter; + public String toString() { + return new StringJoiner(", ", PropertyFilter.class.getSimpleName() + "[", "]") + .add("property='" + property + "'") + .add(condition.toString()) + .toString(); } } @FunctionalInterface - private interface NotionPropertyFilterFactory - extends BiFunction { - - } - - @FunctionalInterface - private interface NotionPropertyFilterCustomizer - extends Consumer { - - } - - static abstract sealed class CompoundFilter extends Filter { - - final List filters = new ArrayList<>(); - - private final NotionCompoundFilterSetter setter; - - private CompoundFilter(NotionCompoundFilterSetter setter) { - this.setter = setter; - } - - @Override - QueryTopLevelFilter toQueryTopLevelFilter() { - return toNotionCompoundFilter(); - } - - @Override - CompoundFilterElement toCompoundFilterElement() { - return toNotionCompoundFilter(); - } - - private notion.api.v1.model.databases.query.filter.CompoundFilter toNotionCompoundFilter() { - var notionCompoundFilter = new notion.api.v1.model.databases.query.filter.CompoundFilter(); - var notionCompoundFilterElements = filters.stream().map(Filter::toCompoundFilterElement).toList(); - setter.accept(notionCompoundFilter, notionCompoundFilterElements); - return notionCompoundFilter; - } - - } - - @FunctionalInterface - private interface NotionCompoundFilterSetter - extends BiConsumer> { + private interface PropertyFilterFactory + extends BiFunction>, T> { } @@ -221,11 +172,14 @@ private interface NotionCompoundFilterSetter *

* Returns entries that match all of the provided filters. */ - public static final class AndFilter extends CompoundFilter { + public static final class AndFilter extends Filter { + + @JsonProperty + @JsonTypeInfo(use = Id.DEDUCTION) + private final List and = new ArrayList<>(); private AndFilter(Filter first, Filter second) { - super(notion.api.v1.model.databases.query.filter.CompoundFilter::setAnd); - filters.addAll(List.of(first, second)); + and.addAll(List.of(first, second)); } /** @@ -234,8 +188,8 @@ private AndFilter(Filter first, Filter second) { * @return a {@link FilterConditionBuilder} instance for an {@link AndFilter} */ public FilterConditionBuilder and() { - return new FilterConditionBuilder<>((property, customizer) -> { - filters.add(new PropertyFilter(property, customizer)); + return new FilterConditionBuilder<>((property, condition) -> { + and.add(new PropertyFilter(property, condition)); return this; }); } @@ -246,7 +200,7 @@ public FilterConditionBuilder and() { * @return a new {@link AndFilter} instance */ public AndFilter and(Filter filter) { - filters.add(Objects.requireNonNull(filter)); + and.add(Objects.requireNonNull(filter)); return this; } @@ -258,11 +212,14 @@ public AndFilter and(Filter filter) { *

* Returns entries that match any of the provided filters. */ - public static final class OrFilter extends CompoundFilter { + public static final class OrFilter extends Filter { + + @JsonProperty + @JsonTypeInfo(use = Id.DEDUCTION) + private final List or = new ArrayList<>(); private OrFilter(Filter first, Filter second) { - super(notion.api.v1.model.databases.query.filter.CompoundFilter::setOr); - filters.addAll(List.of(first, second)); + or.addAll(List.of(first, second)); } /** @@ -271,8 +228,8 @@ private OrFilter(Filter first, Filter second) { * @return a {@link FilterConditionBuilder} instance for an {@link OrFilter} */ public FilterConditionBuilder or() { - return new FilterConditionBuilder<>((property, customizer) -> { - filters.add(new PropertyFilter(property, customizer)); + return new FilterConditionBuilder<>((property, condition) -> { + or.add(new PropertyFilter(property, condition)); return this; }); } @@ -283,7 +240,7 @@ public FilterConditionBuilder or() { * @return a new {@link OrFilter} instance */ public OrFilter or(Filter filter) { - filters.add(Objects.requireNonNull(filter)); + or.add(Objects.requireNonNull(filter)); return this; } @@ -296,9 +253,9 @@ public OrFilter or(Filter filter) { */ public static final class FilterConditionBuilder { - private final NotionPropertyFilterFactory factory; + private final PropertyFilterFactory factory; - private FilterConditionBuilder(NotionPropertyFilterFactory factory) { + private FilterConditionBuilder(PropertyFilterFactory factory) { this.factory = factory; } @@ -316,7 +273,7 @@ public CheckboxCondition checkbox(String property) { * Start the definition of the filter condition for a {@code files} property. * @param property The name of the property as it appears in the database, or the * property ID - * @return a new {@link CheckboxCondition} instance + * @return a new {@link FilesCondition} instance */ public FilesCondition files(String property) { return new FilesCondition<>(property, factory); @@ -363,19 +320,24 @@ public StatusCondition status(String property) { return new StatusCondition<>(property, factory); } + @JsonNaming(SnakeCaseStrategy.class) + @JsonInclude(Include.NON_EMPTY) static abstract sealed class Condition { + private final String name; + private final String property; - private final NotionPropertyFilterFactory factory; + private final PropertyFilterFactory factory; - private Condition(String property, NotionPropertyFilterFactory factory) { + private Condition(String name, String property, PropertyFilterFactory factory) { + this.name = name; this.property = property; this.factory = factory; } - T toFilter(NotionPropertyFilterCustomizer customizer) { - return factory.apply(property, customizer); + T toFilter() { + return factory.apply(property, new SimpleEntry<>(name, this)); } } @@ -387,8 +349,16 @@ T toFilter(NotionPropertyFilterCustomizer customizer) { */ public static final class CheckboxCondition extends Condition { - private CheckboxCondition(String property, NotionPropertyFilterFactory factory) { - super(property, factory); + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Boolean equals; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Boolean doesNotEqual; + + private CheckboxCondition(String property, PropertyFilterFactory factory) { + super("checkbox", property, factory); } /** @@ -397,9 +367,8 @@ private CheckboxCondition(String property, NotionPropertyFilterFactory factor * @return a filter with the newly defined condition */ public T isEqualTo(boolean value) { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setEquals(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setCheckbox(checkboxFilter)); + this.equals = value; + return toFilter(); } /** @@ -408,9 +377,8 @@ public T isEqualTo(boolean value) { * @return a filter with the newly defined condition */ public T isNotEqualTo(boolean value) { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setDoesNotEqual(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setCheckbox(checkboxFilter)); + this.doesNotEqual = value; + return toFilter(); } } @@ -422,8 +390,16 @@ public T isNotEqualTo(boolean value) { */ public static final class FilesCondition extends Condition { - private FilesCondition(String property, NotionPropertyFilterFactory factory) { - super(property, factory); + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Boolean isEmpty; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Boolean isNotEmpty; + + private FilesCondition(String property, PropertyFilterFactory factory) { + super("files", property, factory); } /** @@ -431,9 +407,8 @@ private FilesCondition(String property, NotionPropertyFilterFactory factory) * @return a filter with the newly defined condition */ public T isEmpty() { - FilesFilter filesFilter = new FilesFilter(); - filesFilter.setEmpty(true); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setFile(filesFilter)); + this.isEmpty = true; + return toFilter(); } /** @@ -441,9 +416,8 @@ public T isEmpty() { * @return a filter with the newly defined condition */ public T isNotEmpty() { - FilesFilter filesFilter = new FilesFilter(); - filesFilter.setNotEmpty(true); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setFile(filesFilter)); + this.isNotEmpty = true; + return toFilter(); } } @@ -455,8 +429,24 @@ public T isNotEmpty() { */ public static final class MultiSelectCondition extends Condition { - private MultiSelectCondition(String property, NotionPropertyFilterFactory factory) { - super(property, factory); + @SuppressWarnings("unused") + @JsonProperty + private @Nullable String contains; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable String doesNotContain; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Boolean isEmpty; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Boolean isNotEmpty; + + private MultiSelectCondition(String property, PropertyFilterFactory factory) { + super("multi_select", property, factory); } /** @@ -466,9 +456,8 @@ private MultiSelectCondition(String property, NotionPropertyFilterFactory fac * @return a filter with the newly defined condition */ public T contains(String value) { - MultiSelectFilter multiSelectFilter = new MultiSelectFilter(); - multiSelectFilter.setContains(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setMultiSelect(multiSelectFilter)); + this.contains = value; + return toFilter(); } /** @@ -478,9 +467,8 @@ public T contains(String value) { * @return a filter with the newly defined condition */ public T doesNotContain(String value) { - MultiSelectFilter multiSelectFilter = new MultiSelectFilter(); - multiSelectFilter.setDoesNotContain(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setMultiSelect(multiSelectFilter)); + this.doesNotContain = value; + return toFilter(); } /** @@ -488,9 +476,8 @@ public T doesNotContain(String value) { * @return a filter with the newly defined condition */ public T isEmpty() { - MultiSelectFilter multiSelectFilter = new MultiSelectFilter(); - multiSelectFilter.setEmpty(true); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setMultiSelect(multiSelectFilter)); + this.isEmpty = true; + return toFilter(); } /** @@ -498,9 +485,8 @@ public T isEmpty() { * @return a filter with the newly defined condition */ public T isNotEmpty() { - MultiSelectFilter multiSelectFilter = new MultiSelectFilter(); - multiSelectFilter.setNotEmpty(true); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setMultiSelect(multiSelectFilter)); + this.isNotEmpty = true; + return toFilter(); } } @@ -512,8 +498,40 @@ public T isNotEmpty() { */ public static final class NumberCondition extends Condition { - private NumberCondition(String property, NotionPropertyFilterFactory factory) { - super(property, factory); + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Integer equals; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Integer doesNotEqual; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Integer greaterThan; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Integer greaterThanOrEqualTo; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Integer lessThan; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Integer lessThanOrEqualTo; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Boolean isEmpty; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Boolean isNotEmpty; + + private NumberCondition(String property, PropertyFilterFactory factory) { + super("number", property, factory); } /** @@ -523,9 +541,8 @@ private NumberCondition(String property, NotionPropertyFilterFactory factory) * @return a filter with the newly defined condition */ public T isEqualTo(int value) { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setEquals(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter)); + this.equals = value; + return toFilter(); } /** @@ -535,9 +552,8 @@ public T isEqualTo(int value) { * @return a filter with the newly defined condition */ public T isNotEqualTo(int value) { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setDoesNotEqual(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter)); + this.doesNotEqual = value; + return toFilter(); } /** @@ -546,9 +562,8 @@ public T isNotEqualTo(int value) { * @return a filter with the newly defined condition */ public T isGreaterThan(int value) { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setGreaterThan(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter)); + this.greaterThan = value; + return toFilter(); } /** @@ -558,9 +573,8 @@ public T isGreaterThan(int value) { * @return a filter with the newly defined condition */ public T isGreaterThanOrEqualTo(int value) { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setGreaterThanOrEqualTo(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter)); + this.greaterThanOrEqualTo = value; + return toFilter(); } /** @@ -570,9 +584,8 @@ public T isGreaterThanOrEqualTo(int value) { * @return a filter with the newly defined condition */ public T isLessThan(int value) { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setLessThan(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter)); + this.lessThan = value; + return toFilter(); } /** @@ -582,9 +595,8 @@ public T isLessThan(int value) { * @return a filter with the newly defined condition */ public T isLessThanOrEqualTo(int value) { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setLessThanOrEqualTo(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter)); + this.lessThanOrEqualTo = value; + return toFilter(); } /** @@ -592,9 +604,8 @@ public T isLessThanOrEqualTo(int value) { * @return a filter with the newly defined condition */ public T isEmpty() { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setEmpty(true); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter)); + this.isEmpty = true; + return toFilter(); } /** @@ -602,9 +613,8 @@ public T isEmpty() { * @return a filter with the newly defined condition */ public T isNotEmpty() { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setNotEmpty(true); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setNumber(numberFilter)); + this.isNotEmpty = true; + return toFilter(); } } @@ -616,8 +626,24 @@ public T isNotEmpty() { */ public static final class SelectCondition extends Condition { - private SelectCondition(String property, NotionPropertyFilterFactory factory) { - super(property, factory); + @SuppressWarnings("unused") + @JsonProperty + private @Nullable String equals; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable String doesNotEqual; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Boolean isEmpty; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Boolean isNotEmpty; + + private SelectCondition(String property, PropertyFilterFactory factory) { + super("select", property, factory); } /** @@ -626,9 +652,8 @@ private SelectCondition(String property, NotionPropertyFilterFactory factory) * @return a filter with the newly defined condition */ public T isEqualTo(String value) { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setEquals(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setSelect(selectFilter)); + this.equals = value; + return toFilter(); } /** @@ -638,9 +663,8 @@ public T isEqualTo(String value) { * @return a filter with the newly defined condition */ public T isNotEqualTo(String value) { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setDoesNotEqual(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setSelect(selectFilter)); + this.doesNotEqual = value; + return toFilter(); } /** @@ -648,9 +672,8 @@ public T isNotEqualTo(String value) { * @return a filter with the newly defined condition */ public T isEmpty() { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setEmpty(true); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setSelect(selectFilter)); + this.isEmpty = true; + return toFilter(); } /** @@ -658,9 +681,8 @@ public T isEmpty() { * @return a filter with the newly defined condition */ public T isNotEmpty() { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setNotEmpty(true); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setSelect(selectFilter)); + this.isNotEmpty = true; + return toFilter(); } } @@ -672,8 +694,24 @@ public T isNotEmpty() { */ public static final class StatusCondition extends Condition { - private StatusCondition(String property, NotionPropertyFilterFactory factory) { - super(property, factory); + @SuppressWarnings("unused") + @JsonProperty + private @Nullable String equals; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable String doesNotEqual; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Boolean isEmpty; + + @SuppressWarnings("unused") + @JsonProperty + private @Nullable Boolean isNotEmpty; + + private StatusCondition(String property, PropertyFilterFactory factory) { + super("status", property, factory); } /** @@ -682,9 +720,8 @@ private StatusCondition(String property, NotionPropertyFilterFactory factory) * @return a filter with the newly defined condition */ public T isEqualTo(String value) { - StatusFilter statusFilter = new StatusFilter(); - statusFilter.setEquals(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setStatus(statusFilter)); + this.equals = value; + return toFilter(); } /** @@ -694,9 +731,8 @@ public T isEqualTo(String value) { * @return a filter with the newly defined condition */ public T isNotEqualTo(String value) { - StatusFilter statusFilter = new StatusFilter(); - statusFilter.setDoesNotEqual(value); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setStatus(statusFilter)); + this.doesNotEqual = value; + return toFilter(); } /** @@ -704,9 +740,8 @@ public T isNotEqualTo(String value) { * @return a filter with the newly defined condition */ public T isEmpty() { - StatusFilter statusFilter = new StatusFilter(); - statusFilter.setEmpty(true); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setStatus(statusFilter)); + this.isEmpty = true; + return toFilter(); } /** @@ -714,9 +749,8 @@ public T isEmpty() { * @return a filter with the newly defined condition */ public T isNotEmpty() { - StatusFilter statusFilter = new StatusFilter(); - statusFilter.setNotEmpty(true); - return toFilter(notionPropertyFilter -> notionPropertyFilter.setStatus(statusFilter)); + this.isNotEmpty = true; + return toFilter(); } } diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReader.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReader.java index 8c1455d8..04e0df37 100644 --- a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReader.java +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseItemReader.java @@ -15,22 +15,19 @@ */ package org.springframework.batch.extensions.notion; -import notion.api.v1.NotionClient; -import notion.api.v1.http.JavaNetHttpClient; -import notion.api.v1.logging.Slf4jLogger; -import notion.api.v1.model.databases.QueryResults; -import notion.api.v1.model.databases.query.filter.QueryTopLevelFilter; -import notion.api.v1.model.databases.query.sort.QuerySort; -import notion.api.v1.model.pages.Page; -import notion.api.v1.model.pages.PageProperty; -import notion.api.v1.model.pages.PageProperty.RichText; -import notion.api.v1.request.databases.QueryDatabaseRequest; import org.jspecify.annotations.Nullable; +import org.springframework.batch.extensions.notion.PageProperty.RichTextProperty; +import org.springframework.batch.extensions.notion.PageProperty.TitleProperty; import org.springframework.batch.extensions.notion.mapping.PropertyMapper; import org.springframework.batch.infrastructure.item.ExecutionContext; import org.springframework.batch.infrastructure.item.ItemReader; import org.springframework.batch.infrastructure.item.data.AbstractPaginatedDataItemReader; +import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; +import org.springframework.web.client.ApiVersionInserter; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.support.RestClientAdapter; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; import java.util.Collections; import java.util.Iterator; @@ -39,7 +36,6 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Restartable {@link ItemReader} that reads entries from a Notion database via a paging @@ -71,11 +67,11 @@ public class NotionDatabaseItemReader extends AbstractPaginatedDataItemReader private String baseUrl = DEFAULT_BASE_URL; - private @Nullable QueryTopLevelFilter filter; + private @Nullable Filter filter; - private @Nullable List sorts; + private Sort[] sorts = new Sort[0]; - private @Nullable NotionClient client; + private @Nullable NotionDatabaseService service; private boolean hasMore; @@ -117,7 +113,7 @@ public void setBaseUrl(String baseUrl) { * @see Filter#where(Filter) */ public void setFilter(Filter filter) { - this.filter = filter.toQueryTopLevelFilter(); + this.filter = filter; } /** @@ -130,7 +126,7 @@ public void setFilter(Filter filter) { * @see Sort#by(Sort.Timestamp) */ public void setSorts(Sort... sorts) { - this.sorts = Stream.of(sorts).map(Sort::toQuerySort).toList(); + this.sorts = sorts; } /** @@ -151,10 +147,15 @@ public void setPageSize(int pageSize) { */ @Override protected void doOpen() { - client = new NotionClient(token); - client.setHttpClient(new JavaNetHttpClient()); - client.setLogger(new Slf4jLogger()); - client.setBaseUrl(baseUrl); + RestClient restClient = RestClient.builder() + .baseUrl(baseUrl) + .apiVersionInserter(ApiVersionInserter.useHeader("Notion-Version")) + .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token) + .build(); + + RestClientAdapter adapter = RestClientAdapter.create(restClient); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); + service = factory.createClient(NotionDatabaseService.class); hasMore = true; } @@ -168,53 +169,47 @@ protected Iterator doPageRead() { return Collections.emptyIterator(); } - QueryDatabaseRequest request = new QueryDatabaseRequest(databaseId); - request.setFilter(filter); - request.setSorts(sorts); - request.setStartCursor(nextCursor); - request.setPageSize(pageSize); + QueryRequest request = new QueryRequest(pageSize, nextCursor, filter, sorts); @SuppressWarnings("DataFlowIssue") - QueryResults queryResults = client.queryDatabase(request); + QueryResult result = service.query(databaseId, request); - hasMore = queryResults.getHasMore(); - nextCursor = queryResults.getNextCursor(); + hasMore = result.hasMore(); + nextCursor = result.nextCursor(); - return queryResults.getResults() + return result.results() .stream() .map(NotionDatabaseItemReader::getProperties) .map(propertyMapper::map) .iterator(); } - private static Map getProperties(Page element) { - return element.getProperties() + private static Map getProperties(Page page) { + return page.properties() .entrySet() .stream() .collect(Collectors.toUnmodifiableMap(Entry::getKey, entry -> getPropertyValue(entry.getValue()))); } private static String getPropertyValue(PageProperty property) { - return switch (property.getType()) { - case RichText -> getPlainText(property.getRichText()); - case Title -> getPlainText(property.getTitle()); - default -> throw new IllegalArgumentException("Unsupported type: " + property.getType()); - }; + if (property instanceof RichTextProperty p) { + return getPlainText(p.richText()); + } + if (property instanceof TitleProperty p) { + return getPlainText(p.title()); + } + throw new IllegalArgumentException("Unsupported type: " + property.getClass()); } private static String getPlainText(List texts) { - return texts.isEmpty() ? "" : texts.get(0).getPlainText(); + return texts.isEmpty() ? "" : texts.get(0).plainText(); } /** * {@inheritDoc} */ - @SuppressWarnings("DataFlowIssue") @Override protected void doClose() { - client.close(); - client = null; - hasMore = false; } diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseService.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseService.java new file mode 100644 index 00000000..1e0e88d8 --- /dev/null +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/NotionDatabaseService.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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 org.springframework.batch.extensions.notion; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.service.annotation.HttpExchange; +import org.springframework.web.service.annotation.PostExchange; + +@HttpExchange(url = "/databases", version = "2022-06-28", accept = MediaType.APPLICATION_JSON_VALUE) +interface NotionDatabaseService { + + @PostExchange("/{databaseId}/query") + QueryResult query(@PathVariable String databaseId, @RequestBody QueryRequest request); + +} diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/Page.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/Page.java new file mode 100644 index 00000000..085250d6 --- /dev/null +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/Page.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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 org.springframework.batch.extensions.notion; + +import java.util.Map; + +record Page(Map properties) { +} diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/PageProperty.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/PageProperty.java new file mode 100644 index 00000000..9d8ece29 --- /dev/null +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/PageProperty.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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 org.springframework.batch.extensions.notion; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; +import tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import tools.jackson.databind.annotation.JsonNaming; + +import java.util.List; + +@JsonTypeInfo(use = Id.NAME, property = "type") +@JsonSubTypes({ // + @Type(name = "rich_text", value = PageProperty.RichTextProperty.class), + @Type(name = "title", value = PageProperty.TitleProperty.class) // +}) +interface PageProperty { + + @JsonNaming(SnakeCaseStrategy.class) + record RichTextProperty(List richText) implements PageProperty { + } + + record TitleProperty(List title) implements PageProperty { + } + +} diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/QueryRequest.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/QueryRequest.java new file mode 100644 index 00000000..da2ff997 --- /dev/null +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/QueryRequest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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 org.springframework.batch.extensions.notion; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import tools.jackson.databind.annotation.JsonNaming; + +import java.util.List; + +@JsonNaming(SnakeCaseStrategy.class) +@JsonInclude(Include.NON_EMPTY) +record QueryRequest(int pageSize, @Nullable String startCursor, @Nullable Filter filter, Sort... sorts) { +} diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/QueryResult.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/QueryResult.java new file mode 100644 index 00000000..4ccceb0f --- /dev/null +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/QueryResult.java @@ -0,0 +1,25 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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 org.springframework.batch.extensions.notion; + +import tools.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import tools.jackson.databind.annotation.JsonNaming; + +import java.util.List; + +@JsonNaming(SnakeCaseStrategy.class) +record QueryResult(List results, String nextCursor, boolean hasMore) { +} diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/RichText.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/RichText.java new file mode 100644 index 00000000..58293257 --- /dev/null +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/RichText.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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 org.springframework.batch.extensions.notion; + +import tools.jackson.databind.PropertyNamingStrategies; +import tools.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +record RichText(String plainText) { +} diff --git a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/Sort.java b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/Sort.java index d42a51bb..e285e0a9 100644 --- a/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/Sort.java +++ b/spring-batch-notion/src/main/java/org/springframework/batch/extensions/notion/Sort.java @@ -15,9 +15,10 @@ */ package org.springframework.batch.extensions.notion; -import notion.api.v1.model.databases.query.sort.QuerySort; -import notion.api.v1.model.databases.query.sort.QuerySortDirection; -import notion.api.v1.model.databases.query.sort.QuerySortTimestamp; +import com.fasterxml.jackson.annotation.JsonProperty; +import tools.jackson.databind.EnumNamingStrategies; +import tools.jackson.databind.EnumNamingStrategies.SnakeCaseStrategy; +import tools.jackson.databind.annotation.EnumNaming; import java.util.Objects; @@ -81,66 +82,48 @@ public static Sort by(Timestamp timestamp) { /** * Timestamps associated with database entries. */ + @EnumNaming(SnakeCaseStrategy.class) public enum Timestamp { /** * The time the entry was created. */ - CREATED_TIME(QuerySortTimestamp.CreatedTime), + CREATED_TIME, /** * The time the entry was last edited. */ - LAST_EDITED_TIME(QuerySortTimestamp.LastEditedTime); - - private final QuerySortTimestamp querySortTimestamp; - - Timestamp(QuerySortTimestamp querySortTimestamp) { - this.querySortTimestamp = querySortTimestamp; - } - - private QuerySortTimestamp getQuerySortTimestamp() { - return querySortTimestamp; - } + LAST_EDITED_TIME; } /** * Sort directions. */ + @EnumNaming(SnakeCaseStrategy.class) public enum Direction { /** * Ascending direction. */ - ASCENDING(QuerySortDirection.Ascending), + ASCENDING, /** * Descending direction. */ - DESCENDING(QuerySortDirection.Descending); - - private final QuerySortDirection querySortDirection; - - Direction(QuerySortDirection querySortDirection) { - this.querySortDirection = querySortDirection; - } - - private QuerySortDirection getQuerySortDirection() { - return querySortDirection; - } + DESCENDING; } private Sort() { } - abstract QuerySort toQuerySort(); - private static final class PropertySort extends Sort { + @JsonProperty private final String property; + @JsonProperty private final Direction direction; private PropertySort(String property, Direction direction) { @@ -148,11 +131,6 @@ private PropertySort(String property, Direction direction) { this.direction = Objects.requireNonNull(direction); } - @Override - QuerySort toQuerySort() { - return new QuerySort(property, null, direction.getQuerySortDirection()); - } - @Override public String toString() { return "%s: %s".formatted(property, direction); @@ -162,8 +140,10 @@ public String toString() { private static final class TimestampSort extends Sort { + @JsonProperty private final Timestamp timestamp; + @JsonProperty private final Direction direction; private TimestampSort(Timestamp timestamp, Direction direction) { @@ -171,11 +151,6 @@ private TimestampSort(Timestamp timestamp, Direction direction) { this.direction = Objects.requireNonNull(direction); } - @Override - QuerySort toQuerySort() { - return new QuerySort(null, timestamp.getQuerySortTimestamp(), direction.getQuerySortDirection()); - } - @Override public String toString() { return "%s: %s".formatted(timestamp, direction); diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/FilterTests.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/FilterTests.java index 0b8e88c4..37204625 100644 --- a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/FilterTests.java +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/FilterTests.java @@ -15,25 +15,16 @@ */ package org.springframework.batch.extensions.notion; -import notion.api.v1.model.databases.query.filter.CompoundFilter; -import notion.api.v1.model.databases.query.filter.PropertyFilter; -import notion.api.v1.model.databases.query.filter.QueryTopLevelFilter; -import notion.api.v1.model.databases.query.filter.condition.CheckboxFilter; -import notion.api.v1.model.databases.query.filter.condition.FilesFilter; -import notion.api.v1.model.databases.query.filter.condition.MultiSelectFilter; -import notion.api.v1.model.databases.query.filter.condition.NumberFilter; -import notion.api.v1.model.databases.query.filter.condition.SelectFilter; -import notion.api.v1.model.databases.query.filter.condition.StatusFilter; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.FieldSource; +import tools.jackson.databind.json.JsonMapper; import java.util.List; -import java.util.function.Supplier; import java.util.stream.Stream; -import static org.assertj.core.api.BDDAssertions.then; import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; import static org.springframework.batch.extensions.notion.Filter.where; /** @@ -41,244 +32,272 @@ */ class FilterTests { + // Test cases from https://developers.notion.com/reference/post-database-query-filter + + private final JsonMapper jsonMapper = new JsonMapper(); + @ParameterizedTest @FieldSource({ "PROPERTY_FILTERS", "COMPOUND_FILTERS", "NESTED_FILTERS" }) - void toQueryTopLevelFilter(Filter underTest, QueryTopLevelFilter expected) { + void toJson(Filter underTest, String expected) throws Exception { // WHEN - QueryTopLevelFilter result = underTest.toQueryTopLevelFilter(); + String result = jsonMapper.writeValueAsString(underTest); // THEN - then(result).usingRecursiveComparison().isEqualTo(expected); + assertEquals(expected, result, true); } static final List CHECKBOX_FILTERS = Stream.of(true, false) .flatMap(value -> Stream.of( // - arguments( // - where().checkbox("property").isEqualTo(value), // - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setEquals(value); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - })), - arguments( // - where().checkbox("property").isNotEqualTo(value), // - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setDoesNotEqual(value); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - })))) + arguments(where().checkbox("Task completed").isEqualTo(value), """ + { + "filter": { + "property": "Task completed", + "checkbox": { + "equals": %s + } + } + } + """.formatted(value)), // + arguments(where().checkbox("Task completed").isNotEqualTo(value), """ + { + "filter": { + "property": "Task completed", + "checkbox": { + "does_not_equal": %s + } + } + } + """.formatted(value)))) .toList(); static final List FILES_FILTERS = List.of( // - arguments( // - where().files("property").isEmpty(), // - supply(() -> { - FilesFilter filesFilter = new FilesFilter(); - filesFilter.setEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setFile(filesFilter); - return propertyFilter; - })), - arguments( // - where().files("property").isNotEmpty(), // - supply(() -> { - FilesFilter filesFilter = new FilesFilter(); - filesFilter.setNotEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setFile(filesFilter); - return propertyFilter; - }))); + arguments(where().files("Blueprint").isEmpty(), """ + { + "filter": { + "property": "Blueprint", + "files": { + "is_empty": true + } + } + } + """), // + arguments(where().files("Blueprint").isNotEmpty(), """ + { + "filter": { + "property": "Blueprint", + "files": { + "is_not_empty": true + } + } + } + """)); static final List MULTI_SELECT_FILTERS = List.of( // - arguments( // - where().multiSelect("property").contains("value"), // - supply(() -> { - MultiSelectFilter multiSelectFilter = new MultiSelectFilter(); - multiSelectFilter.setContains("value"); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setMultiSelect(multiSelectFilter); - return propertyFilter; - })), - arguments( // - where().multiSelect("property").doesNotContain("value"), // - supply(() -> { - MultiSelectFilter multiSelectFilter = new MultiSelectFilter(); - multiSelectFilter.setDoesNotContain("value"); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setMultiSelect(multiSelectFilter); - return propertyFilter; - })), - arguments( // - where().multiSelect("property").isEmpty(), // - supply(() -> { - MultiSelectFilter multiSelectFilter = new MultiSelectFilter(); - multiSelectFilter.setEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setMultiSelect(multiSelectFilter); - return propertyFilter; - })), - arguments( // - where().multiSelect("property").isNotEmpty(), // - supply(() -> { - MultiSelectFilter multiSelectFilter = new MultiSelectFilter(); - multiSelectFilter.setNotEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setMultiSelect(multiSelectFilter); - return propertyFilter; - }))); + arguments(where().multiSelect("Programming language").contains("TypeScript"), """ + { + "filter": { + "property": "Programming language", + "multi_select": { + "contains": "TypeScript" + } + } + } + """), // + arguments(where().multiSelect("Programming language").doesNotContain("TypeScript"), """ + { + "filter": { + "property": "Programming language", + "multi_select": { + "does_not_contain": "TypeScript" + } + } + } + """), // + arguments(where().multiSelect("Programming language").isEmpty(), """ + { + "filter": { + "property": "Programming language", + "multi_select": { + "is_empty": true + } + } + } + """), // + arguments(where().multiSelect("Programming language").isNotEmpty(), """ + { + "filter": { + "property": "Programming language", + "multi_select": { + "is_not_empty": true + } + } + } + """)); static final List NUMBER_FILTERS = List.of( // - arguments( // - where().number("property").isEqualTo(42), // - supply(() -> { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setEquals(42); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setNumber(numberFilter); - return propertyFilter; - })), - arguments( // - where().number("property").isNotEqualTo(42), // - supply(() -> { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setDoesNotEqual(42); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setNumber(numberFilter); - return propertyFilter; - })), - arguments( // - where().number("property").isGreaterThan(42), // - supply(() -> { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setGreaterThan(42); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setNumber(numberFilter); - return propertyFilter; - })), - arguments( // - where().number("property").isGreaterThanOrEqualTo(42), // - supply(() -> { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setGreaterThanOrEqualTo(42); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setNumber(numberFilter); - return propertyFilter; - })), - arguments( // - where().number("property").isLessThan(42), // - supply(() -> { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setLessThan(42); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setNumber(numberFilter); - return propertyFilter; - })), - arguments( // - where().number("property").isLessThanOrEqualTo(42), // - supply(() -> { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setLessThanOrEqualTo(42); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setNumber(numberFilter); - return propertyFilter; - })), - arguments( // - where().number("property").isEmpty(), // - supply(() -> { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setNumber(numberFilter); - return propertyFilter; - })), - arguments( // - where().number("property").isNotEmpty(), // - supply(() -> { - NumberFilter numberFilter = new NumberFilter(); - numberFilter.setNotEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setNumber(numberFilter); - return propertyFilter; - }))); + arguments(where().number("Estimated working days").isEqualTo(42), """ + { + "filter": { + "property": "Estimated working days", + "number": { + "equals": 42 + } + } + } + """), // + arguments(where().number("Estimated working days").isNotEqualTo(42), """ + { + "filter": { + "property": "Estimated working days", + "number": { + "does_not_equal": 42 + } + } + } + """), // + arguments(where().number("Estimated working days").isGreaterThan(42), """ + { + "filter": { + "property": "Estimated working days", + "number": { + "greater_than": 42 + } + } + } + """), // + arguments(where().number("Estimated working days").isGreaterThanOrEqualTo(42), """ + { + "filter": { + "property": "Estimated working days", + "number": { + "greater_than_or_equal_to": 42 + } + } + } + """), // + arguments(where().number("Estimated working days").isLessThan(42), """ + { + "filter": { + "property": "Estimated working days", + "number": { + "less_than": 42 + } + } + } + """), // + arguments(where().number("Estimated working days").isLessThanOrEqualTo(42), """ + { + "filter": { + "property": "Estimated working days", + "number": { + "less_than_or_equal_to": 42 + } + } + } + """), // + arguments(where().number("Estimated working days").isEmpty(), """ + { + "filter": { + "property": "Estimated working days", + "number": { + "is_empty": true + } + } + } + """), // + arguments(where().number("Estimated working days").isNotEmpty(), """ + { + "filter": { + "property": "Estimated working days", + "number": { + "is_not_empty": true + } + } + } + """)); static final List SELECT_FILTERS = List.of( // - arguments( // - where().select("property").isEqualTo("value"), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setEquals("value"); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - })), - arguments( // - where().select("property").isNotEqualTo("value"), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setDoesNotEqual("value"); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - })), - arguments( // - where().select("property").isEmpty(), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - })), - arguments( // - where().select("property").isNotEmpty(), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setNotEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }))); + arguments(where().select("Backend framework").isEqualTo("Spring"), """ + { + "filter": { + "property": "Backend framework", + "select": { + "equals": "Spring" + } + } + } + """), // + arguments(where().select("Backend framework").isNotEqualTo("Spring"), """ + { + "filter": { + "property": "Backend framework", + "select": { + "does_not_equal": "Spring" + } + } + } + """), // + arguments(where().select("Backend framework").isEmpty(), """ + { + "filter": { + "property": "Backend framework", + "select": { + "is_empty": true + } + } + } + """), // + arguments(where().select("Backend framework").isNotEmpty(), """ + { + "filter": { + "property": "Backend framework", + "select": { + "is_not_empty": true + } + } + } + """)); static final List STATUS_FILTERS = List.of( // - arguments( // - where().status("property").isEqualTo("value"), // - supply(() -> { - StatusFilter statusFilter = new StatusFilter(); - statusFilter.setEquals("value"); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setStatus(statusFilter); - return propertyFilter; - })), - arguments( // - where().status("property").isNotEqualTo("value"), // - supply(() -> { - StatusFilter statusFilter = new StatusFilter(); - statusFilter.setDoesNotEqual("value"); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setStatus(statusFilter); - return propertyFilter; - })), - arguments( // - where().status("property").isEmpty(), // - supply(() -> { - StatusFilter statusFilter = new StatusFilter(); - statusFilter.setEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setStatus(statusFilter); - return propertyFilter; - })), - arguments( // - where().status("property").isNotEmpty(), // - supply(() -> { - StatusFilter statusFilter = new StatusFilter(); - statusFilter.setNotEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("property"); - propertyFilter.setStatus(statusFilter); - return propertyFilter; - }))); + arguments(where().status("Project status").isEqualTo("Not started"), """ + { + "filter": { + "property": "Project status", + "status": { + "equals": "Not started" + } + } + } + """), // + arguments(where().status("Project status").isNotEqualTo("Not started"), """ + { + "filter": { + "property": "Project status", + "status": { + "does_not_equal": "Not started" + } + } + } + """), // + arguments(where().status("Project status").isEmpty(), """ + { + "filter": { + "property": "Project status", + "status": { + "is_empty": true + } + } + } + """), // + arguments(where().status("Project status").isNotEmpty(), """ + { + "filter": { + "property": "Project status", + "status": { + "is_not_empty": true + } + } + } + """)); static final List PROPERTY_FILTERS = Stream.of( // CHECKBOX_FILTERS, // @@ -291,284 +310,246 @@ void toQueryTopLevelFilter(Filter underTest, QueryTopLevelFilter expected) { .toList(); static final List AND_FILTERS = List.of( // + arguments(where().checkbox("Complete").isEqualTo(true).and().number("Days").isGreaterThan(10), """ + { + "filter": { + "and": [ + { + "property": "Complete", + "checkbox": { + "equals": true + } + }, + { + "property": "Days", + "number": { + "greater_than": 10 + } + } + ] + } + } + """), + arguments(where().checkbox("Complete").isEqualTo(true).and(where().number("Days").isGreaterThan(10)), """ + { + "filter": { + "and": [ + { + "property": "Complete", + "checkbox": { + "equals": true + } + }, + { + "property": "Days", + "number": { + "greater_than": 10 + } + } + ] + } + } + """), arguments( // @formatter:off - where().checkbox("active").isEqualTo(false) - .and().select("another").isNotEmpty(), // + where().checkbox("Complete").isEqualTo(true) + .and(where().number("Days").isGreaterThan(10)) + .and().checkbox("Archived").isNotEqualTo(false) + .and(where().select("Language").isEmpty()), // @formatter:on - supply(() -> { - CompoundFilter compoundFilter = new CompoundFilter(); - - compoundFilter.setAnd(List.of( // - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setEquals(false); - PropertyFilter propertyFilter = new PropertyFilter("active"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - }), supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setNotEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("another"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }))); - - return compoundFilter; - })), - arguments( // @formatter:off - where().checkbox("active").isEqualTo(false) - .and(where().select("another").isNotEmpty()), - // @formatter:on - supply(() -> { - CompoundFilter compoundFilter = new CompoundFilter(); - - compoundFilter.setAnd(List.of( // - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setEquals(false); - PropertyFilter propertyFilter = new PropertyFilter("active"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - }), supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setNotEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("another"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }))); - - return compoundFilter; - })), - arguments( // @formatter:off - where().checkbox("active").isEqualTo(false) - .and(where().select("another").isNotEmpty()) - .and().checkbox("one-more").isNotEqualTo(true) - .and(where().select("another-more").isEmpty()), - // @formatter:on - supply(() -> { - CompoundFilter compoundFilter = new CompoundFilter(); - - compoundFilter.setAnd(List.of( // - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setEquals(false); - PropertyFilter propertyFilter = new PropertyFilter("active"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - }), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setNotEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("another"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }), // - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setDoesNotEqual(true); - PropertyFilter propertyFilter = new PropertyFilter("one-more"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - }), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("another-more"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }))); - - return compoundFilter; - }))); + """ + { + "filter": { + "and": [ + { + "property": "Complete", + "checkbox": { + "equals": true + } + }, + { + "property": "Days", + "number": { + "greater_than": 10 + } + }, + { + "property": "Archived", + "checkbox": { + "does_not_equal": false + } + }, + { + "property": "Language", + "select": { + "is_empty": true + } + } + ] + } + } + """)); static final List OR_FILTERS = List.of( // + arguments(where().checkbox("Complete").isEqualTo(true).or().number("Days").isGreaterThan(10), """ + { + "filter": { + "or": [ + { + "property": "Complete", + "checkbox": { + "equals": true + } + }, + { + "property": "Days", + "number": { + "greater_than": 10 + } + } + ] + } + } + """), + arguments(where().checkbox("Complete").isEqualTo(true).or(where().number("Days").isGreaterThan(10)), """ + { + "filter": { + "or": [ + { + "property": "Complete", + "checkbox": { + "equals": true + } + }, + { + "property": "Days", + "number": { + "greater_than": 10 + } + } + ] + } + } + """), arguments( // @formatter:off - where().checkbox("active").isEqualTo(false) - .or().select("another").isNotEmpty(), - // @formatter:on - supply(() -> { - CompoundFilter compoundFilter = new CompoundFilter(); - - compoundFilter.setOr(List.of( // - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setEquals(false); - PropertyFilter propertyFilter = new PropertyFilter("active"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - }), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setNotEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("another"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }))); - - return compoundFilter; - })), - arguments( // @formatter:off - where().checkbox("active").isEqualTo(false) - .or(where().select("another").isNotEmpty()), - // @formatter:on - supply(() -> { - CompoundFilter compoundFilter = new CompoundFilter(); - - compoundFilter.setOr(List.of( // - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setEquals(false); - PropertyFilter propertyFilter = new PropertyFilter("active"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - }), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setNotEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("another"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }))); - - return compoundFilter; - })), - arguments( // @formatter:off - where().checkbox("active").isEqualTo(false) - .or(where().select("another").isNotEmpty()) - .or().checkbox("one-more").isNotEqualTo(true) - .or(where().select("another-more").isEmpty()), + where().checkbox("Complete").isEqualTo(true) + .or(where().number("Days").isGreaterThan(10)) + .or().checkbox("Archived").isNotEqualTo(false) + .or(where().select("Language").isEmpty()), // @formatter:on - supply(() -> { - CompoundFilter compoundFilter = new CompoundFilter(); - - compoundFilter.setOr(List.of( // - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setEquals(false); - PropertyFilter propertyFilter = new PropertyFilter("active"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - }), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setNotEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("another"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }), // - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setDoesNotEqual(true); - PropertyFilter propertyFilter = new PropertyFilter("one-more"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - }), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("another-more"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }))); - - return compoundFilter; - }))); + """ + { + "filter": { + "or": [ + { + "property": "Complete", + "checkbox": { + "equals": true + } + }, + { + "property": "Days", + "number": { + "greater_than": 10 + } + }, + { + "property": "Archived", + "checkbox": { + "does_not_equal": false + } + }, + { + "property": "Language", + "select": { + "is_empty": true + } + } + ] + } + } + """)); static final List COMPOUND_FILTERS = Stream.of(AND_FILTERS, OR_FILTERS).flatMap(List::stream).toList(); static final List NESTED_FILTERS = List.of( // + arguments(where(where().checkbox("Task completed").isEqualTo(true)), """ + { + "filter": { + "property": "Task completed", + "checkbox": { + "equals": true + } + } + } + """), arguments( // @formatter:off - where(where().checkbox("active").isEqualTo(true)), + where().checkbox("Complete").isEqualTo(true) + .and(where().select("Language").isEmpty().or().select("Language").isEqualTo("Java")), // @formatter:on - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setEquals(true); - PropertyFilter propertyFilter = new PropertyFilter("active"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - })), + """ + { + "filter": { + "and": [ + { + "property": "Complete", + "checkbox": { + "equals": true + } + }, + { + "or": [ + { + "property": "Language", + "select": { + "is_empty": true + } + }, + { + "property": "Language", + "select": { + "equals": "Java" + } + } + ] + } + ] + } + } + """), arguments( // @formatter:off - where().checkbox("active").isEqualTo(true) - .and(where().select("another").isEmpty().or().select("another").isEqualTo("value")), + where(where().checkbox("Complete").isEqualTo(true) + .or().select("Language").isNotEmpty()) + .and().select("Backend framework").isEmpty(), // @formatter:on - supply(() -> { - CompoundFilter compoundFilter = new CompoundFilter(); - - compoundFilter.setAnd(List.of( // - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setEquals(true); - PropertyFilter propertyFilter = new PropertyFilter("active"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - }), // - supply(() -> { - CompoundFilter innerFilter = new CompoundFilter(); - - innerFilter.setOr(List.of( // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("another"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setEquals("value"); - PropertyFilter propertyFilter = new PropertyFilter("another"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }))); - - return innerFilter; - }))); - - return compoundFilter; - })), - arguments( // @formatter:off - where(where().checkbox("active").isEqualTo(false) - .or().select("another").isNotEmpty()) - .and().select("one-more").isEmpty(), - // @formatter:on - supply(() -> { - CompoundFilter compoundFilter = new CompoundFilter(); - - compoundFilter.setAnd(List.of( // - supply(() -> { - CompoundFilter innerFilter = new CompoundFilter(); - - innerFilter.setOr(List.of( // - supply(() -> { - CheckboxFilter checkboxFilter = new CheckboxFilter(); - checkboxFilter.setEquals(false); - PropertyFilter propertyFilter = new PropertyFilter("active"); - propertyFilter.setCheckbox(checkboxFilter); - return propertyFilter; - }), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setNotEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("another"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }))); - - return innerFilter; - }), // - supply(() -> { - SelectFilter selectFilter = new SelectFilter(); - selectFilter.setEmpty(true); - PropertyFilter propertyFilter = new PropertyFilter("one-more"); - propertyFilter.setSelect(selectFilter); - return propertyFilter; - }))); - - return compoundFilter; - }))); - - private static T supply(Supplier supplier) { - return supplier.get(); - } + """ + { + "filter": { + "and": [ + { + "or": [ + { + "property": "Complete", + "checkbox": { + "equals": true + } + }, + { + "property": "Language", + "select": { + "is_not_empty": true + } + } + ] + }, + { + "property": "Backend framework", + "select": { + "is_empty": true + } + } + ] + } + } + """)); } diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionJvmSdkTests.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionJvmSdkTests.java deleted file mode 100644 index 780a2f74..00000000 --- a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/NotionJvmSdkTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2024-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * 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 org.springframework.batch.extensions.notion; - -import com.tngtech.archunit.base.DescribedPredicate; -import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.junit.AnalyzeClasses; -import com.tngtech.archunit.junit.ArchTest; -import com.tngtech.archunit.lang.ArchRule; - -import static com.tngtech.archunit.base.DescribedPredicate.anyElementThat; -import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; - -/** - * @author Stefano Cordio - */ -@AnalyzeClasses(packagesOf = NotionDatabaseItemReader.class) -class NotionJvmSdkTests { - - private static final DescribedPredicate RESIDE_IN_NOTION_JVM_SDK_PACKAGE = // - resideInAPackage("notion.api.."); - - @ArchTest - void library_types_should_not_be_exposed(JavaClasses classes) { - // @formatter:off - ArchRule rule = methods() - .that().arePublic().or().areProtected() - .should().notHaveRawReturnType(RESIDE_IN_NOTION_JVM_SDK_PACKAGE) - .andShould().notHaveRawParameterTypes(anyElementThat(RESIDE_IN_NOTION_JVM_SDK_PACKAGE)); - // @formatter:on - rule.check(classes); - } - -} diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/PagePropertyTests.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/PagePropertyTests.java new file mode 100644 index 00000000..32d23404 --- /dev/null +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/PagePropertyTests.java @@ -0,0 +1,152 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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 org.springframework.batch.extensions.notion; + +import org.junit.jupiter.api.Test; +import org.springframework.batch.extensions.notion.PageProperty.RichTextProperty; +import org.springframework.batch.extensions.notion.PageProperty.TitleProperty; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.BDDAssertions.then; + +/** + * @author Stefano Cordio + */ +class PagePropertyTests { + + // Test cases from https://developers.notion.com/reference/page-property-values + + private final JsonMapper jsonMapper = new JsonMapper(); + + @Test + void richTextProperty() { + // GIVEN + String json = """ + { + "Description": { + "id": "HbZT", + "type": "rich_text", + "rich_text": [ + { + "type": "text", + "text": { + "content": "There is some ", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "There is some ", + "href": null + }, + { + "type": "text", + "text": { + "content": "text", + "link": null + }, + "annotations": { + "bold": true, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "text", + "href": null + }, + { + "type": "text", + "text": { + "content": " in this property!", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": " in this property!", + "href": null + } + ] + } + } + """; + Map expected = Map.of("Description", new RichTextProperty(List.of( // + new RichText("There is some "), // + new RichText("text"), // + new RichText(" in this property!")))); + // WHEN + Map result = jsonMapper.readValue(json, new TypeReference<>() { + }); + // THEN + then(result).isEqualTo(expected); + } + + @Test + void titleProperty() { + // GIVEN + String json = """ + { + "Title": { + "id": "title", + "type": "title", + "title": [ + { + "type": "text", + "text": { + "content": "A better title for the page", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "This is also not done", + "href": null + } + ] + } + } + """; + Map expected = Map.of("Title", + new TitleProperty(List.of(new RichText("This is also not done")))); + // WHEN + Map result = jsonMapper.readValue(json, new TypeReference<>() { + }); + // THEN + then(result).isEqualTo(expected); + } + +} diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/QueryRequestTests.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/QueryRequestTests.java new file mode 100644 index 00000000..764f3700 --- /dev/null +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/QueryRequestTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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 org.springframework.batch.extensions.notion; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.FieldSource; +import tools.jackson.databind.json.JsonMapper; + +import java.util.List; + +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; +import static org.springframework.batch.extensions.notion.Sort.Timestamp.CREATED_TIME; + +/** + * @author Stefano Cordio + */ +class QueryRequestTests { + + private final JsonMapper jsonMapper = new JsonMapper(); + + @ParameterizedTest + @FieldSource + void toJson(QueryRequest underTest, String expected) throws Exception { + // WHEN + String result = jsonMapper.writeValueAsString(underTest); + // THEN + assertEquals(expected, result, true); + } + + static List toJson = List.of( // + arguments(new QueryRequest(42, null, null), """ + { + "page_size" : 42 + } + """), // + arguments(new QueryRequest(42, "cursor", null), """ + { + "page_size" : 42, + "start_cursor" : "cursor" + } + """), // + arguments(new QueryRequest(42, null, null, Sort.by("property")), """ + { + "page_size" : 42, + "sorts" : [ + { + "direction" : "ascending", + "property" : "property" + } + ] + } + """), // + arguments(new QueryRequest(42, null, null, Sort.by("property"), Sort.by(CREATED_TIME)), """ + { + "page_size" : 42, + "sorts" : [ + { + "property" : "property", + "direction" : "ascending" + }, + { + "timestamp" : "created_time", + "direction" : "ascending" + } + ] + } + """)); + +} diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/RichTextTests.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/RichTextTests.java new file mode 100644 index 00000000..c68e6294 --- /dev/null +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/RichTextTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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 org.springframework.batch.extensions.notion; + +import org.junit.jupiter.api.Test; +import tools.jackson.databind.json.JsonMapper; + +import static org.assertj.core.api.BDDAssertions.then; + +/** + * @author Stefano Cordio + */ +class RichTextTests { + + private final JsonMapper jsonMapper = new JsonMapper(); + + @Test + void fromJson() { + // GIVEN + String json = """ + { + "type": "text", + "text": { + "content": "Some words ", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "Some words ", + "href": null + } + """; + RichText expected = new RichText("Some words "); + // WHEN + RichText result = jsonMapper.readValue(json, RichText.class); + // THEN + then(result).isEqualTo(expected); + } + +} diff --git a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/SortTests.java b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/SortTests.java index fbd387b7..6479d3f9 100644 --- a/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/SortTests.java +++ b/spring-batch-notion/src/test/java/org/springframework/batch/extensions/notion/SortTests.java @@ -15,22 +15,16 @@ */ package org.springframework.batch.extensions.notion; -import notion.api.v1.model.databases.query.sort.QuerySort; -import notion.api.v1.model.databases.query.sort.QuerySortDirection; -import notion.api.v1.model.databases.query.sort.QuerySortTimestamp; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.FieldSource; +import tools.jackson.databind.json.JsonMapper; import java.util.List; -import static notion.api.v1.model.databases.query.sort.QuerySortDirection.Ascending; -import static notion.api.v1.model.databases.query.sort.QuerySortDirection.Descending; -import static notion.api.v1.model.databases.query.sort.QuerySortTimestamp.CreatedTime; -import static notion.api.v1.model.databases.query.sort.QuerySortTimestamp.LastEditedTime; -import static org.assertj.core.api.BDDAssertions.from; import static org.assertj.core.api.BDDAssertions.then; import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; import static org.springframework.batch.extensions.notion.Sort.Direction.ASCENDING; import static org.springframework.batch.extensions.notion.Sort.Direction.DESCENDING; import static org.springframework.batch.extensions.notion.Sort.Timestamp.CREATED_TIME; @@ -41,28 +35,72 @@ */ class SortTests { + private final JsonMapper jsonMapper = new JsonMapper(); + @ParameterizedTest @FieldSource - void toQuerySort(Sort underTest, String property, QuerySortTimestamp timestamp, QuerySortDirection direction) { + void toJson(Sort underTest, String expected) throws Exception { // WHEN - QuerySort result = underTest.toQuerySort(); + String result = jsonMapper.writeValueAsString(underTest); // THEN - then(result) // - .returns(direction, from(QuerySort::getDirection)) - .returns(property, from(QuerySort::getProperty)) - .returns(timestamp, from(QuerySort::getTimestamp)); + assertEquals(expected, result, true); } - static List toQuerySort = List.of( // - arguments(Sort.by("property"), "property", null, Ascending), - arguments(Sort.by("property", ASCENDING), "property", null, Ascending), - arguments(Sort.by("property", DESCENDING), "property", null, Descending), - arguments(Sort.by(CREATED_TIME), null, CreatedTime, Ascending), - arguments(Sort.by(CREATED_TIME, ASCENDING), null, CreatedTime, Ascending), - arguments(Sort.by(CREATED_TIME, DESCENDING), null, CreatedTime, Descending), - arguments(Sort.by(LAST_EDITED_TIME), null, LastEditedTime, Ascending), - arguments(Sort.by(LAST_EDITED_TIME, ASCENDING), null, LastEditedTime, Ascending), - arguments(Sort.by(LAST_EDITED_TIME, DESCENDING), null, LastEditedTime, Descending)); + static List toJson = List.of( // + arguments(Sort.by("property"), """ + { + "property" : "property", + "direction" : "ascending" + } + """), // + arguments(Sort.by("property", ASCENDING), """ + { + "property" : "property", + "direction" : "ascending" + } + """), // + arguments(Sort.by("property", DESCENDING), """ + { + "property" : "property", + "direction" : "descending" + } + """), // + arguments(Sort.by(CREATED_TIME), """ + { + "timestamp" : "created_time", + "direction" : "ascending" + } + """), // + arguments(Sort.by(CREATED_TIME, ASCENDING), """ + { + "timestamp" : "created_time", + "direction" : "ascending" + } + """), // + arguments(Sort.by(CREATED_TIME, DESCENDING), """ + { + "timestamp" : "created_time", + "direction" : "descending" + } + """), // + arguments(Sort.by(LAST_EDITED_TIME), """ + { + "timestamp" : "last_edited_time", + "direction" : "ascending" + } + """), // + arguments(Sort.by(LAST_EDITED_TIME, ASCENDING), """ + { + "timestamp" : "last_edited_time", + "direction" : "ascending" + } + """), // + arguments(Sort.by(LAST_EDITED_TIME, DESCENDING), """ + { + "timestamp" : "last_edited_time", + "direction" : "descending" + } + """)); @ParameterizedTest @FieldSource