Skip to content

Commit d3052a5

Browse files
committed
Add DeleteObject API Implementation
1 parent 416c921 commit d3052a5

File tree

6 files changed

+166
-0
lines changed

6 files changed

+166
-0
lines changed

app/src/main/java/org/vss/KVStore.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ public interface KVStore {
88

99
PutObjectResponse put(PutObjectRequest request);
1010

11+
DeleteObjectResponse delete(DeleteObjectRequest request);
12+
1113
ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request);
1214
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.vss.api;
2+
3+
import jakarta.inject.Inject;
4+
import jakarta.ws.rs.POST;
5+
import jakarta.ws.rs.Path;
6+
import jakarta.ws.rs.Produces;
7+
import jakarta.ws.rs.core.MediaType;
8+
import jakarta.ws.rs.core.Response;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.vss.*;
11+
12+
@Path(VssApiEndpoint.DELETE_OBJECT)
13+
@Slf4j
14+
public class DeleteObjectApi extends AbstractVssApi {
15+
16+
@Inject
17+
public DeleteObjectApi(KVStore kvstore) {
18+
super(kvstore);
19+
}
20+
21+
@POST
22+
@Produces(MediaType.APPLICATION_OCTET_STREAM)
23+
public Response execute(byte[] payload) {
24+
try {
25+
DeleteObjectRequest request = DeleteObjectRequest.parseFrom(payload);
26+
DeleteObjectResponse response = kvStore.delete(request);
27+
return toResponse(response);
28+
} catch (Exception e) {
29+
log.error("Exception in DeleteObjectApi: ", e);
30+
return toErrorResponse(e);
31+
}
32+
}
33+
}

app/src/main/java/org/vss/api/VssApiEndpoint.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
public class VssApiEndpoint {
44
public static final String GET_OBJECT = "/getObject";
55
public static final String PUT_OBJECTS = "/putObjects";
6+
public static final String DELETE_OBJECT = "/deleteObject";
67
public static final String LIST_KEY_VERSIONS = "/listKeyVersions";
78
}

app/src/main/java/org/vss/impl/postgres/PostgresBackendImpl.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import org.jooq.Insert;
1212
import org.jooq.Query;
1313
import org.jooq.Update;
14+
import org.vss.DeleteObjectRequest;
15+
import org.vss.DeleteObjectResponse;
1416
import org.vss.GetObjectRequest;
1517
import org.vss.GetObjectResponse;
1618
import org.vss.KVStore;
@@ -140,6 +142,20 @@ private VssDbRecord buildVssRecord(String storeId, KeyValue kv) {
140142
.setVersion(kv.getVersion());
141143
}
142144

