Skip to content

Commit

Permalink
closes #178: findOne and findOneUpdate per spec, improved tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivan Senic committed Mar 16, 2023
1 parent 01aa9ff commit 3931f20
Show file tree
Hide file tree
Showing 7 changed files with 390 additions and 243 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.stargate.sgv2.jsonapi.api.model.command.clause.update.UpdateClause;
import javax.annotation.Nullable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

Expand All @@ -17,15 +18,26 @@
@JsonTypeName("findOneAndUpdate")
public record FindOneAndUpdateCommand(
@Valid @JsonProperty("filter") FilterClause filterClause,
@Valid @JsonProperty("update") UpdateClause updateClause,
@NotNull @Valid @JsonProperty("update") UpdateClause updateClause,
@Valid @Nullable Options options)
implements ReadCommand, Filterable {

@Schema(
name = "FindOneAndUpdateCommand.Options",
description = "Options for `findOneAndUpdate` command.")
public record Options(
@Valid
@Nullable
@Nullable
@Pattern(
regexp = "(after|before)",
message = "returnDocument value can only be 'before' or 'after'")
@Schema(
description =
"Specifies which document to perform the projection on. If `before` the projection is performed on the document before the update is applied, if `after` the document projection is from the document after the update.",
defaultValue = "before")
String returnDocument,
boolean upsert) {}
@Schema(
description =
"When `true`, if no documents match the `filter` clause the command will create a new _empty_ document and apply the `update` clause to the empty document.",
defaultValue = "false")
boolean upsert) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,12 @@
import io.stargate.sgv2.jsonapi.api.model.command.ReadCommand;
import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.FilterClause;
import io.stargate.sgv2.jsonapi.api.model.command.clause.sort.SortClause;
import javax.annotation.Nullable;
import javax.validation.Valid;
import org.eclipse.microprofile.openapi.annotations.media.Schema;

