Skip to content

Commit 59f214f

Browse files
authored
Merge pull request #576 from psteinroe/feat/invalidate
feat: add invalidateQueries
2 parents 56df42e + 2bbcd5a commit 59f214f

File tree

10 files changed

+114
-2
lines changed

10 files changed

+114
-2
lines changed

.changeset/thick-ears-refuse.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@supabase-cache-helpers/postgrest-server": patch
3+
---
4+
5+
feat: add invalidateQueries method

docs/pages/postgrest/server.mdx

+13
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,16 @@ const res = await cache.swr(
171171
);
172172
```
173173

174+
175+
## Invalidating Queries
176+
177+
To invaldiate all queries for a specific table, you can use the `invalidateQueries` method:
178+
179+
180+
```ts
181+
await cache.invalidateQueries({
182+
schema: 'public',
183+
table: 'contact',
184+
});
185+
```
186+

packages/postgrest-server/src/key.ts

+4
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ export function encode<Result>(
2424
parser.orderByKey,
2525
].join(SEPARATOR);
2626
}
27+
28+
export function buildTablePrefix(schema: string, table: string) {
29+
return [schema, table].join(SEPARATOR);
30+
}

packages/postgrest-server/src/query-cache.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
} from '@supabase/postgrest-js';
77

88
import { Context } from './context';
9-
import { encode } from './key';
9+
import { buildTablePrefix, encode } from './key';
1010
import { Value } from './stores/entry';
1111
import { Store } from './stores/interface';
1212
import { SwrCache } from './swr-cache';
@@ -44,6 +44,17 @@ export class QueryCache {
4444
});
4545
}
4646

47+
/**
48+
* Invalidate all cache entries for a given table
49+
*/
50+
async invalidateQueries({
51+
schema,
52+
table,
53+
}: { schema: string; table: string }) {
54+
const prefix = buildTablePrefix(schema, table);
55+
return this.inner.removeByPrefix(prefix);
56+
}
57+
4758
/**
4859
* Perform a cached postgrest query
4960
*/

packages/postgrest-server/src/stores/interface.ts

+5
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,9 @@ export interface Store {
3232
* Removes the key from the store.
3333
*/
3434
remove(key: string | string[]): Promise<void>;
35+
36+
/**
37+
* Removes all keys with the given prefix.
38+
*/
39+
removeByPrefix(prefix: string): Promise<void>;
3540
}

packages/postgrest-server/src/stores/memory.ts

+8
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,12 @@ export class MemoryStore implements Store {
4444
}
4545
return Promise.resolve();
4646
}
47+
48+
public async removeByPrefix(prefix: string): Promise<void> {
49+
for (const key of this.state.keys()) {
50+
if (key.startsWith(prefix)) {
51+
this.state.delete(key);
52+
}
53+
}
54+
}
4755
}

packages/postgrest-server/src/stores/redis.ts

+20
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,24 @@ export class RedisStore implements Store {
4444
);
4545
this.redis.del(...cacheKeys);
4646
}
47+
48+
public async removeByPrefix(prefix: string): Promise<void> {
49+
const pattern = `${prefix}*`;
50+
let cursor = '0';
51+
52+
do {
53+
const [nextCursor, keys] = await this.redis.scan(
54+
cursor,
55+
'MATCH',
56+
pattern,
57+
'COUNT',
58+
100,
59+
);
60+
cursor = nextCursor;
61+
62+
if (keys.length > 0) {
63+
await this.redis.del(...keys);
64+
}
65+
} while (cursor !== '0');
66+
}
4767
}

packages/postgrest-server/src/swr-cache.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { PostgrestResponse } from '@supabase/postgrest-js';
21
import type { Context } from './context';
32
import { Value } from './stores/entry';
43
import { Store } from './stores/interface';
@@ -27,6 +26,13 @@ export class SwrCache {
2726
this.stale = stale;
2827
}
2928

29+
/**
30+
* Invalidate all keys that start with the given prefix
31+
**/
32+
async removeByPrefix(prefix: string) {
33+
return this.store.removeByPrefix(prefix);
34+
}
35+
3036
/**
3137
* Return the cached value
3238
*

packages/postgrest-server/src/tiered-store.ts

+7
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,11 @@ export class TieredStore implements Store {
7171
public async remove(key: string): Promise<void> {
7272
await Promise.all(this.tiers.map((t) => t.remove(key)));
7373
}
74+
75+
/**
76+
* Removes all keys with the given prefix.
77+
*/
78+
public async removeByPrefix(prefix: string): Promise<void> {
79+
await Promise.all(this.tiers.map((t) => t.removeByPrefix(prefix)));
80+
}
7481
}

packages/postgrest-server/tests/query-cache.test.ts

+33
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,39 @@ describe('QueryCache', () => {
350350
});
351351
});
352352

353+
describe('.invalidateQueries()', () => {
354+
it('should work', async () => {
355+
const map = new Map();
356+
357+
const cache = new QueryCache(ctx, {
358+
stores: [new MemoryStore({ persistentMap: map })],
359+
fresh: 1000,
360+
stale: 2000,
361+
});
362+
363+
const query = client
364+
.from('contact')
365+
.select('id,username')
366+
.eq('username', contacts[0].username!)
367+
.single();
368+
369+
const spy = vi.spyOn(query, 'then');
370+
371+
const res = await cache.query(query);
372+
373+
await cache.invalidateQueries({
374+
schema: 'public',
375+
table: 'contact',
376+
});
377+
378+
const res2 = await cache.query(query);
379+
380+
expect(res.data?.username).toEqual(contacts[0].username);
381+
expect(res2.data?.username).toEqual(contacts[0].username);
382+
expect(spy).toHaveBeenCalledTimes(2);
383+
});
384+
});
385+
353386
it('should dedupe', async () => {
354387
const map = new Map();
355388

0 commit comments

Comments
 (0)