145+
@Override
146+
public DeleteObjectResponse delete(DeleteObjectRequest request) {
147+
String storeId = request.getStoreId();
148+
VssDbRecord vssDbRecord = buildVssRecord(storeId, request.getKeyValue());
149+
150+
context.transaction((ctx) -> {
151+
DSLContext dsl = ctx.dsl();
152+
Query deleteObjectQuery = buildDeleteObjectQuery(dsl, vssDbRecord);
153+
dsl.execute(deleteObjectQuery);
154+
});
155+
156+
return DeleteObjectResponse.newBuilder().build();
157+
}
158+
143159
@Override
144160
public ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request) {
145161
String storeId = request.getStoreId();

app/src/test/java/org/vss/AbstractKVStoreIntegrationTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,32 @@ void putShouldSucceedWhenNoGlobalVersionIsGiven() {
131131
assertThat(getObject(KVStore.GLOBAL_VERSION_KEY).getVersion(), is(0L));
132132
}
133133

134+
@Test
135+
void deleteShouldSucceedWhenItemExists() {
136+
assertDoesNotThrow(() -> putObjects(null, List.of(kv("k1", "k1v1", 0))));
137+
assertDoesNotThrow(() -> deleteObject(kv("k1", "", 1)));
138+
139+
KeyValue response = getObject("k1");
140+
assertThat(response.getKey(), is("k1"));
141+
assertTrue(response.getValue().isEmpty());
142+
}
143+
144+
@Test
145+
void deleteShouldSucceedWhenItemDoesNotExist() {
146+
assertDoesNotThrow(() -> deleteObject(kv("non_existent_key", "", 0)));
147+
}
148+
149+
@Test
150+
void deleteShouldBeIdempotent() {
151+
assertDoesNotThrow(() -> putObjects(null, List.of(kv("k1", "k1v1", 0))));
152+
assertDoesNotThrow(() -> deleteObject(kv("k1", "", 1)));
153+
assertDoesNotThrow(() -> deleteObject(kv("k1", "", 1)));
154+
155+
KeyValue response = getObject("k1");
156+
assertThat(response.getKey(), is("k1"));
157+
assertTrue(response.getValue().isEmpty());
158+
}
159+
134160
@Test
135161
void getShouldReturnEmptyResponseWhenKeyDoesNotExist() {
136162
KeyValue response = getObject("non_existent_key");
@@ -370,6 +396,12 @@ private void putObjects(@Nullable Long globalVersion, List<KeyValue> keyValues)
370396
this.kvStore.put(putObjectRequestBuilder.build());
371397
}
372398

399+
private void deleteObject(KeyValue keyValue) {
400+
DeleteObjectRequest request = DeleteObjectRequest.newBuilder()
401+
.setStoreId(STORE_ID).setKeyValue(keyValue).build();
402+
this.kvStore.delete(request);
403+
}
404+
373405
private ListKeyVersionsResponse list(@Nullable String nextPageToken, @Nullable Integer pageSize,
374406
@Nullable String keyPrefix) {
375407
ListKeyVersionsRequest.Builder listRequestBuilder = ListKeyVersionsRequest.newBuilder()
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.vss.api;
2+
3+
import com.google.protobuf.ByteString;
4+
import jakarta.ws.rs.core.Response;
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.params.ParameterizedTest;
8+
import org.junit.jupiter.params.provider.Arguments;
9+
import org.junit.jupiter.params.provider.MethodSource;
10+
import org.vss.*;
11+
import org.vss.exception.ConflictException;
12+
13+
import java.nio.charset.StandardCharsets;
14+
import java.util.stream.Stream;
15+
16+
import static org.hamcrest.MatcherAssert.assertThat;
17+
import static org.hamcrest.Matchers.is;
18+
import static org.mockito.ArgumentMatchers.any;
19+
import static org.mockito.Mockito.*;
20+
21+
public class DeleteObjectApiTest {
22+
private DeleteObjectApi deleteObjectApi;
23+
private KVStore mockKVStore;
24+
25+
private static String TEST_STORE_ID = "storeId";
26+
private static String TEST_KEY = "key";
27+
private static KeyValue TEST_KV = KeyValue.newBuilder().setKey(TEST_KEY).setValue(
28+
ByteString.copyFrom("test_value", StandardCharsets.UTF_8)).build();
29+
30+
@BeforeEach
31+
void setUp() {
32+
mockKVStore = mock(KVStore.class);
33+
deleteObjectApi = new DeleteObjectApi(mockKVStore);
34+
}
35+
36+
@Test
37+
void execute_ValidPayload_ReturnsResponse() {
38+
DeleteObjectRequest expectedRequest =
39+
DeleteObjectRequest.newBuilder().setStoreId(TEST_STORE_ID).setKeyValue(
40+
KeyValue.newBuilder().setKey(TEST_KEY).setVersion(0)
41+
).build();
42+
byte[] payload = expectedRequest.toByteArray();
43+
DeleteObjectResponse mockResponse = DeleteObjectResponse.newBuilder().build();
44+
when(mockKVStore.delete(expectedRequest)).thenReturn(mockResponse);
45+
46+
Response actualResponse = deleteObjectApi.execute(payload);
47+
48+
assertThat(actualResponse.getStatus(), is(Response.Status.OK.getStatusCode()));
49+
assertThat(actualResponse.getEntity(), is(mockResponse.toByteArray()));
50+
verify(mockKVStore).delete(expectedRequest);
51+
}
52+
53+
@ParameterizedTest
54+
@MethodSource("provideErrorTestCases")
55+
void execute_InvalidPayload_ReturnsErrorResponse(Exception exception,
56+
ErrorCode errorCode) {
57+
DeleteObjectRequest expectedRequest =
58+
DeleteObjectRequest.newBuilder().setStoreId(TEST_STORE_ID).setKeyValue(
59+
KeyValue.newBuilder().setKey(TEST_KEY).setVersion(0)
60+
).build();
61+
byte[] payload = expectedRequest.toByteArray();
62+
when(mockKVStore.delete(any())).thenThrow(exception);
63+
64+
Response response = deleteObjectApi.execute(payload);
65+
66+
ErrorResponse expectedErrorResponse = ErrorResponse.newBuilder()
67+
.setErrorCode(errorCode)
68+
.setMessage("")
69+
.build();
70+
assertThat(response.getEntity(), is(expectedErrorResponse.toByteArray()));
71+
assertThat(response.getStatus(), is(expectedErrorResponse.getErrorCode().getNumber()));
72+
verify(mockKVStore).delete(expectedRequest);
73+
}
74+
75+
private static Stream<Arguments> provideErrorTestCases() {
76+
return Stream.of(
77+
Arguments.of(new ConflictException(""), ErrorCode.CONFLICT_EXCEPTION),
78+
Arguments.of(new IllegalArgumentException(""), ErrorCode.INVALID_REQUEST_EXCEPTION),
79+
Arguments.of(new RuntimeException(""), ErrorCode.INTERNAL_SERVER_EXCEPTION)
80+
);
81+
}
82+
}

0 commit comments

Comments
 (0)