Skip to content

Commit 3a363e0

Browse files
Merge branch 'main' into fixing-links-404
2 parents 0837c71 + e22740f commit 3a363e0

3 files changed

Lines changed: 126 additions & 4 deletions

File tree

.changeset/swift-moles-fetch.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@refinedev/supabase": patch
3+
---
4+
5+
fix(supabase): handle realtime subscriptions with multiple filters #6360
6+
7+
Supabase Realtime `postgres_changes` subscriptions support a single `filter` string.
8+
When multiple filters are provided, `liveProvider` now uses only the first valid filter
9+
and logs a warning instead of generating an invalid subscription payload.
10+
11+
Resolves #6360

packages/supabase/src/liveProvider/index.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ export const liveProvider = (
5050
};
5151

5252
const mapFilter = (filters?: CrudFilters): string | undefined => {
53-
if (!filters || filters?.length === 0) {
53+
if (!filters || filters.length === 0) {
5454
return;
5555
}
5656

57-
return filters
57+
const mapped = filters
5858
.map((filter: CrudFilter): string | undefined => {
5959
if ("field" in filter) {
6060
return `${filter.field}=${mapOperator(filter.operator)}.${
@@ -63,17 +63,32 @@ export const liveProvider = (
6363
}
6464
return;
6565
})
66-
.filter(Boolean)
67-
.join(",");
66+
.filter((x): x is string => Boolean(x));
67+
68+
if (mapped.length === 0) return;
69+
70+
if (mapped.length > 1) {
71+
// Supabase Realtime currently supports only a single `filter` string
72+
// for postgres_changes. Joining multiple filters with commas
73+
// results in an invalid payload and may break the subscription.
74+
console.warn(
75+
`[refine/supabase] Multiple filters are not supported for Supabase Realtime subscriptions. Using only the first filter: "${mapped[0]}".`,
76+
);
77+
}
78+
79+
return mapped[0];
6880
};
6981

7082
const events = types
7183
.map((x) => supabaseTypes[x])
7284
.sort((a, b) => a.localeCompare(b));
85+
7386
const filter = mapFilter(params?.filters);
87+
7488
const ch = `${channel}:${events.join("|")}${filter ? `:${filter}` : ""}`;
7589

7690
let client = supabaseClient.channel(ch);
91+
7792
for (let i = 0; i < events.length; i++) {
7893
client = client.on(
7994
"postgres_changes",
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { SupabaseClient } from "@supabase/supabase-js";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
3+
import { liveProvider } from "../../src/liveProvider";
4+
5+
describe("liveProvider", () => {
6+
afterEach(() => {
7+
vi.restoreAllMocks();
8+
});
9+
10+
const createMockSupabaseClient = () => {
11+
const realtimeChannel = {
12+
on: vi.fn().mockReturnThis(),
13+
subscribe: vi.fn().mockReturnThis(),
14+
};
15+
16+
const client = {
17+
channel: vi.fn().mockReturnValue(realtimeChannel),
18+
removeChannel: vi.fn(),
19+
rest: { schemaName: "public" },
20+
} as unknown as SupabaseClient<any, any, any>;
21+
22+
return { client, realtimeChannel };
23+
};
24+
25+
it("uses only the first realtime filter and warns when multiple filters are provided", () => {
26+
const warnSpy = vi
27+
.spyOn(console, "warn")
28+
.mockImplementation(() => undefined);
29+
const { client, realtimeChannel } = createMockSupabaseClient();
30+
const provider = liveProvider(client);
31+
32+
provider.subscribe({
33+
channel: "resources/posts",
34+
types: ["created", "updated"],
35+
callback: vi.fn(),
36+
params: {
37+
filters: [
38+
{ field: "id", operator: "eq", value: 1 },
39+
{ field: "status", operator: "eq", value: "published" },
40+
],
41+
},
42+
});
43+
44+
expect(client.channel).toHaveBeenCalledWith(
45+
"resources/posts:INSERT|UPDATE:id=eq.1",
46+
);
47+
expect(realtimeChannel.on).toHaveBeenCalledTimes(2);
48+
expect(realtimeChannel.on).toHaveBeenCalledWith(
49+
"postgres_changes",
50+
expect.objectContaining({
51+
event: "INSERT",
52+
filter: "id=eq.1",
53+
schema: "public",
54+
table: "posts",
55+
}),
56+
expect.any(Function),
57+
);
58+
expect(realtimeChannel.on).toHaveBeenCalledWith(
59+
"postgres_changes",
60+
expect.objectContaining({
61+
event: "UPDATE",
62+
filter: "id=eq.1",
63+
schema: "public",
64+
table: "posts",
65+
}),
66+
expect.any(Function),
67+
);
68+
expect(realtimeChannel.subscribe).toHaveBeenCalledTimes(1);
69+
expect(warnSpy).toHaveBeenCalledTimes(1);
70+
expect(warnSpy).toHaveBeenCalledWith(
71+
expect.stringContaining(`Using only the first filter: "id=eq.1".`),
72+
);
73+
});
74+
75+
it("does not warn when a single realtime filter is provided", () => {
76+
const warnSpy = vi
77+
.spyOn(console, "warn")
78+
.mockImplementation(() => undefined);
79+
const { client } = createMockSupabaseClient();
80+
const provider = liveProvider(client);
81+
82+
provider.subscribe({
83+
channel: "resources/posts",
84+
types: ["updated"],
85+
callback: vi.fn(),
86+
params: {
87+
filters: [{ field: "status", operator: "eq", value: "published" }],
88+
},
89+
});
90+
91+
expect(client.channel).toHaveBeenCalledWith(
92+
"resources/posts:UPDATE:status=eq.published",
93+
);
94+
expect(warnSpy).not.toHaveBeenCalled();
95+
});
96+
});

0 commit comments

Comments
 (0)