From 94dbf28831af1b2470deda3488f2ed9305f08840 Mon Sep 17 00:00:00 2001 From: Michael Schnell Date: Fri, 29 Dec 2023 12:02:05 +0100 Subject: [PATCH] Added new projection admin implementation --- README.md | 27 +- admin/README.md | 3 + admin/pom.xml | 182 +++++++++++++ .../admin/HttpProjectionAdminEventStore.java | 253 ++++++++++++++++++ .../HttpProjectionAdminEventStoreIT.java | 94 +++++++ .../org/fuin/esc/eshttp/ESHttpEventStore.java | 52 ++-- pom.xml | 1 + .../esc/spi}/ProjectionJavaScriptBuilder.java | 3 +- .../spi}/ProjectionJavaScriptBuilderTest.java | 3 +- 9 files changed, 569 insertions(+), 49 deletions(-) create mode 100644 admin/README.md create mode 100644 admin/pom.xml create mode 100644 admin/src/main/java/org/fuin/esc/admin/HttpProjectionAdminEventStore.java create mode 100644 admin/src/test/java/org/fuin/esc/admin/HttpProjectionAdminEventStoreIT.java rename {eshttp/src/main/java/org/fuin/esc/eshttp => spi/src/main/java/org/fuin/esc/spi}/ProjectionJavaScriptBuilder.java (98%) rename {eshttp/src/test/java/org/fuin/esc/eshttp => spi/src/test/java/org/fuin/esc/spi}/ProjectionJavaScriptBuilderTest.java (98%) diff --git a/README.md b/README.md index 08230876..afd5f2c7 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,23 @@ Defines a common event store Java interface and provides some adapters (like for ## Status ![Warning](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/warning.gif) **This is work in progress** ![Warning](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/warning.gif) -| Module | Description | Status | Comment | -|:-------|:------------|--------|:--------| -| [esc-api](api) | Defines the event store commons API. | ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~92% | -| [esc-http](eshttp) | HTTP adapter for Greg Young's [event store](https://www.geteventstore.com/)| ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~66% | -| [esc-esjc](esjc) | [Event Store Java Client](https://github.com/msemys/esjc) adapter for Greg Young's [event store](https://www.geteventstore.com/)| ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~80% | -| [esc-grpc](grpc) | [Event Store DB Client](https://github.com/EventStore/EventStoreDB-Client-Java) adapter for Greg Young's [event store](https://www.geteventstore.com/)| ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~80% | -| [esc-jpa](jpa) | JPA adapter | ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~59% | -| [esc-mem](mem) | In-memory implementation | ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~60% | -| [esc-spi](spi) | Helper classes for adapters and implementations | ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~67% | -| [esc-test](test) | Cucumber tests for adapters and implementations | ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Subscriptions not tested yet | +| Module | Description | Status | Comment | +|:------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------|--------|:-----------------------------| +| [esc-api](api) | Defines the event store commons API. | ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~92% | +| [esc-http-admin](admin) | HTTP projection admin adapter for Greg Young's [event store](https://www.geteventstore.com/) | ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~80% | +| [esc-grpc](grpc) | [Event Store DB Client](https://github.com/EventStore/EventStoreDB-Client-Java) adapter for Greg Young's [event store](https://www.geteventstore.com/) | ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~80% | +| [esc-jpa](jpa) | JPA adapter | ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~59% | +| [esc-mem](mem) | In-memory implementation | ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~60% | +| [esc-spi](spi) | Helper classes for adapters and implementations | ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Test coverage ~67% | +| [esc-test](test) | Cucumber tests for adapters and implementations | ![OK](https://raw.githubusercontent.com/fuinorg/event-store-commons/master/doc/ok.png) | Subscriptions not tested yet | + +Deprecated modules: + +| Module | Description | Comment | +|:-------------------|:-------------------------------------------------------------|:------------------------------------------------------| +| [esc-http](eshttp) | HTTP adapter for Greg Young's event store | No longer supported by event store (use GRPC instead) | +| [esc-esjc](esjc) | Event Store Java Client adapter for Greg Young's event store | No longer supported by event store (use GRPC instead) | + ## Architecture ![Layers](https://raw.github.com/fuinorg/event-store-commons/master/doc/event-store-commons.png) diff --git a/admin/README.md b/admin/README.md new file mode 100644 index 00000000..00817181 --- /dev/null +++ b/admin/README.md @@ -0,0 +1,3 @@ +# esc-http-admin +HTTP based implementation of the projection admin API. + diff --git a/admin/pom.xml b/admin/pom.xml new file mode 100644 index 00000000..6df6c0ff --- /dev/null +++ b/admin/pom.xml @@ -0,0 +1,182 @@ + + + + 4.0.0 + + + org.fuin.esc + esc-parent + 0.7.0-SNAPSHOT + + + esc-http-admin + jar + HTTP based implementation of the projection administration API. + + + + + + + org.fuin.esc + esc-api + + + + org.fuin.esc + esc-spi + + + + jakarta.validation + jakarta.validation-api + true + + + + org.slf4j + slf4j-api + + + + + + org.junit.jupiter + junit-jupiter + test + + + + org.assertj + assertj-core + test + + + + nl.jqno.equalsverifier + equalsverifier + test + + + + ch.qos.logback + logback-classic + test + + + + org.hibernate.validator + hibernate-validator + test + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.apache.maven.plugins + maven-source-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + org.apache.maven.plugins + maven-jar-plugin + + + **/* + + + + org.fuin.esc.prjadmin + + + + + + + org.apache.maven.plugins + maven-jdeps-plugin + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + io.fabric8 + docker-maven-plugin + + + + + eventstore/eventstore:${eventstore.version} + + + bridge + + + 1113:1113 + 2113:2113 + + + TRUE + All + true + /tmp/log-eventstore + + + false + + + + http://localhost:2113/web/index.html#/ + GET + + + + + + + + + + + + start-images + pre-integration-test + + start + + + + stop-images + post-integration-test + + stop + + + + + + + + + + + diff --git a/admin/src/main/java/org/fuin/esc/admin/HttpProjectionAdminEventStore.java b/admin/src/main/java/org/fuin/esc/admin/HttpProjectionAdminEventStore.java new file mode 100644 index 00000000..7324e3fa --- /dev/null +++ b/admin/src/main/java/org/fuin/esc/admin/HttpProjectionAdminEventStore.java @@ -0,0 +1,253 @@ +package org.fuin.esc.admin; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import org.fuin.esc.api.*; +import org.fuin.esc.spi.ProjectionJavaScriptBuilder; +import org.fuin.esc.spi.TenantStreamId; +import org.fuin.objects4j.common.ConstraintViolationException; +import org.fuin.objects4j.common.Contract; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.List; + +/** + * HTTP based eventstore projection admin implementation. + */ +public final class HttpProjectionAdminEventStore implements ProjectionAdminEventStore { + + private static final Logger LOG = LoggerFactory.getLogger(HttpProjectionAdminEventStore.class); + + private final HttpClient httpClient; + + private final URL url; + + private final TenantId tenantId; + + private final Duration timeout; + + public HttpProjectionAdminEventStore(@NotNull final HttpClient httpClient, + @NotNull final URL url) { + this(httpClient, url, null, null); + } + + public HttpProjectionAdminEventStore(@NotNull final HttpClient httpClient, + @NotNull final URL url, + @Nullable final TenantId tenantId, + @Nullable final Duration timeout) { + Contract.requireArgNotNull("httpClient", httpClient); + Contract.requireArgNotNull("url", url); + this.httpClient = httpClient; + this.url = url; + this.tenantId = tenantId; + this.timeout = timeout == null ? Duration.of(10, ChronoUnit.SECONDS) : timeout; + } + + @Override + public ProjectionAdminEventStore open() { + // Do nothing + return this; + } + + @Override + public void close() { + // Do nothing + } + + @Override + public boolean projectionExists(StreamId projectionId) { + Contract.requireArgNotNull("projectionId", projectionId); + requireProjection(projectionId); + + final TenantStreamId pid = new TenantStreamId(tenantId, projectionId); + final String msg = "projectionExists(" + pid + ")"; + + final URI uri = URI.create(url.toString() + "projection/" + pid.asString() + "/state"); + + final HttpRequest request = HttpRequest.newBuilder().uri(uri) + .setHeader("Accept", "application/json") + .timeout(timeout) + .GET() + .build(); + LOG.debug("{} REQUEST: {}", msg, request); + try { + final HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + LOG.debug("{} RESPONSE: {}", msg, response.statusCode()); + if (response.statusCode() == 404) { + return false; + } + if (response.statusCode() == 200) { + return true; + } + throw new RuntimeException(msg + " [Status=" + response.statusCode() + "]"); + } catch (final InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new RuntimeException(msg, ex); + } catch (final IOException ex) { + throw new RuntimeException(msg, ex); + } + + } + + @Override + public void enableProjection(StreamId projectionId) throws StreamNotFoundException { + enableDisable(new TenantStreamId(tenantId, projectionId), "enable"); + } + + @Override + public void disableProjection(StreamId projectionId) throws StreamNotFoundException { + enableDisable(new TenantStreamId(tenantId, projectionId), "disable"); + } + + @Override + public void createProjection(StreamId projectionId, boolean enable, @NotNull TypeName... eventType) throws StreamAlreadyExistsException { + Contract.requireArgNotNull("eventType", eventType); + createProjection(projectionId, enable, Arrays.asList(eventType)); + } + + @Override + public void createProjection(StreamId projectionId, boolean enable, List eventTypes) throws StreamAlreadyExistsException { + Contract.requireArgNotNull("projectionId", projectionId); + Contract.requireArgNotNull("eventTypes", eventTypes); + requireProjection(projectionId); + + final TenantStreamId pid = new TenantStreamId(tenantId, projectionId); + final String msg = "createProjection(" + pid + "," + enable + type2str(eventTypes) + ")"; + + + final URI uri = URI.create(url.toString() + "projections/continuous?name=" + pid.asString() + "&emit=yes&checkpoints=yes&enabled=" + yesNo(enable)); + + final String javascript = new ProjectionJavaScriptBuilder(pid).types(eventTypes).build(); + final HttpRequest request = HttpRequest.newBuilder().uri(uri) + .setHeader("Accept", "application/json") + .setHeader("Content-Type", "application/json; charset=utf-8") + .timeout(timeout) + .POST(HttpRequest.BodyPublishers.ofString(javascript, StandardCharsets.UTF_8)) + .build(); + + LOG.debug("{} REQUEST: {}", msg, request); + + try { + final HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + LOG.debug("{} RESPONSE: {}", msg, response.statusCode()); + if (response.statusCode() == 201) { + // CREATED + return; + } + throw new RuntimeException(msg + " [Status=" + response.statusCode() + "]"); + } catch (final InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new RuntimeException(msg, ex); + } catch (final IOException ex) { + throw new RuntimeException(msg, ex); + } + } + + @Override + public void deleteProjection(StreamId projectionId) throws StreamNotFoundException { + Contract.requireArgNotNull("projectionId", projectionId); + requireProjection(projectionId); + + final TenantStreamId pid = new TenantStreamId(tenantId, projectionId); + final String msg = "deleteProjection(" + pid + ")"; + + final URI uri = URI.create(url.toString() + "projection/" + projectionId.asString() + "?deleteCheckpointStream=yes&deleteStateStream=yes&deleteEmittedStreams=yes"); + + final HttpRequest request = HttpRequest.newBuilder().uri(uri).timeout(timeout).DELETE().build(); + LOG.debug("{} DELETE: {}", msg, request); + try { + final HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + LOG.debug("{} RESPONSE: {}", msg, response.statusCode()); + if (response.statusCode() == 200) { + return; + } + if (response.statusCode() == 404) { + throw new StreamNotFoundException(pid); + } + throw new RuntimeException(msg + " [Status=" + response.statusCode() + "]"); + + } catch (final InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new RuntimeException(msg, ex); + } catch (final IOException ex) { + throw new RuntimeException(msg, ex); + } + } + + private void enableDisable(final TenantStreamId projectionId, final String action) { + + Contract.requireArgNotNull("projectionId", projectionId); + requireProjection(projectionId); + + final String msg = action + "Projection(" + projectionId + ")"; + + final URI uri = URI.create(url.toString() + "projection/" + projectionId.asString() + "/command/" + action); + + final HttpRequest request = HttpRequest.newBuilder().uri(uri) + .setHeader("Accept", "application/json") + .setHeader("Content-Type", "application/json; charset=utf-8") + .timeout(timeout) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + LOG.debug("{} REQUEST: {}", msg, request); + try { + final HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + LOG.debug("{} RESPONSE: {}", msg, response.statusCode()); + if (response.statusCode() == 200) { + return; + } + if (response.statusCode() == 404) { + throw new StreamNotFoundException(projectionId); + } + throw new RuntimeException(msg + " [Status=" + response.statusCode() + "]"); + } catch (final InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new RuntimeException(msg, ex); + } catch (final IOException ex) { + throw new RuntimeException(msg, ex); + } + } + + /** + * Converts a boolean value to "yes" or "no". + * + * @param b Boolean value to convert. + * @return String "yes" or "no". + */ + static String yesNo(final boolean b) { + if (b) { + return "yes"; + } + return "no"; + } + + static String type2str(final List eventTypes) { + if (eventTypes == null) { + return ""; + } + final StringBuilder sb = new StringBuilder(); + for (final TypeName eventType : eventTypes) { + sb.append(","); + sb.append(eventType.asBaseType()); + } + return sb.toString(); + } + + static void requireProjection(final StreamId projectionId) { + if (!projectionId.isProjection()) { + throw new ConstraintViolationException("The stream identifier is not a projection id"); + } + } + +} diff --git a/admin/src/test/java/org/fuin/esc/admin/HttpProjectionAdminEventStoreIT.java b/admin/src/test/java/org/fuin/esc/admin/HttpProjectionAdminEventStoreIT.java new file mode 100644 index 00000000..8a451ec0 --- /dev/null +++ b/admin/src/test/java/org/fuin/esc/admin/HttpProjectionAdminEventStoreIT.java @@ -0,0 +1,94 @@ +package org.fuin.esc.admin; + +import org.fuin.esc.api.ProjectionStreamId; +import org.fuin.esc.api.TypeName; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.Authenticator; +import java.net.MalformedURLException; +import java.net.PasswordAuthentication; +import java.net.URL; +import java.net.http.HttpClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests the {@link HttpProjectionAdminEventStore} class. + */ +class HttpProjectionAdminEventStoreIT { + + private static HttpClient httpClient; + + private HttpProjectionAdminEventStore testee; + + @BeforeAll + static void beforeAll() { + httpClient = HttpClient.newBuilder() + .authenticator(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication("admin", "changeit".toCharArray()); + } + }) + .build(); + } + + @BeforeEach + void beforeEach() throws MalformedURLException { + testee = new HttpProjectionAdminEventStore(httpClient, new URL("http://127.0.0.1:2113/")); + } + + @Test + void testProjectionNotExists() { + assertThat(testee.projectionExists(new ProjectionStreamId("test-not-existing"))).isFalse(); + } + + @Test + void testEnableDisableProjection() { + + // GIVEN + final ProjectionStreamId projectionId = new ProjectionStreamId("test-disabled"); + testee.createProjection(projectionId, false, new TypeName("one"), new TypeName("two")); + + // WHEN + testee.enableProjection(projectionId); + + // THEN + // TODO assertThat(testee.projectionEnabled()).isTrue(); + + } + + @Test + void testCreateAndExistsProjection() { + + // GIVEN + final ProjectionStreamId projectionId = new ProjectionStreamId("test-create"); + assertThat(testee.projectionExists(projectionId)).isFalse(); + + // WHEN + testee.createProjection(projectionId, true, new TypeName("one"), new TypeName("two")); + + // THEN + assertThat(testee.projectionExists(projectionId)).isTrue(); + + } + + @Test + void testDeleteProjection() { + + // GIVEN + final ProjectionStreamId projectionId = new ProjectionStreamId("test-delete"); + testee.createProjection(projectionId, false, new TypeName("one"), new TypeName("two")); + assertThat(testee.projectionExists(projectionId)).isTrue(); + + // WHEN + testee.deleteProjection(projectionId); + + // THEN + assertThat(testee.projectionExists(projectionId)).isFalse(); + + } + +} diff --git a/eshttp/src/main/java/org/fuin/esc/eshttp/ESHttpEventStore.java b/eshttp/src/main/java/org/fuin/esc/eshttp/ESHttpEventStore.java index d4c749aa..8f720f85 100644 --- a/eshttp/src/main/java/org/fuin/esc/eshttp/ESHttpEventStore.java +++ b/eshttp/src/main/java/org/fuin/esc/eshttp/ESHttpEventStore.java @@ -17,22 +17,7 @@ */ package org.fuin.esc.eshttp; -import static org.fuin.esc.api.ExpectedVersion.ANY; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadFactory; - import jakarta.validation.constraints.NotNull; - import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; @@ -52,32 +37,27 @@ import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; import org.apache.http.impl.nio.client.HttpAsyncClients; import org.apache.http.util.EntityUtils; -import org.fuin.esc.api.CommonEvent; -import org.fuin.esc.api.EventNotFoundException; -import org.fuin.esc.api.ExpectedVersion; -import org.fuin.esc.api.SimpleStreamId; -import org.fuin.esc.api.StreamAlreadyExistsException; -import org.fuin.esc.api.StreamDeletedException; -import org.fuin.esc.api.StreamEventsSlice; -import org.fuin.esc.api.StreamId; -import org.fuin.esc.api.StreamNotFoundException; -import org.fuin.esc.api.StreamReadOnlyException; -import org.fuin.esc.api.StreamState; -import org.fuin.esc.api.TenantId; -import org.fuin.esc.api.TypeName; -import org.fuin.esc.api.WrongExpectedVersionException; -import org.fuin.esc.spi.AbstractReadableEventStore; -import org.fuin.esc.spi.DeserializerRegistry; -import org.fuin.esc.spi.EnhancedMimeType; -import org.fuin.esc.spi.EscSpiUtils; -import org.fuin.esc.spi.SerDeserializerRegistry; -import org.fuin.esc.spi.SerializerRegistry; -import org.fuin.esc.spi.TenantStreamId; +import org.fuin.esc.api.*; +import org.fuin.esc.spi.*; import org.fuin.objects4j.common.ConstraintViolationException; import org.fuin.objects4j.common.Contract; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; + +import static org.fuin.esc.api.ExpectedVersion.ANY; + /** * Implementation that connects to the http://www.geteventstore.com via HTTP API. */ diff --git a/pom.xml b/pom.xml index 48caca5d..ae75df61 100644 --- a/pom.xml +++ b/pom.xml @@ -332,6 +332,7 @@ api spi + admin mem jpa eshttp diff --git a/eshttp/src/main/java/org/fuin/esc/eshttp/ProjectionJavaScriptBuilder.java b/spi/src/main/java/org/fuin/esc/spi/ProjectionJavaScriptBuilder.java similarity index 98% rename from eshttp/src/main/java/org/fuin/esc/eshttp/ProjectionJavaScriptBuilder.java rename to spi/src/main/java/org/fuin/esc/spi/ProjectionJavaScriptBuilder.java index 7f5ef8e6..a00889e7 100644 --- a/eshttp/src/main/java/org/fuin/esc/eshttp/ProjectionJavaScriptBuilder.java +++ b/spi/src/main/java/org/fuin/esc/spi/ProjectionJavaScriptBuilder.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.esc.eshttp; +package org.fuin.esc.spi; import java.util.List; @@ -23,7 +23,6 @@ import org.fuin.esc.api.StreamId; import org.fuin.esc.api.TypeName; -import org.fuin.esc.spi.TenantStreamId; import org.fuin.objects4j.common.Contract; /** diff --git a/eshttp/src/test/java/org/fuin/esc/eshttp/ProjectionJavaScriptBuilderTest.java b/spi/src/test/java/org/fuin/esc/spi/ProjectionJavaScriptBuilderTest.java similarity index 98% rename from eshttp/src/test/java/org/fuin/esc/eshttp/ProjectionJavaScriptBuilderTest.java rename to spi/src/test/java/org/fuin/esc/spi/ProjectionJavaScriptBuilderTest.java index 51e4554d..83d7865a 100644 --- a/eshttp/src/test/java/org/fuin/esc/eshttp/ProjectionJavaScriptBuilderTest.java +++ b/spi/src/test/java/org/fuin/esc/spi/ProjectionJavaScriptBuilderTest.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.esc.eshttp; +package org.fuin.esc.spi; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; @@ -25,6 +25,7 @@ import org.fuin.esc.api.SimpleStreamId; import org.fuin.esc.api.TypeName; +import org.fuin.esc.spi.ProjectionJavaScriptBuilder; import org.junit.jupiter.api.Test; /**