Skip to content

Commit a761a16

Browse files
committed
Add cached find helpers
1 parent 7478a8b commit a761a16

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

src/Database/Database.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8564,6 +8564,75 @@ public function find(string $collection, array $queries = [], string $forPermiss
85648564
return $results;
85658565
}
85668566

8567+
/**
8568+
* Find documents through a cache-aside lookup.
8569+
*
8570+
* The caller owns authorization context. Use Authorization::skip() around
8571+
* this method for internal reads that should bypass request-user roles.
8572+
*
8573+
* @param string $collection
8574+
* @param array<Query> $queries
8575+
* @param string|null $namespace
8576+
* @param array<string> $roles
8577+
* @param string $forPermission
8578+
* @return array<Document>
8579+
* @throws DatabaseException
8580+
* @throws QueryException
8581+
* @throws TimeoutException
8582+
* @throws Exception
8583+
*/
8584+
public function cachedFind(
8585+
string $collection,
8586+
array $queries = [],
8587+
?string $namespace = null,
8588+
array $roles = [],
8589+
string $forPermission = Database::PERMISSION_READ,
8590+
): array {
8591+
$collectionDocument = $this->silent(fn () => $this->getCollection($collection));
8592+
8593+
if ($collectionDocument->isEmpty()) {
8594+
throw new NotFoundException('Collection not found');
8595+
}
8596+
8597+
$payload = $this->withCache(
8598+
key: $this->getFindCacheKey($collection, $namespace),
8599+
callback: fn (): array => \array_map(
8600+
static fn (Document $document): array => $document->getArrayCopy(),
8601+
$this->find($collection, $queries, $forPermission),
8602+
),
8603+
hash: $this->getFindCacheField($collectionDocument, $queries, $roles, 'documents'),
8604+
);
8605+
8606+
if (!\is_array($payload)) {
8607+
return [];
8608+
}
8609+
8610+
$documents = [];
8611+
foreach ($payload as $document) {
8612+
if (!\is_array($document)) {
8613+
continue;
8614+
}
8615+
8616+
$documents[] = $this->createDocumentInstance($collection, $document);
8617+
}
8618+
8619+
return $documents;
8620+
}
8621+
8622+
/**
8623+
* Purge all cached find entries for a collection namespace.
8624+
*
8625+
* @param string $collection
8626+
* @param string|null $namespace
8627+
* @return bool
8628+
*/
8629+
public function purgeCachedFind(string $collection, ?string $namespace = null): bool
8630+
{
8631+
return $this->cache->purge(
8632+
$this->getFindCacheKey($collection, $namespace)
8633+
);
8634+
}
8635+
85678636
/**
85688637
* Execute a callback behind a cache-aside lookup.
85698638
*

tests/unit/ListCacheTest.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
use Utopia\Cache\Cache;
88
use Utopia\Database\Adapter\Memory as DatabaseMemory;
99
use Utopia\Database\Database;
10+
use Utopia\Database\Document;
11+
use Utopia\Database\Helpers\Permission;
12+
use Utopia\Database\Helpers\Role;
13+
use Utopia\Database\Query;
1014

1115
class ListCacheTest extends TestCase
1216
{
@@ -182,6 +186,101 @@ function () use (&$callbackCalls): string {
182186
$this->assertSame('fresh', $value);
183187
$this->assertSame(2, $callbackCalls);
184188
}
189+
190+
public function testCachedFindUsesCacheUntilPurged(): void
191+
{
192+
$cache = new HashMemoryCache();
193+
$database = $this->createDatabase($cache);
194+
$database->createCollection('wafRules', [
195+
new Document([
196+
'$id' => 'projectId',
197+
'type' => Database::VAR_STRING,
198+
'size' => 255,
199+
'required' => false,
200+
'signed' => true,
201+
'array' => false,
202+
'filters' => [],
203+
]),
204+
], permissions: [
205+
Permission::read(Role::any()),
206+
Permission::create(Role::any()),
207+
]);
208+
209+
$database->createDocument('wafRules', new Document([
210+
'$id' => 'rule-a',
211+
'projectId' => 'project-a',
212+
]));
213+
214+
$queries = [
215+
Query::equal('projectId', ['project-a']),
216+
Query::limit(25),
217+
];
218+
219+
$first = $database->cachedFind('wafRules', $queries, '_39', ['waf']);
220+
$this->assertCount(1, $first);
221+
$this->assertSame('rule-a', $first[0]->getId());
222+
223+
$database->createDocument('wafRules', new Document([
224+
'$id' => 'rule-b',
225+
'projectId' => 'project-a',
226+
]));
227+
228+
$cached = $database->cachedFind('wafRules', $queries, '_39', ['waf']);
229+
$this->assertCount(1, $cached);
230+
$this->assertSame('rule-a', $cached[0]->getId());
231+
232+
$this->assertTrue($database->purgeCachedFind('wafRules', '_39'));
233+
234+
$fresh = $database->cachedFind('wafRules', $queries, '_39', ['waf']);
235+
$this->assertCount(2, $fresh);
236+
$this->assertSame(['rule-a', 'rule-b'], \array_map(
237+
static fn (Document $document): string => $document->getId(),
238+
$fresh,
239+
));
240+
}
241+
242+
public function testCachedFindSeparatesEntriesByRoles(): void
243+
{
244+
$cache = new HashMemoryCache();
245+
$database = $this->createDatabase($cache);
246+
$database->createCollection('wafRules', [
247+
new Document([
248+
'$id' => 'projectId',
249+
'type' => Database::VAR_STRING,
250+
'size' => 255,
251+
'required' => false,
252+
'signed' => true,
253+
'array' => false,
254+
'filters' => [],
255+
]),
256+
], permissions: [
257+
Permission::read(Role::any()),
258+
Permission::create(Role::any()),
259+
]);
260+
261+
$database->createDocument('wafRules', new Document([
262+
'$id' => 'rule-a',
263+
'projectId' => 'project-a',
264+
]));
265+
266+
$queries = [
267+
Query::equal('projectId', ['project-a']),
268+
Query::limit(25),
269+
];
270+
271+
$database->cachedFind('wafRules', $queries, '_39', ['waf']);
272+
273+
$database->createDocument('wafRules', new Document([
274+
'$id' => 'rule-b',
275+
'projectId' => 'project-a',
276+
]));
277+
278+
$cached = $database->cachedFind('wafRules', $queries, '_39', ['waf']);
279+
$this->assertCount(1, $cached);
280+
281+
$roleSeparated = $database->cachedFind('wafRules', $queries, '_39', ['manager']);
282+
$this->assertCount(2, $roleSeparated);
283+
}
185284
}
186285

187286
class HashMemoryCache implements Adapter

0 commit comments

Comments
 (0)