Skip to content

Commit 899347c

Browse files
committed
Add DeleteObject API Implementation
1 parent 6cec109 commit 899347c

File tree

6 files changed

+173
-0
lines changed

6 files changed

+173
-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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.DeleteObjectRequest;
11+
import org.vss.DeleteObjectResponse;
12+
import org.vss.KVStore;
13+
14+
@Path(VssApiEndpoint.DELETE_OBJECT)
15+
@Slf4j
16+
public class DeleteObjectApi extends AbstractVssApi {
17+
@Inject
18+
public DeleteObjectApi(KVStore kvstore) {
19+
super(kvstore);
20+
}
21+
22+
@POST
23+
@Produces(MediaType.APPLICATION_OCTET_STREAM)
24+
public Response execute(byte[] payload) {
25+
try {
26+
DeleteObjectRequest request = DeleteObjectRequest.parseFrom(payload);
27+
DeleteObjectResponse response = kvStore.delete(request);
28+
return toResponse(response);
29+
} catch (Exception e) {
30+
log.error("Exception in DeleteObjectApi: ", e);
31+
return toErrorResponse(e);
32+
}
33+
}
34+
}

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
@@ -165,6 +165,32 @@ void putAndDeleteShouldSucceedAsAtomicTransaction() {
165165
assertThat(getObject(KVStore.GLOBAL_VERSION_KEY).getVersion(), is(0L));
166166
}
167167

168+
@Test
169+
void deleteShouldSucceedWhenItemExists() {
170+
assertDoesNotThrow(() -> putObjects(null, List.of(kv("k1", "k1v1", 0))));
171+
assertDoesNotThrow(() -> deleteObject(kv("k1", "", 1)));
172+
173+
KeyValue response = getObject("k1");
174+
assertThat(response.getKey(), is("k1"));
175+
assertTrue(response.getValue().isEmpty());
176+
}
177+
178+
@Test
179+
void deleteShouldSucceedWhenItemDoesNotExist() {
180+
assertDoesNotThrow(() -> deleteObject(kv("non_existent_key", "", 0)));
181+
}
182+
183+
@Test
184+
void deleteShouldBeIdempotent() {
185+
assertDoesNotThrow(() -> putObjects(null, List.of(kv("k1", "k1v1", 0))));
186+
assertDoesNotThrow(() -> deleteObject(kv("k1", "", 1)));
187+
assertDoesNotThrow(() -> deleteObject(kv("k1", "", 1)));
188+
189+
KeyValue response = getObject("k1");
190+
assertThat(response.getKey(), is("k1"));
191+
assertTrue(response.getValue().isEmpty());
192+
}
193+
168194
@Test
169195
void getShouldReturnEmptyResponseWhenKeyDoesNotExist() {
170196
KeyValue response = getObject("non_existent_key");
@@ -417,6 +443,12 @@ private void putAndDeleteObjects(@Nullable Long globalVersion, List<KeyValue> pu
417443
this.kvStore.put(putObjectRequestBuilder.build());
418444
}
419445

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

0 commit comments

Comments
 (0)