Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions src/Typesense/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Statamic\Search\Result;
use Statamic\Support\Arr;
use Typesense\Client;
use Typesense\Collection as TypesenseCollection;
use Typesense\Exceptions\ObjectNotFound;
use Typesense\Exceptions\TypesenseClientError;

Expand Down Expand Up @@ -45,21 +46,15 @@ public function fields(Searchable $searchable)
public function delete($document)
{
try {
$this->getOrCreateIndex()->documents[$document->getSearchReference()]?->delete();
$this->collection()->documents[$document->getSearchReference()]?->delete();
} catch (ObjectNotFound $e) {
// do nothing, this just prevents errors bubbling up when the document doesnt exist
// Nothing to do when the document, or the whole collection, isn't there.
}
}

public function exists()
{
try {
$this->getOrCreateIndex();

return true;
} catch (\Throwable $e) {
return false;
}
return (bool) $this->collection()->exists();
}

public function insertDocuments(Documents $documents)
Expand All @@ -69,7 +64,9 @@ public function insertDocuments(Documents $documents)

protected function deleteIndex()
{
$collection = $this->getOrCreateIndex();
if (! ($collection = $this->collection())->exists()) {
return;
}

$collection->delete();

Expand Down Expand Up @@ -107,7 +104,9 @@ public function searchUsingApi($query, array $options = []): array
->join(',') ?: '*';
}

$this->getOrCreateIndex();
if (! $this->exists()) {
return ['raw' => [], 'results' => collect()];
}

// Using POST multiSearch to avoid potential request size limitations
// (regular search uses GET and is limited in size by Typesense)
Expand Down Expand Up @@ -145,7 +144,7 @@ public function searchUsingApi($query, array $options = []): array

public function getOrCreateIndex()
{
$collection = $this->client->getCollections()->{$this->name};
$collection = $this->collection();

// The client hands back the same Collection instance for a given name and
// remembers whether it exists, so this only asks Typesense the first time.
Expand Down Expand Up @@ -174,10 +173,15 @@ public function getOrCreateIndex()
public function getTypesenseSchemaFields(): Collection
{
return Blink::once('statamic-typesense::schema::'.$this->name(), function () {
return collect(Arr::get($this->getOrCreateIndex()->retrieve(), 'fields', []));
return $this->exists() ? collect(Arr::get($this->collection()->retrieve(), 'fields', [])) : collect();
});
}

private function collection(): TypesenseCollection
{
return $this->client->getCollections()->{$this->name};
}

private function getDefaultFields(Searchable $entry): array
{
return [
Expand All @@ -187,7 +191,7 @@ private function getDefaultFields(Searchable $entry): array

public function getCount()
{
return $this->getOrCreateIndex()->retrieve()['num_documents'] ?? 0;
return $this->exists() ? ($this->collection()->retrieve()['num_documents'] ?? 0) : 0;
}

public function client()
Expand Down
22 changes: 22 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

use Statamic\Testing\AddonTestCase;
use StatamicRadPack\Typesense\ServiceProvider;
use StatamicRadPack\Typesense\Typesense\Index;
use Typesense\ApiCall;
use Typesense\Client;
use Typesense\Collections;
use Typesense\MultiSearch;

class TestCase extends AddonTestCase
{
Expand Down Expand Up @@ -59,4 +64,21 @@ protected function resolveApplicationConfiguration($app)
],
]);
}

/**
* Build an Index whose requests go through a mocked HTTP layer, so tests can assert
* on the requests the driver actually makes without a running Typesense server.
*/
protected function indexWithMockedApi(ApiCall $apiCall, string $name = 'test'): Index
{
$client = new Client([
'api_key' => 'xyz',
'nodes' => [['host' => 'localhost', 'port' => '8108', 'path' => '', 'protocol' => 'http']],
]);

$client->collections = new Collections($apiCall);
$client->multiSearch = new MultiSearch($apiCall);

return new Index($client, $name, []);
}
}
131 changes: 131 additions & 0 deletions tests/Unit/ExistsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

namespace StatamicRadPack\Typesense\Tests\Unit;

use Mockery;
use PHPUnit\Framework\Attributes\Test;
use Statamic\Search\Documents;
use StatamicRadPack\Typesense\Tests\TestCase;
use Typesense\ApiCall;
use Typesense\Exceptions\ObjectNotFound;

