Skip to content

Commit 56df42e

Browse files
authored
Merge pull request #574 from psteinroe/fix/only-store-nonempty-results
fix: only store nonempty results
2 parents 3893eb4 + 7fb5bf7 commit 56df42e

File tree

5 files changed

+143
-3
lines changed

5 files changed

+143
-3
lines changed

.changeset/fifty-oranges-hope.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+
fix: only store non-empty results

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Value } from './stores/entry';
1111
import { Store } from './stores/interface';
1212
import { SwrCache } from './swr-cache';
1313
import { TieredStore } from './tiered-store';
14+
import { isEmpty } from './utils';
1415

1516
export type QueryCacheOpts = {
1617
stores: Store[];
@@ -84,7 +85,7 @@ export class QueryCache {
8485

8586
const result = await this.dedupeQuery(query);
8687

87-
if (!opts?.store || opts.store(result)) {
88+
if (!isEmpty(result) && (!opts?.store || opts.store(result))) {
8889
await this.inner.set(key, result, opts);
8990
}
9091

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

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { PostgrestResponse } from '@supabase/postgrest-js';
12
import type { Context } from './context';
23
import { Value } from './stores/entry';
34
import { Store } from './stores/interface';
5+
import { isEmpty } from './utils';
46

57
export type SwrCacheOpts = {
68
ctx: Context;
@@ -97,15 +99,21 @@ export class SwrCache {
9799
if (typeof value !== 'undefined') {
98100
if (revalidate) {
99101
this.ctx.waitUntil(
100-
loadFromOrigin(key).then((res) => this.set(key, res, opts)),
102+
loadFromOrigin(key).then((res) => {
103+
if (!isEmpty(res)) {
104+
this.set(key, res, opts);
105+
}
106+
}),
101107
);
102108
}
103109

104110
return value;
105111
}
106112

107113
const loadedValue = await loadFromOrigin(key);
108-
this.ctx.waitUntil(this.set(key, loadedValue));
114+
if (!isEmpty(loadedValue)) {
115+
this.ctx.waitUntil(this.set(key, loadedValue));
116+
}
109117
return loadedValue;
110118
}
111119
}
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Value } from './stores/entry';
2+
3+
/**
4+
* A result is empty if
5+
* - it does not contain a truhty data field
6+
* - it does not contain a count field
7+
* - data is an empty array
8+
*
9+
* @template Result - The Result of the query
10+
* @param result - The value to check
11+
* @returns true if the result is empty
12+
*/
13+
export function isEmpty<Result>(result: Value<Result>) {
14+
if (typeof result.count === 'number') {
15+
return false;
16+
}
17+
18+
if (!result.data) {
19+
return true;
20+
}
21+
22+
if (Array.isArray(result.data)) {
23+
return result.data.length === 0;
24+
}
25+
26+
return false;
27+
}

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

+99
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,56 @@ describe('QueryCache', () => {
119119
expect(spy).toHaveBeenCalledTimes(1);
120120
});
121121

122+
it('should store count-only queries', async () => {
123+
const map = new Map();
124+
125+
const cache = new QueryCache(ctx, {
126+
stores: [new MemoryStore({ persistentMap: map })],
127+
fresh: 1000,
128+
stale: 2000,
129+
});
130+
131+
const query = client
132+
.from('contact')
133+
.select('id,username', { count: 'exact', head: true })
134+
.ilike('username', `${testRunPrefix}%`);
135+
136+
const spy = vi.spyOn(query, 'then');
137+
138+
const res = await cache.query(query);
139+
140+
const res2 = await cache.query(query);
141+
142+
expect(res.count).toEqual(4);
143+
expect(res2.count).toEqual(4);
144+
expect(spy).toHaveBeenCalledTimes(1);
145+
});
146+
147+
it('should not store empty results', async () => {
148+
const map = new Map();
149+
150+
const cache = new QueryCache(ctx, {
151+
stores: [new MemoryStore({ persistentMap: map })],
152+
fresh: 1000,
153+
stale: 2000,
154+
});
155+
156+
const query = client
157+
.from('contact')
158+
.select('id,username')
159+
.eq('username', 'unknown')
160+
.maybeSingle();
161+
162+
const spy = vi.spyOn(query, 'then');
163+
164+
const res = await cache.query(query);
165+
const res2 = await cache.query(query);
166+
167+
expect(res.data).toBeFalsy();
168+
expect(res2.data).toBeFalsy();
169+
expect(spy).toHaveBeenCalledTimes(2);
170+
});
171+
122172
it('should not store result if store() returns false', async () => {
123173
const map = new Map();
124174

@@ -249,6 +299,55 @@ describe('QueryCache', () => {
249299
expect(res2.count).toEqual(4);
250300
expect(spy).toHaveBeenCalledTimes(1);
251301
});
302+
303+
it('should store count-only queries', async () => {
304+
const map = new Map();
305+
306+
const cache = new QueryCache(ctx, {
307+
stores: [new MemoryStore({ persistentMap: map })],
308+
fresh: 1000,
309+
stale: 2000,
310+
});
311+
312+
const query = client
313+
.from('contact')
314+
.select('id,username', { count: 'exact', head: true })
315+
.ilike('username', `${testRunPrefix}%`);
316+
317+
const spy = vi.spyOn(query, 'then');
318+
319+
const res = await cache.swr(query);
320+
321+
const res2 = await cache.swr(query);
322+
323+
expect(res.count).toEqual(4);
324+
expect(res2.count).toEqual(4);
325+
expect(spy).toHaveBeenCalledTimes(1);
326+
});
327+
328+
it('should not store empty results', async () => {
329+
const map = new Map();
330+
331+
const cache = new QueryCache(ctx, {
332+
stores: [new MemoryStore({ persistentMap: map })],
333+
fresh: 1000,
334+
stale: 2000,
335+
});
336+
337+
const query = client
338+
.from('contact')
339+
.select('id,username')
340+
.ilike('username', 'unknown');
341+
342+
const spy = vi.spyOn(query, 'then');
343+
344+
const res = await cache.swr(query);
345+
const res2 = await cache.swr(query);
346+
347+
expect(res.data).toEqual([]);
348+
expect(res2.data).toEqual([]);
349+
expect(spy).toHaveBeenCalledTimes(2);
350+
});
252351
});
253352

254353
it('should dedupe', async () => {

0 commit comments

Comments
 (0)