diff --git a/supabase/migrations/20260717170414_fix_fetch_gem_with_people_share_token_key.sql b/supabase/migrations/20260717170414_fix_fetch_gem_with_people_share_token_key.sql new file mode 100644 index 0000000..b06c2d0 --- /dev/null +++ b/supabase/migrations/20260717170414_fix_fetch_gem_with_people_share_token_key.sql @@ -0,0 +1,46 @@ +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.fetch_gem_with_people(gem_id_param uuid) + RETURNS jsonb + LANGUAGE plpgsql + AS $function$ +DECLARE + gem_data_var jsonb; + lines_data_var jsonb; + people_data_var jsonb; +BEGIN + SELECT + row_to_json(g) INTO gem_data_var + FROM + public.gems g + WHERE + g.id = gem_id_param + LIMIT 1; + -- The client model requires the `gem_share_tokens` key to be present, even + -- though it's not needed here. + gem_data_var := jsonb_set(gem_data_var, '{gem_share_tokens}', 'null'::jsonb, TRUE); + -- Fetch the lines associated with the gem + SELECT + jsonb_agg(DISTINCT l) FILTER (WHERE l.id IS NOT NULL) INTO lines_data_var + FROM + public.lines l + WHERE + l.gem_id = gem_id_param; + -- Fetch the people associated with the lines + SELECT + jsonb_agg(DISTINCT p) FILTER (WHERE p.id IS NOT NULL) INTO people_data_var + FROM + public.people p + WHERE + p.id IN ( + SELECT + l.person_id + FROM + public.lines l + WHERE + l.gem_id = gem_id_param); + -- Combine gem_data, lines_data, and people_data into a single JSONB object + RETURN jsonb_build_object('gem', jsonb_set(gem_data_var, '{lines}', coalesce(lines_data_var, '[]'::jsonb), TRUE), 'people', people_data_var); +END; +$function$ +; diff --git a/supabase/schemas/public/functions/fetch_gem_with_people.sql b/supabase/schemas/public/functions/fetch_gem_with_people.sql index e64dc75..f8fb927 100644 --- a/supabase/schemas/public/functions/fetch_gem_with_people.sql +++ b/supabase/schemas/public/functions/fetch_gem_with_people.sql @@ -14,6 +14,9 @@ BEGIN WHERE g.id = gem_id_param LIMIT 1; + -- The client model requires the `gem_share_tokens` key to be present, even + -- though it's not needed here. + gem_data_var := jsonb_set(gem_data_var, '{gem_share_tokens}', 'null'::jsonb, TRUE); -- Fetch the lines associated with the gem SELECT jsonb_agg(DISTINCT l) FILTER (WHERE l.id IS NOT NULL) INTO lines_data_var @@ -35,6 +38,6 @@ BEGIN WHERE l.gem_id = gem_id_param); -- Combine gem_data, lines_data, and people_data into a single JSONB object - RETURN jsonb_build_object('gem', jsonb_set(gem_data_var, '{lines}', lines_data_var, TRUE), 'people', people_data_var); + RETURN jsonb_build_object('gem', jsonb_set(gem_data_var, '{lines}', coalesce(lines_data_var, '[]'::jsonb), TRUE), 'people', people_data_var); END; $function$; diff --git a/supabase/tests/public/functions/fetch_gem_with_people.test.sql b/supabase/tests/public/functions/fetch_gem_with_people.test.sql new file mode 100644 index 0000000..885ff04 --- /dev/null +++ b/supabase/tests/public/functions/fetch_gem_with_people.test.sql @@ -0,0 +1,98 @@ +BEGIN; +SELECT no_plan(); + + -- Arrange (shared setup) + SELECT tests.create_chucklechest_user('owner'); + SELECT tests.create_chest('owner') AS chest_id \gset + + SELECT tests.authenticate_as_service_role(); + INSERT INTO public.people (nickname, date_of_birth, chest_id) + VALUES ('Alice', '1990-01-01', :'chest_id') + RETURNING id AS person_id \gset + +SAVEPOINT arrange_all; + + + -- Arrange + SELECT tests.authenticate_as_service_role(); + INSERT INTO public.gems (number, occurred_at, chest_id) + VALUES (1, '2024-06-15'::date, :'chest_id') + RETURNING id AS gem_id \gset + + INSERT INTO public.lines (text, gem_id, chest_id, person_id) + VALUES ('Hello!', :'gem_id', :'chest_id', :person_id); + + -- Act + SELECT public.fetch_gem_with_people(:'gem_id') AS result \gset + + -- Assert + SELECT is( + (:'result'::jsonb -> 'gem' -> 'gem_share_tokens'), + 'null'::jsonb, + 'Given: gem with no share token. When: fetch_gem_with_people called. Then: gem_share_tokens key is present with a JSON null value (client model requires the key to exist).' + ); + + SELECT is( + jsonb_array_length(:'result'::jsonb -> 'gem' -> 'lines'), + 1, + 'Given: gem with one line. When: fetch_gem_with_people called. Then: lines array has 1 entry.' + ); + + SELECT is( + (:'result'::jsonb -> 'gem' -> 'lines' -> 0 ->> 'text'), + 'Hello!', + 'Given: gem with one line. When: fetch_gem_with_people called. Then: lines array contains the line text.' + ); + + SELECT is( + (:'result'::jsonb -> 'people' -> 0 ->> 'nickname'), + 'Alice', + 'Given: line assigned to a person. When: fetch_gem_with_people called. Then: people array contains that person.' + ); + + +ROLLBACK TO arrange_all; + + + -- Arrange + SELECT tests.authenticate_as_service_role(); + INSERT INTO public.gems (number, occurred_at, chest_id) + VALUES (1, '2024-06-15'::date, :'chest_id') + RETURNING id AS gem_id \gset + + INSERT INTO public.lines (text, gem_id, chest_id, person_id) + VALUES ('Just narration.', :'gem_id', :'chest_id', NULL); + + -- Act + SELECT public.fetch_gem_with_people(:'gem_id') AS result \gset + + -- Assert + SELECT is( + (:'result'::jsonb -> 'people'), + 'null'::jsonb, + 'Given: gem with no lines assigned to a person. When: fetch_gem_with_people called. Then: people is null.' + ); + + +ROLLBACK TO arrange_all; + + + -- Arrange + SELECT tests.authenticate_as_service_role(); + INSERT INTO public.gems (number, occurred_at, chest_id) + VALUES (1, '2024-06-15'::date, :'chest_id') + RETURNING id AS gem_id \gset + + -- Act + SELECT public.fetch_gem_with_people(:'gem_id') AS result \gset + + -- Assert + SELECT is( + (:'result'::jsonb -> 'gem' ->> 'id')::uuid, + :'gem_id'::uuid, + 'Given: gem with zero lines. When: fetch_gem_with_people called. Then: gem data is still returned (not nulled out).' + ); + + +SELECT * FROM finish(); +ROLLBACK;