class ExistsTest extends TestCase
{
private function apiWithMissingCollection(string $name = 'test'): ApiCall
{
$apiCall = Mockery::mock(ApiCall::class);

$apiCall->shouldReceive('get')
->with('/collections/'.$name, [])
->andThrow(new ObjectNotFound);

$apiCall->shouldNotReceive('post')->with('/collections', Mockery::any(), Mockery::any(), Mockery::any());

return $apiCall;
}

#[Test]
public function exists_is_false_when_the_collection_is_missing()
{
$index = $this->indexWithMockedApi($this->apiWithMissingCollection());

$this->assertFalse($index->exists());
}

#[Test]
public function exists_is_true_when_the_collection_is_present()
{
$apiCall = Mockery::mock(ApiCall::class);

$apiCall->shouldReceive('get')
->with('/collections/test', [])
->once()
->andReturn(['name' => 'test']);

$this->assertTrue($this->indexWithMockedApi($apiCall)->exists());
}

#[Test]
public function deleting_a_document_does_not_create_a_missing_collection()
{
$apiCall = $this->apiWithMissingCollection();

$apiCall->shouldReceive('delete')
->with('/collections/test/documents/entry%3A%3A1', true, [])
->andThrow(new ObjectNotFound);

$document = Mockery::mock();
$document->shouldReceive('getSearchReference')->andReturn('entry::1');

$this->indexWithMockedApi($apiCall)->delete($document);
}

#[Test]
public function searching_a_missing_collection_returns_no_results_without_creating_it()
{
$apiCall = $this->apiWithMissingCollection();

$apiCall->shouldNotReceive('post')->with('/multi_search', Mockery::any(), Mockery::any(), Mockery::any());

$results = $this->indexWithMockedApi($apiCall)->searchUsingApi('*');

$this->assertTrue($results['results']->isEmpty());
}

#[Test]
public function update_creates_a_missing_collection_without_trying_to_delete_it()
{
$apiCall = Mockery::mock(ApiCall::class);

$apiCall->shouldReceive('get')
->with('/collections/test', [])
->once()
->andThrow(new ObjectNotFound);

$apiCall->shouldNotReceive('delete');

$apiCall->shouldReceive('post')
->with('/collections', Mockery::type('array'), true, [])
->once()
->andReturn(['name' => 'test']);

$this->indexWithMockedApi($apiCall)->update();
}

#[Test]
public function count_is_zero_when_the_collection_is_missing()
{
$this->assertSame(0, $this->indexWithMockedApi($this->apiWithMissingCollection())->getCount());
}

#[Test]
public function schema_fields_are_empty_when_the_collection_is_missing()
{
$fields = $this->indexWithMockedApi($this->apiWithMissingCollection())->getTypesenseSchemaFields();

$this->assertTrue($fields->isEmpty());
}

#[Test]
public function inserting_documents_creates_a_missing_collection()
{
$apiCall = Mockery::mock(ApiCall::class);

$apiCall->shouldReceive('get')
->with('/collections/test', [])
->once()
->andThrow(new ObjectNotFound);

$apiCall->shouldReceive('post')
->with('/collections', Mockery::type('array'), true, [])
->once()
->andReturn(['name' => 'test']);

$apiCall->shouldReceive('post')
->with('/collections/test/documents/import', Mockery::any(), false, ['action' => 'upsert'])
->once()
->andReturn('{"success":true}');

$this->indexWithMockedApi($apiCall)->insertDocuments(new Documents(['entry::1' => ['id' => 'entry::1']]));
}
}
19 changes: 0 additions & 19 deletions tests/Unit/GetOrCreateIndexTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,11 @@
use Mockery;
use PHPUnit\Framework\Attributes\Test;
use StatamicRadPack\Typesense\Tests\TestCase;
use StatamicRadPack\Typesense\Typesense\Index;
use Typesense\ApiCall;
use Typesense\Client;
use Typesense\Collections;
use Typesense\Exceptions\ObjectNotFound;

class GetOrCreateIndexTest extends TestCase
{
/**
* Build an Index whose collection lookups go through a mocked HTTP layer, so we can
* count the requests the driver actually makes.
*/
private function indexWithMockedApi(ApiCall $apiCall, string $name = 'test'): Index
{
$client = new Client([
'api_key' => 'xyz',
'nodes' => [['host' => 'localhost', 'port' => '8108', 'path' => '', 'protocol' => 'http']],
]);

$client->collections = new Collections($apiCall);

return new Index($client, $name, []);
}

#[Test]
public function it_only_looks_the_collection_up_once()
{
Expand Down
13 changes: 1 addition & 12 deletions tests/Unit/SearchUsingApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
use StatamicRadPack\Typesense\Tests\TestCase;
use StatamicRadPack\Typesense\Typesense\Index;
use Typesense\ApiCall;
use Typesense\Client;
use Typesense\Collections;
use Typesense\Exceptions\TypesenseClientError;
use Typesense\MultiSearch;

class SearchUsingApiTest extends TestCase
{
Expand All @@ -27,15 +24,7 @@ private function indexWithMultiSearchResult(array $result, string $name = 'test'
->once()
->andReturn(['results' => [$result]]);

$client = new Client([
'api_key' => 'xyz',
'nodes' => [['host' => 'localhost', 'port' => '8108', 'path' => '', 'protocol' => 'http']],
]);

$client->collections = new Collections($apiCall);
$client->multiSearch = new MultiSearch($apiCall);

return new Index($client, $name, []);
return $this->indexWithMockedApi($apiCall, $name);
}

#[Test]
Expand Down
Loading