@Schema(description = "Command that finds a single JSON document from a collection.")
@JsonTypeName("findOne")
public record FindOneCommand(
@Valid @JsonProperty("filter") FilterClause filterClause,
@Valid @JsonProperty("sort") SortClause sortClause,
@Nullable Options options)
implements ReadCommand, Filterable {
public record Options() {}
}
@Valid @JsonProperty("sort") SortClause sortClause)
implements ReadCommand, Filterable {}
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ public Class<FindOneAndUpdateCommand> getCommandClass() {
public Operation resolveCommand(CommandContext ctx, FindOneAndUpdateCommand command) {
ReadOperation readOperation = resolve(ctx, command);
DocumentUpdater documentUpdater = DocumentUpdater.construct(command.updateClause());

// resolve options
FindOneAndUpdateCommand.Options options = command.options();
boolean returnUpdatedDocument =
command.options() != null && "after".equals(command.options().returnDocument());
options != null && "after".equals(command.options().returnDocument());
boolean upsert = command.options() != null && command.options().upsert();

// return
return new ReadAndUpdateOperation(
ctx,
readOperation,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package io.stargate.sgv2.jsonapi.api.model.command.impl;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

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 FindOneAndUpdateCommandTest {

@Inject ObjectMapper objectMapper;

@Inject Validator validator;

@Nested
class Validation {

@Test
public void noUpdateClause() throws Exception {
String json =
"""
{
"findOneAndUpdate": {
"filter": {"name": "Aaron"}
}
}
""";

FindOneAndUpdateCommand command = objectMapper.readValue(json, FindOneAndUpdateCommand.class);
Set<ConstraintViolation<FindOneAndUpdateCommand>> result = validator.validate(command);

assertThat(result)
.isNotEmpty()
.extracting(ConstraintViolation::getMessage)
.contains("must not be null");
}

@Test
public void invalidReturnDocumentOption() throws Exception {
String json =
"""
{
"findOneAndUpdate": {
"filter": {"name": "Aaron"},
"update": { "$set": {"name": "Tatu"}},
"options": {
"returnDocument": "yes"
}
}
}
""";

FindOneAndUpdateCommand command = objectMapper.readValue(json, FindOneAndUpdateCommand.class);
Set<ConstraintViolation<FindOneAndUpdateCommand>> result = validator.validate(command);

assertThat(result)
.isNotEmpty()
.extracting(ConstraintViolation::getMessage)
.contains("returnDocument value can only be 'before' or 'after'");
}

@Test
public void validReturnDocumentOption() throws Exception {
String json =
"""
{
"findOneAndUpdate": {
"filter": {"name": "Aaron"},
"update": { "$set": {"name": "Tatu"}},
"options": {
"returnDocument": "after"
}
}
}
""";

FindOneAndUpdateCommand command = objectMapper.readValue(json, FindOneAndUpdateCommand.class);
Set<ConstraintViolation<FindOneAndUpdateCommand>> result = validator.validate(command);

assertThat(result).isEmpty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
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.Mock;
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.clause.update.UpdateClause;
import io.stargate.sgv2.jsonapi.api.model.command.clause.update.UpdateOperator;
import io.stargate.sgv2.jsonapi.api.model.command.impl.FindOneAndUpdateCommand;
import io.stargate.sgv2.jsonapi.service.bridge.config.DocumentConfig;
import io.stargate.sgv2.jsonapi.service.operation.model.Operation;
import io.stargate.sgv2.jsonapi.service.operation.model.ReadType;
import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase;
import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindOperation;
import io.stargate.sgv2.jsonapi.service.operation.model.impl.ReadAndUpdateOperation;
import io.stargate.sgv2.jsonapi.service.shredding.Shredder;
import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId;
import io.stargate.sgv2.jsonapi.service.testutil.DocumentUpdaterUtils;
import io.stargate.sgv2.jsonapi.service.updater.DocumentUpdater;
import javax.inject.Inject;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@QuarkusTest
@TestProfile(NoGlobalResourcesTestProfile.Impl.class)
public class FindOneAndUpdateCommandResolverTest {
@Inject ObjectMapper objectMapper;
@Inject DocumentConfig documentConfig;
@Inject Shredder shredder;
@Inject FindOneAndUpdateCommandResolver resolver;

@Nested
class Resolve {

@Mock CommandContext commandContext;

@Test
public void idFilterCondition() throws Exception {
String json =
"""
{
"findOneAndUpdate": {
"filter" : {"_id" : "id"},
"update" : {"$set" : {"location" : "New York"}}
}
}
""";

FindOneAndUpdateCommand command = objectMapper.readValue(json, FindOneAndUpdateCommand.class);
Operation operation = resolver.resolveCommand(commandContext, command);

assertThat(operation)
.isInstanceOfSatisfying(
ReadAndUpdateOperation.class,
op -> {
assertThat(op.commandContext()).isEqualTo(commandContext);
assertThat(op.returnDocumentInResponse()).isTrue();
assertThat(op.returnUpdatedDocument()).isFalse();
assertThat(op.upsert()).isFalse();
assertThat(op.shredder()).isEqualTo(shredder);
assertThat(op.updateLimit()).isEqualTo(1);
assertThat(op.retryLimit()).isEqualTo(documentConfig.lwt().retries());
assertThat(op.documentUpdater())
.isInstanceOfSatisfying(
DocumentUpdater.class,
updater -> {
UpdateClause updateClause =
DocumentUpdaterUtils.updateClause(
UpdateOperator.SET,
objectMapper.createObjectNode().put("location", "New York"));

assertThat(updater.updateOperations())
.isEqualTo(updateClause.buildOperations());
});
assertThat(op.readOperation())
.isInstanceOfSatisfying(
FindOperation.class,
find -> {
DBFilterBase.IDFilter filter =
new DBFilterBase.IDFilter(
DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("id"));

assertThat(find.objectMapper()).isEqualTo(objectMapper);
assertThat(find.commandContext()).isEqualTo(commandContext);
assertThat(find.pageSize()).isEqualTo(1);
assertThat(find.limit()).isEqualTo(1);
assertThat(find.pagingState()).isNull();
assertThat(find.readType()).isEqualTo(ReadType.DOCUMENT);
assertThat(find.filters()).singleElement().isEqualTo(filter);
});
});
}

@Test
public void idFilterConditionWithOptions() throws Exception {
String json =
"""
{
"findOneAndUpdate": {
"filter" : {"_id" : "id"},
"update" : {"$set" : {"location" : "New York"}},
"options" : {"returnDocument" : "after", "upsert": true }
}
}
""";

FindOneAndUpdateCommand command = objectMapper.readValue(json, FindOneAndUpdateCommand.class);
Operation operation = resolver.resolveCommand(commandContext, command);

assertThat(operation)
.isInstanceOfSatisfying(
ReadAndUpdateOperation.class,
op -> {
assertThat(op.commandContext()).isEqualTo(commandContext);
assertThat(op.returnDocumentInResponse()).isTrue();
assertThat(op.returnUpdatedDocument()).isTrue();
assertThat(op.upsert()).isTrue();
assertThat(op.shredder()).isEqualTo(shredder);
assertThat(op.updateLimit()).isEqualTo(1);
assertThat(op.retryLimit()).isEqualTo(documentConfig.lwt().retries());
assertThat(op.documentUpdater())
.isInstanceOfSatisfying(
DocumentUpdater.class,
updater -> {
UpdateClause updateClause =
DocumentUpdaterUtils.updateClause(
UpdateOperator.SET,
objectMapper.createObjectNode().put("location", "New York"));

assertThat(updater.updateOperations())
.isEqualTo(updateClause.buildOperations());
});
assertThat(op.readOperation())
.isInstanceOfSatisfying(
FindOperation.class,
find -> {
DBFilterBase.IDFilter filter =
new DBFilterBase.IDFilter(
DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("id"));

assertThat(find.objectMapper()).isEqualTo(objectMapper);
assertThat(find.commandContext()).isEqualTo(commandContext);
assertThat(find.pageSize()).isEqualTo(1);
assertThat(find.limit()).isEqualTo(1);
assertThat(find.pagingState()).isNull();
assertThat(find.readType()).isEqualTo(ReadType.DOCUMENT);
assertThat(find.filters()).singleElement().isEqualTo(filter);
});
});
}

@Test
public void dynamicFilterCondition() throws Exception {
String json =
"""
{
"findOneAndUpdate": {
"filter" : {"col" : "val"},
"update" : {"$set" : {"location" : "New York"}}
}
}
""";

FindOneAndUpdateCommand command = objectMapper.readValue(json, FindOneAndUpdateCommand.class);
Operation operation = resolver.resolveCommand(commandContext, command);

assertThat(operation)
.isInstanceOfSatisfying(
ReadAndUpdateOperation.class,
op -> {
assertThat(op.commandContext()).isEqualTo(commandContext);
assertThat(op.returnDocumentInResponse()).isTrue();
assertThat(op.returnUpdatedDocument()).isFalse();
assertThat(op.upsert()).isFalse();
assertThat(op.shredder()).isEqualTo(shredder);
assertThat(op.updateLimit()).isEqualTo(1);
assertThat(op.retryLimit()).isEqualTo(documentConfig.lwt().retries());
assertThat(op.documentUpdater())
.isInstanceOfSatisfying(
DocumentUpdater.class,
updater -> {
UpdateClause updateClause =
DocumentUpdaterUtils.updateClause(
UpdateOperator.SET,
objectMapper.createObjectNode().put("location", "New York"));

assertThat(updater.updateOperations())
.isEqualTo(updateClause.buildOperations());
});
assertThat(op.readOperation())
.isInstanceOfSatisfying(
FindOperation.class,
find -> {
DBFilterBase.TextFilter filter =
new DBFilterBase.TextFilter(
"col", DBFilterBase.MapFilterBase.Operator.EQ, "val");

assertThat(find.objectMapper()).isEqualTo(objectMapper);
assertThat(find.commandContext()).isEqualTo(commandContext);
assertThat(find.pageSize()).isEqualTo(1);
assertThat(find.limit()).isEqualTo(1);
assertThat(find.pagingState()).isNull();
assertThat(find.readType()).isEqualTo(ReadType.DOCUMENT);
assertThat(find.filters()).singleElement().isEqualTo(filter);
});
});
}
}
}
Loading

0 comments on commit 3931f20

Please sign in to comment.