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;
/**