fix(db): add atomic collection creation RPC path with migration script

This commit is contained in:
Lukas Müllner 2026-03-06 08:14:35 +01:00
parent 38de6d8b86
commit fb96d0bfb7
3 changed files with 11586 additions and 8 deletions

11498
DB_README.md

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,48 @@
-- Atomic collection creation + owner membership insert
-- Run this in Supabase SQL Editor.
create or replace function public.create_collection_with_owner(
p_name text,
p_description text default null
)
returns public.collections
language plpgsql
security definer
set search_path = public
as $$
declare
v_user_id uuid := auth.uid();
v_collection public.collections;
begin
if v_user_id is null then
raise exception 'Not authenticated';
end if;
insert into public.collections (
name,
owner_id,
description
)
values (
p_name,
v_user_id,
nullif(trim(p_description), '')
)
returning * into v_collection;
insert into public.collection_members (
collection_id,
user_id,
role
)
values (
v_collection.id,
v_user_id,
'owner'
);
return v_collection;
end;
$$;
grant execute on function public.create_collection_with_owner(text, text) to authenticated;

View file

@ -263,13 +263,57 @@ class CollectionService {
}) async { }) async {
final userId = _requireUserId(); final userId = _requireUserId();
final trimmedDescription = description?.trim();
final normalizedDescription =
(trimmedDescription != null && trimmedDescription.isNotEmpty)
? trimmedDescription
: null;
try {
final rpcResult = await supabase.rpc(
'create_collection_with_owner',
params: {
'p_name': name,
'p_description': normalizedDescription,
},
);
final row = rpcResult is List
? (rpcResult.isNotEmpty
? rpcResult.first as Map<String, dynamic>
: <String, dynamic>{})
: rpcResult as Map<String, dynamic>;
if (row.isNotEmpty) {
return Collection(
id: row['id'] as String,
name: row['name'] as String,
description: row['description'] as String?,
ownerId: row['owner_id'] as String,
createdAt: DateTime.parse(row['created_at'] as String),
role: 'owner',
itemCount: 0,
memberCount: 1,
);
}
} catch (e) {
final message = e.toString();
final rpcUnavailable = message.contains('create_collection_with_owner') &&
(message.contains('not found') ||
message.contains('does not exist') ||
message.contains('PGRST202'));
if (!rpcUnavailable) {
rethrow;
}
debugPrint('CollectionService.create RPC unavailable, using fallback: $e');
}
final row = await supabase final row = await supabase
.from('collections') .from('collections')
.insert({ .insert({
'name': name, 'name': name,
'owner_id': userId, 'owner_id': userId,
if (description != null && description.isNotEmpty) 'description': normalizedDescription,
'description': description,
}) })
.select() .select()
.single(); .single();