fix(db): add atomic collection creation RPC path with migration script
This commit is contained in:
parent
38de6d8b86
commit
fb96d0bfb7
3 changed files with 11586 additions and 8 deletions
11498
DB_README.md
11498
DB_README.md
File diff suppressed because one or more lines are too long
48
db/atomic_create_collection_with_owner.sql
Normal file
48
db/atomic_create_collection_with_owner.sql
Normal 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;
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue