From 5f7093c6aa06e74f2ac89e7157cc8ebc4a4e13f8 Mon Sep 17 00:00:00 2001 From: Ivan Senic Date: Thu, 16 Feb 2023 11:35:49 +0100 Subject: [PATCH 1/2] closes 105: delete collection command implementation --- .../jsonapi/api/model/command/Command.java | 2 + .../command/impl/CreateCollectionCommand.java | 4 +- .../command/impl/DeleteCollectionCommand.java | 23 +++ .../model/impl/CreateNamespaceOperation.java | 2 +- .../model/impl/DeleteCollectionOperation.java | 33 +++++ .../model/impl/DeleteCollectionResolver.java | 24 ++++ .../impl/DeleteCollectionCommandTest.java | 66 +++++++++ .../v1/NamespaceResourceIntegrationTest.java | 132 +++++++++++++++--- .../impl/DeleteCollectionOperationTest.java | 56 ++++++++ .../impl/DeleteCollectionResolverTest.java | 51 +++++++ 10 files changed, 374 insertions(+), 19 deletions(-) create mode 100644 src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/DeleteCollectionCommand.java create mode 100644 src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteCollectionOperation.java create mode 100644 src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteCollectionResolver.java create mode 100644 src/test/java/io/stargate/sgv2/jsonapi/api/model/command/impl/DeleteCollectionCommandTest.java create mode 100644 src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteCollectionOperationTest.java create mode 100644 src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteCollectionResolverTest.java diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Command.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Command.java index 497524186d..1640094c00 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Command.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Command.java @@ -5,6 +5,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.CountDocumentsCommands; import io.stargate.sgv2.jsonapi.api.model.command.impl.CreateCollectionCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.CreateNamespaceCommand; +import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteCollectionCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteOneCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.FindCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.FindOneAndUpdateCommand; @@ -38,6 +39,7 @@ @JsonSubTypes.Type(value = CountDocumentsCommands.class), @JsonSubTypes.Type(value = CreateNamespaceCommand.class), @JsonSubTypes.Type(value = CreateCollectionCommand.class), + @JsonSubTypes.Type(value = DeleteCollectionCommand.class), @JsonSubTypes.Type(value = DeleteOneCommand.class), @JsonSubTypes.Type(value = FindCommand.class), @JsonSubTypes.Type(value = FindOneCommand.class), diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/CreateCollectionCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/CreateCollectionCommand.java index f963c28585..7fa3fc0fad 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/CreateCollectionCommand.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/CreateCollectionCommand.java @@ -3,14 +3,14 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import io.stargate.sgv2.jsonapi.api.model.command.NamespaceCommand; import javax.annotation.Nullable; -import javax.validation.constraints.NotNull; +import javax.validation.constraints.NotBlank; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Schema; @Schema(description = "Command that creates a collection.") @JsonTypeName("createCollection") public record CreateCollectionCommand( - @NotNull + @NotBlank @Schema( description = "Name of the collection", implementation = Object.class, diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/DeleteCollectionCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/DeleteCollectionCommand.java new file mode 100644 index 0000000000..f1adc2959b --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/DeleteCollectionCommand.java @@ -0,0 +1,23 @@ +package io.stargate.sgv2.jsonapi.api.model.command.impl; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.stargate.sgv2.jsonapi.api.model.command.NamespaceCommand; +import javax.validation.constraints.NotBlank; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +/** + * Command for deleting a collection. + * + * @param name Name of the collection + */ +@Schema(description = "Command that deletes a collection if one exists.") +@JsonTypeName("deleteCollection") +public record DeleteCollectionCommand( + @NotBlank + @Schema( + description = "Name of the collection", + implementation = Object.class, + type = SchemaType.OBJECT) + String name) + implements NamespaceCommand {} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/CreateNamespaceOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/CreateNamespaceOperation.java index 0b1fe7e64b..654990c6bf 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/CreateNamespaceOperation.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/CreateNamespaceOperation.java @@ -18,7 +18,7 @@ public record CreateNamespaceOperation(String name, String replicationMap) imple // simple pattern for the cql private static final String CREATE_KEYSPACE_CQL = - "CREATE KEYSPACE IF NOT EXISTS \"%s\" WITH REPLICATION = %s;\n"; + "CREATE KEYSPACE IF NOT EXISTS \"%s\" WITH REPLICATION = %s;"; /** {@inheritDoc} */ @Override diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteCollectionOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteCollectionOperation.java new file mode 100644 index 0000000000..57ff973b6e --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteCollectionOperation.java @@ -0,0 +1,33 @@ +package io.stargate.sgv2.jsonapi.service.operation.model.impl; + +import io.smallrye.mutiny.Uni; +import io.stargate.bridge.proto.QueryOuterClass; +import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; +import io.stargate.sgv2.jsonapi.api.model.command.CommandResult; +import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor; +import io.stargate.sgv2.jsonapi.service.operation.model.Operation; +import java.util.function.Supplier; + +/** + * Implementation of the delete collection. + * + * @param context Command context, carries namespace of the collection. + * @param name Collection name. + */ +public record DeleteCollectionOperation(CommandContext context, String name) implements Operation { + + private static final String DROP_TABLE_CQL = "DROP TABLE IF EXISTS %s.%s;"; + + @Override + public Uni> execute(QueryExecutor queryExecutor) { + String cql = DROP_TABLE_CQL.formatted(context.namespace(), name); + QueryOuterClass.Query query = QueryOuterClass.Query.newBuilder().setCql(cql).build(); + + // execute + return queryExecutor + .executeSchemaChange(query) + + // if we have a result always respond positively + .map(any -> new SchemaChangeResult(true)); + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteCollectionResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteCollectionResolver.java new file mode 100644 index 0000000000..ea6b9ce430 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteCollectionResolver.java @@ -0,0 +1,24 @@ +package io.stargate.sgv2.jsonapi.service.resolver.model.impl; + +import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; +import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteCollectionCommand; +import io.stargate.sgv2.jsonapi.service.operation.model.Operation; +import io.stargate.sgv2.jsonapi.service.operation.model.impl.DeleteCollectionOperation; +import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; +import javax.enterprise.context.ApplicationScoped; + +/** + * Resolver for the {@link DeleteCollectionCommand}. + */ +@ApplicationScoped +public class DeleteCollectionResolver implements CommandResolver { + @Override + public Class getCommandClass() { + return DeleteCollectionCommand.class; + } + + @Override + public Operation resolveCommand(CommandContext ctx, DeleteCollectionCommand command) { + return new DeleteCollectionOperation(ctx, command.name()); + } +} diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/impl/DeleteCollectionCommandTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/impl/DeleteCollectionCommandTest.java new file mode 100644 index 0000000000..694d83a2e6 --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/impl/DeleteCollectionCommandTest.java @@ -0,0 +1,66 @@ +package io.stargate.sgv2.jsonapi.api.model.command.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.stargate.sgv2.common.testprofiles.NoGlobalResourcesTestProfile; +import java.util.Set; +import javax.inject.Inject; +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(NoGlobalResourcesTestProfile.Impl.class) +class DeleteCollectionCommandTest { + + @Inject ObjectMapper objectMapper; + + @Inject Validator validator; + + @Nested + class Validation { + + @Test + public void noName() throws Exception { + String json = + """ + { + "deleteCollection": { + } + } + """; + + DeleteCollectionCommand command = objectMapper.readValue(json, DeleteCollectionCommand.class); + Set> result = validator.validate(command); + + assertThat(result) + .isNotEmpty() + .extracting(ConstraintViolation::getMessage) + .contains("must not be blank"); + } + + @Test + public void nameBlank() throws Exception { + String json = + """ + { + "deleteCollection": { + "name": " " + } + } + """; + + DeleteCollectionCommand command = objectMapper.readValue(json, DeleteCollectionCommand.class); + Set> result = validator.validate(command); + + assertThat(result) + .isNotEmpty() + .extracting(ConstraintViolation::getMessage) + .contains("must not be blank"); + } + } +} diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/NamespaceResourceIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/NamespaceResourceIntegrationTest.java index 0a324db404..e17ca8336f 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/NamespaceResourceIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/NamespaceResourceIntegrationTest.java @@ -31,20 +31,71 @@ public static void enableLog() { } @Nested - class PostCommand { + class CreateCollection { @Test public void happyPath() { String json = - String.format( - """ - { - "createCollection": { - "name": "%s" - } - } - """, - "col" + RandomStringUtils.randomNumeric(16)); + """ + { + "createCollection": { + "name": "%s" + } + } + """ + .formatted("col" + RandomStringUtils.randomNumeric(16)); + + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(NamespaceResource.BASE_PATH, keyspaceId.asInternal()) + .then() + .statusCode(200) + .body("status.ok", is(1)); + } + } + + @Nested + class DeleteCollection { + + @Test + public void happyPath() { + String collection = RandomStringUtils.randomAlphabetic(16); + + // first create + String createJson = + """ + { + "createCollection": { + "name": "%s" + } + } + """ + .formatted(collection); + + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(createJson) + .when() + .post(NamespaceResource.BASE_PATH, keyspaceId.asInternal()) + .then() + .statusCode(200) + .body("status.ok", is(1)); + + // then delete + String json = + """ + { + "deleteCollection": { + "name": "%s" + } + } + """ + .formatted(collection); + given() .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) .contentType(ContentType.JSON) @@ -52,7 +103,56 @@ public void happyPath() { .when() .post(NamespaceResource.BASE_PATH, keyspaceId.asInternal()) .then() - .statusCode(200); + .statusCode(200) + .body("status.ok", is(1)); + } + + @Test + public void notExisting() { + String collection = RandomStringUtils.randomAlphabetic(16); + + // delete not existing + String json = + """ + { + "deleteCollection": { + "name": "%s" + } + } + """ + .formatted(collection); + + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(NamespaceResource.BASE_PATH, keyspaceId.asInternal()) + .then() + .statusCode(200) + .body("status.ok", is(1)); + } + + @Test + public void invalidCommand() { + String json = + """ + { + "deleteCollection": { + } + } + """; + + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(NamespaceResource.BASE_PATH, keyspaceId.asInternal()) + .then() + .statusCode(200) + .body("errors[0].message", is(not(blankString()))) + .body("errors[0].exceptionClass", is("ConstraintViolationException")); } } @@ -92,11 +192,11 @@ public void malformedBody() { public void unknownCommand() { String json = """ - { - "unknownCommand": { - } - } - """; + { + "unknownCommand": { + } + } + """; given() .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteCollectionOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteCollectionOperationTest.java new file mode 100644 index 0000000000..76f22f99fe --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteCollectionOperationTest.java @@ -0,0 +1,56 @@ +package io.stargate.sgv2.jsonapi.service.operation.model.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; +import io.stargate.sgv2.common.bridge.AbstractValidatingStargateBridgeTest; +import io.stargate.sgv2.common.testprofiles.NoGlobalResourcesTestProfile; +import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; +import io.stargate.sgv2.jsonapi.api.model.command.CommandResult; +import io.stargate.sgv2.jsonapi.api.model.command.CommandStatus; +import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor; +import java.util.function.Supplier; +import javax.inject.Inject; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(NoGlobalResourcesTestProfile.Impl.class) +public class DeleteCollectionOperationTest extends AbstractValidatingStargateBridgeTest { + @Inject QueryExecutor queryExecutor; + + @Nested + class Execute { + + @Test + public void happyPath() throws Exception { + String namespace = RandomStringUtils.randomAlphanumeric(16); + String collection = RandomStringUtils.randomAlphanumeric(16); + CommandContext commandContext = new CommandContext(namespace, null); + + String cql = "DROP TABLE IF EXISTS %s.%s;".formatted(namespace, collection); + withQuery(cql).returningNothing(); + + DeleteCollectionOperation operation = + new DeleteCollectionOperation(commandContext, collection); + + Supplier result = + operation + .execute(queryExecutor) + .subscribe() + .withSubscriber(UniAssertSubscriber.create()) + .awaitItem() + .assertCompleted() + .getItem(); + + assertThat(result.get()) + .satisfies( + commandResult -> { + assertThat(commandResult.status().get(CommandStatus.OK)).isEqualTo(1); + }); + } + } +} diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteCollectionResolverTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteCollectionResolverTest.java new file mode 100644 index 0000000000..cfe7f3f158 --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteCollectionResolverTest.java @@ -0,0 +1,51 @@ +package io.stargate.sgv2.jsonapi.service.resolver.model.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.stargate.sgv2.common.testprofiles.NoGlobalResourcesTestProfile; +import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; +import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteCollectionCommand; +import io.stargate.sgv2.jsonapi.service.operation.model.Operation; +import io.stargate.sgv2.jsonapi.service.operation.model.impl.DeleteCollectionOperation; +import javax.inject.Inject; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(NoGlobalResourcesTestProfile.Impl.class) +class DeleteCollectionResolverTest { + + @Inject ObjectMapper objectMapper; + @Inject DeleteCollectionResolver resolver; + + @Nested + class ResolveCommand { + + @Test + public void happyPath() throws Exception { + String json = + """ + { + "deleteCollection": { + "name" : "my_collection" + } + } + """; + + DeleteCollectionCommand command = objectMapper.readValue(json, DeleteCollectionCommand.class); + CommandContext context = new CommandContext("my_namespace", null); + Operation result = resolver.resolveCommand(context, command); + + assertThat(result) + .isInstanceOfSatisfying( + DeleteCollectionOperation.class, + op -> { + assertThat(op.name()).isEqualTo("my_collection"); + assertThat(op.context()).isEqualTo(context); + }); + } + } +} From 70b9b396893693ee64c56a57d8e03f48f19f11ae Mon Sep 17 00:00:00 2001 From: Ivan Senic Date: Thu, 16 Feb 2023 11:52:08 +0100 Subject: [PATCH 2/2] swagger and format --- .../io/stargate/sgv2/jsonapi/StargateJsonApi.java | 11 +++++++++++ .../sgv2/jsonapi/api/v1/NamespaceResource.java | 1 + .../resolver/model/impl/DeleteCollectionResolver.java | 4 +--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java b/src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java index 871950c8bc..2b8e5e46a9 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java @@ -220,6 +220,17 @@ } } """), + @ExampleObject( + name = "deleteCollection", + summary = "`DeleteCollection` command", + value = + """ + { + "deleteCollection": { + "name": "events" + } + } + """), @ExampleObject( name = "resultCount", summary = "countDocuments command result", diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/NamespaceResource.java b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/NamespaceResource.java index ad1bac5b67..09e71d8f83 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/NamespaceResource.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/NamespaceResource.java @@ -56,6 +56,7 @@ public NamespaceResource(CommandProcessor commandProcessor) { schema = @Schema(anyOf = {CreateCollectionCommand.class}), examples = { @ExampleObject(ref = "createCollection"), + @ExampleObject(ref = "deleteCollection"), })) @APIResponses( @APIResponse( diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteCollectionResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteCollectionResolver.java index ea6b9ce430..78b0cf3ca8 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteCollectionResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteCollectionResolver.java @@ -7,9 +7,7 @@ import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; import javax.enterprise.context.ApplicationScoped; -/** - * Resolver for the {@link DeleteCollectionCommand}. - */ +/** Resolver for the {@link DeleteCollectionCommand}. */ @ApplicationScoped public class DeleteCollectionResolver implements CommandResolver { @Override