Skip to content

Commit a6e4e4e

Browse files
committed
address read model review feedback
1 parent 3cadea4 commit a6e4e4e

4 files changed

Lines changed: 145 additions & 8 deletions

File tree

docs/supabase-data-architecture.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ Signed-out safe listing catalogue used by map, SEO, sitemap, homepage, and publi
5757

5858
This table intentionally includes public listing content such as description, accepted items, rejected items, type, area/country, coordinates, slug, and low-sensitivity presentation metadata.
5959

60+
`coordinates` is the JSON shape consumed by the app. `latitude` and `longitude` are generated from that same value so map viewport queries can use normal database indexes without exposing or querying the base `listings.location` column directly.
61+
6062
Residential listings keep their description, accepted items, and rejected items public, but owner identity and residential media are hidden:
6163

6264
- `name` is `null` for residential rows.

src/app/actions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,10 @@ export const signUpAction = async (formData: FormData, request?: Request) => {
217217
"Error running hook URI",
218218
"Failed to reach hook within maximum time",
219219
];
220+
const errorMessage = error?.message ?? "";
221+
const lowerCaseErrorMessage = errorMessage.toLowerCase();
220222
const isHookTimeout = hookTimeoutPatterns.some((pattern) =>
221-
error?.message?.includes(pattern)
223+
errorMessage.includes(pattern)
222224
);
223225
if (isHookTimeout) {
224226
redirectUrl.searchParams.append("error", t("generic"));
@@ -231,7 +233,7 @@ export const signUpAction = async (formData: FormData, request?: Request) => {
231233
"User already registered",
232234
];
233235
const accountExists = accountExistsPatterns.some((pattern) =>
234-
error?.message?.toLowerCase().includes(pattern.toLowerCase())
236+
lowerCaseErrorMessage.includes(pattern.toLowerCase())
235237
);
236238
if (accountExists) {
237239
redirectUrl.searchParams.append("error", t("accountExists"));

src/features/chat/chatData.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,9 @@ export async function getChatThreads(
229229
fetchProfileCards(supabase, profileIds),
230230
fetchListingContactCards(supabase, listingIds),
231231
threadIds.length > 0
232-
? supabase
233-
.from("chat_messages")
234-
.select("id, content, created_at, read_at, sender_id, thread_id")
235-
.in("thread_id", threadIds)
236-
.order("created_at", { ascending: false })
237-
.order("id", { ascending: false })
232+
? supabase.rpc("latest_chat_messages_for_threads", {
233+
thread_ids: threadIds,
234+
})
238235
: { data: [], error: null },
239236
]);
240237

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
-- Follow-up hardening and performance fixes from PR review.
2+
3+
alter table public.public_listings
4+
add column latitude double precision
5+
generated always as ((coordinates->>'latitude')::double precision) stored,
6+
add column longitude double precision
7+
generated always as ((coordinates->>'longitude')::double precision) stored;
8+
9+
create index public_listings_longitude_latitude_idx
10+
on public.public_listings (longitude, latitude);
11+
12+
create index if not exists chat_messages_thread_id_created_at_id_idx
13+
on public.chat_messages (thread_id, created_at desc, id desc);
14+
15+
create or replace function public.listings_in_view(
16+
min_lat double precision,
17+
min_long double precision,
18+
max_lat double precision,
19+
max_long double precision
20+
) returns table(id bigint, slug text, type text, coordinates jsonb)
21+
language sql
22+
security invoker
23+
stable
24+
set search_path = ''
25+
as $$
26+
select
27+
public_listings.id,
28+
public_listings.slug,
29+
public_listings.type,
30+
public_listings.coordinates
31+
from public.public_listings
32+
where public_listings.latitude between min_lat and max_lat
33+
and public_listings.longitude between min_long and max_long
34+
$$;
35+
36+
alter function public.listings_in_view(
37+
double precision,
38+
double precision,
39+
double precision,
40+
double precision
41+
) owner to postgres;
42+
43+
revoke all privileges on function public.listings_in_view(
44+
double precision,
45+
double precision,
46+
double precision,
47+
double precision
48+
) from anon, authenticated, public;
49+
50+
grant execute on function public.listings_in_view(
51+
double precision,
52+
double precision,
53+
double precision,
54+
double precision
55+
) to anon, authenticated, service_role;
56+
57+
create or replace function public.latest_chat_messages_for_threads(thread_ids uuid[])
58+
returns table(
59+
id uuid,
60+
content text,
61+
created_at timestamp with time zone,
62+
read_at timestamp with time zone,
63+
sender_id uuid,
64+
thread_id uuid
65+
)
66+
language sql
67+
security invoker
68+
stable
69+
set search_path = ''
70+
as $$
71+
select distinct on (chat_messages.thread_id)
72+
chat_messages.id,
73+
chat_messages.content,
74+
chat_messages.created_at,
75+
chat_messages.read_at,
76+
chat_messages.sender_id,
77+
chat_messages.thread_id
78+
from public.chat_messages
79+
where auth.uid() is not null
80+
and chat_messages.thread_id = any(thread_ids)
81+
order by chat_messages.thread_id, chat_messages.created_at desc, chat_messages.id desc
82+
$$;
83+
84+
alter function public.latest_chat_messages_for_threads(uuid[]) owner to postgres;
85+
86+
revoke all privileges on function public.latest_chat_messages_for_threads(uuid[])
87+
from anon, authenticated, public;
88+
89+
grant execute on function public.latest_chat_messages_for_threads(uuid[])
90+
to authenticated, service_role;
91+
92+
create or replace function private.enforce_chat_thread_insert()
93+
returns trigger
94+
language plpgsql
95+
security definer
96+
set search_path = ''
97+
as $$
98+
declare
99+
jwt_role text;
100+
begin
101+
jwt_role := nullif(current_setting('request.jwt.claim.role', true), '');
102+
103+
if jwt_role = 'service_role'
104+
or ((select auth.uid()) is null and jwt_role is null)
105+
then
106+
return new;
107+
end if;
108+
109+
if (select auth.uid()) is null or new.initiator_id is distinct from (select auth.uid()) then
110+
raise exception 'Users can only start chat threads as themselves.'
111+
using errcode = '42501';
112+
end if;
113+
114+
if (
115+
select count(*)
116+
from public.chat_messages
117+
where sender_id = new.initiator_id
118+
and created_at >= now() - interval '1 hour'
119+
) >= 10 then
120+
raise exception 'Too many messages sent recently to start a new chat thread.'
121+
using errcode = '42501';
122+
end if;
123+
124+
if (
125+
select count(*)
126+
from public.chat_threads
127+
where initiator_id = new.initiator_id
128+
and created_at >= now() - interval '1 hour'
129+
) >= 6 then
130+
raise exception 'Too many chat threads started recently.'
131+
using errcode = '42501';
132+
end if;
133+
134+
return new;
135+
end;
136+
$$;

0 commit comments

Comments
 (0)