diff --git a/app/Http/Controllers/V1/KeyValueController.php b/app/Http/Controllers/V1/KeyValueController.php index 0323235..e4c75b8 100644 --- a/app/Http/Controllers/V1/KeyValueController.php +++ b/app/Http/Controllers/V1/KeyValueController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Http\Resources\KeyValueResource; use App\Interfaces\KeyValueServiceInterface; +use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\ResourceCollection; class KeyValueController extends Controller @@ -20,4 +21,16 @@ public function index(): ResourceCollection return KeyValueResource::collection($data); } + + /** + * Accept a key and return the corresponding latest value. + * When given a key AND a timestamp,return whatever the value of the key at the time was. + */ + public function show(Request $request, string $key): KeyValueResource + { + $timestamp = $request->query('timestamp'); + $data = $this->keyValueService->getValue($key, $timestamp); + + return new KeyValueResource($data); + } } diff --git a/app/Interfaces/KeyValueRepositoryInterface.php b/app/Interfaces/KeyValueRepositoryInterface.php index 29d1ef2..3cf772b 100644 --- a/app/Interfaces/KeyValueRepositoryInterface.php +++ b/app/Interfaces/KeyValueRepositoryInterface.php @@ -4,5 +4,9 @@ interface KeyValueRepositoryInterface { + public function getKeyLatest(string $key); + + public function getByTimestamp(string $key, int $timestamp); + public function getAll(); } diff --git a/app/Interfaces/KeyValueServiceInterface.php b/app/Interfaces/KeyValueServiceInterface.php index 89c9c06..6073dd2 100644 --- a/app/Interfaces/KeyValueServiceInterface.php +++ b/app/Interfaces/KeyValueServiceInterface.php @@ -4,5 +4,7 @@ interface KeyValueServiceInterface { + public function getValue(string $key, ?string $timestamp = ''); + public function getAll(); } diff --git a/app/Repositories/DynamoDbKeyValueRepository.php b/app/Repositories/DynamoDbKeyValueRepository.php index e59855d..c38c0fb 100644 --- a/app/Repositories/DynamoDbKeyValueRepository.php +++ b/app/Repositories/DynamoDbKeyValueRepository.php @@ -4,12 +4,27 @@ use App\Interfaces\KeyValueRepositoryInterface; use App\Models\KeyValue; +use BaoPham\DynamoDb\RawDynamoDbQuery; use Illuminate\Database\Eloquent\Collection; class DynamoDbKeyValueRepository implements KeyValueRepositoryInterface { public function __construct(private KeyValue $model) {} + public function getKeyLatest(string $key) + { + // @phpstan-ignore-next-line + return $this->model->where('key', $key) + ->decorate(function (RawDynamoDbQuery $raw) { + $raw->query['ScanIndexForward'] = false; + })->firstOrFail(); + } + + public function getByTimestamp(string $key, int $timestamp) + { + return $this->model->findOrFail(['key' => $key, 'timestamp' => $timestamp]); + } + public function getAll(): Collection { return $this->model->all(); diff --git a/app/Services/KeyValueService.php b/app/Services/KeyValueService.php index 27d73d8..3cfceae 100644 --- a/app/Services/KeyValueService.php +++ b/app/Services/KeyValueService.php @@ -9,6 +9,17 @@ class KeyValueService implements KeyValueServiceInterface { public function __construct(private KeyValueRepositoryInterface $keyValueRepository) {} + public function getValue(string $key, ?string $timestamp = '') + { + if ($timestamp) { + $data = $this->keyValueRepository->getByTimestamp($key, (int) $timestamp); + } else { + $data = $this->keyValueRepository->getKeyLatest($key); + } + + return $data; + } + public function getAll() { return $this->keyValueRepository->getAll(); diff --git a/routes/api.php b/routes/api.php index 742c95b..eb997dd 100644 --- a/routes/api.php +++ b/routes/api.php @@ -9,3 +9,4 @@ })->middleware('auth:sanctum'); Route::get('get_all_records', [KeyValueController::class, 'index']); +Route::get('object/{key}', [KeyValueController::class, 'show']); diff --git a/tests/Feature/Controllers/V1/KeyValueControllerTest.php b/tests/Feature/Controllers/V1/KeyValueControllerTest.php index 78bfa79..2a37d72 100644 --- a/tests/Feature/Controllers/V1/KeyValueControllerTest.php +++ b/tests/Feature/Controllers/V1/KeyValueControllerTest.php @@ -35,3 +35,30 @@ ], ]); }); + +it('can fetch the latest value for a key', function () { + $response = $this->get('/api/v1/object/mykey'); + + $response->assertStatus(200) + ->assertJson([ + 'data' => [ + 'key' => 'mykey', + 'value' => 'latest_value', + ], + ]); +}); + +it('can fetch the value for a key at a specific timestamp', function () { + $timestamp = 1625236523; + + $response = $this->get('/api/v1/object/mykey?timestamp=' . $timestamp); + + $response->assertStatus(200) + ->assertJson([ + 'data' => [ + 'key' => 'mykey', + 'value' => 'value1', + 'timestamp' => $timestamp, + ], + ]); +}); diff --git a/tests/Unit/Repositories/DynamoDbKeyValueRepositoryTest.php b/tests/Unit/Repositories/DynamoDbKeyValueRepositoryTest.php index 03c3b4d..19d1dbe 100644 --- a/tests/Unit/Repositories/DynamoDbKeyValueRepositoryTest.php +++ b/tests/Unit/Repositories/DynamoDbKeyValueRepositoryTest.php @@ -11,6 +11,26 @@ $this->repository = new DynamoDbKeyValueRepository($this->mockModel); }); +it('can get the latest value for a key', function () { + $mockedKeyValue = new KeyValue(['key' => 'test_key', 'value' => 'latest_value']); + + $this->mockModel->shouldReceive('where')->once()->with('key', 'test_key')->andReturnSelf(); + $this->mockModel->shouldReceive('decorate')->once()->andReturnSelf(); + $this->mockModel->shouldReceive('firstOrFail')->once()->andReturn($mockedKeyValue); + + $result = $this->repository->getKeyLatest('test_key'); + + expect($result)->toBe($mockedKeyValue); +}); + +it('throws ModelNotFoundException when getting by timestamp for non-existing key and timestamp', function () { + $this->mockModel->shouldReceive('findOrFail')->once()->andThrow(ModelNotFoundException::class); + + $this->expectException(ModelNotFoundException::class); + + $this->repository->getByTimestamp('non_existing_key', time()); +}); + it('can get all key-value pairs', function () { $mockedCollection = new Collection([ new KeyValue(['key' => 'key1', 'value' => 'value1']), diff --git a/tests/Unit/Services/KeyValueServiceTest.php b/tests/Unit/Services/KeyValueServiceTest.php index d17f557..9789e2c 100644 --- a/tests/Unit/Services/KeyValueServiceTest.php +++ b/tests/Unit/Services/KeyValueServiceTest.php @@ -2,12 +2,30 @@ use App\Interfaces\KeyValueRepositoryInterface; use App\Services\KeyValueService; +use Carbon\CarbonImmutable; beforeEach(function () { $this->mockRepository = mock(KeyValueRepositoryInterface::class); $this->service = new KeyValueService($this->mockRepository); }); +it('can get the latest value for a key', function () { + $this->mockRepository->shouldReceive('getKeyLatest')->once()->with('test_key')->andReturn('latest_value'); + + $result = $this->service->getValue('test_key'); + + expect($result)->toBe('latest_value'); +}); + +it('can get the value for a key at a specific timestamp', function () { + $timestamp = CarbonImmutable::now('UTC')->timestamp; + $this->mockRepository->shouldReceive('getByTimestamp')->once()->with('test_key', $timestamp)->andReturn('value_at_timestamp'); + + $result = $this->service->getValue('test_key', (string) $timestamp); + + expect($result)->toBe('value_at_timestamp'); +}); + it('can get all key-value pairs', function () { $this->mockRepository->shouldReceive('getAll')->once()->andReturn(['key1' => 'value1', 'key2' => 'value2']);