fix(sandbox): use posting engine and recover failed seeds (#2297)
* fix(sandbox): seed through posting engine and recover failed attempts * test(sandbox): align CI auth schema for anonymous users
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
-- A settings row is created before demo data and is not a completion marker.
|
||||
-- User-scoped coordination also covers the first request, before a company exists.
|
||||
CREATE TABLE public.sandbox_seed_attempts (
|
||||
user_id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
company_id uuid REFERENCES public.companies(id) ON DELETE SET NULL,
|
||||
attempt_id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
status text NOT NULL CHECK (status IN ('running', 'failed', 'complete')),
|
||||
started_at timestamptz NOT NULL DEFAULT now(),
|
||||
completed_at timestamptz,
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX sandbox_seed_attempts_company_id ON public.sandbox_seed_attempts(company_id);
|
||||
ALTER TABLE public.sandbox_seed_attempts ENABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON public.sandbox_seed_attempts FROM anon, authenticated;
|
||||
GRANT SELECT ON public.sandbox_seed_attempts TO authenticated;
|
||||
CREATE POLICY sandbox_seed_attempts_read_own ON public.sandbox_seed_attempts
|
||||
FOR SELECT TO authenticated USING (user_id = (SELECT auth.uid()));
|
||||
CREATE TRIGGER sandbox_seed_attempts_updated_at BEFORE UPDATE ON public.sandbox_seed_attempts
|
||||
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
||||
|
||||
CREATE FUNCTION public.claim_sandbox_seed()
|
||||
RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
v_user uuid := auth.uid();
|
||||
v_attempt public.sandbox_seed_attempts%ROWTYPE;
|
||||
v_company uuid;
|
||||
v_complete boolean := false;
|
||||
BEGIN
|
||||
-- Read the server-owned auth record, not editable user_metadata.
|
||||
IF v_user IS NULL OR NOT EXISTS (
|
||||
SELECT 1 FROM auth.users WHERE id = v_user AND is_anonymous IS TRUE
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Sandbox requires an anonymous user' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended('sandbox-seed:' || v_user::text, 0));
|
||||
SELECT * INTO v_attempt FROM public.sandbox_seed_attempts WHERE user_id = v_user FOR UPDATE;
|
||||
IF FOUND THEN
|
||||
IF v_attempt.status = 'running' AND v_attempt.started_at > now() - interval '15 minutes' THEN
|
||||
RETURN jsonb_build_object('status', 'busy');
|
||||
END IF;
|
||||
v_company := v_attempt.company_id;
|
||||
v_complete := v_attempt.status = 'complete';
|
||||
ELSE
|
||||
SELECT active_company_id INTO v_company FROM public.user_preferences WHERE user_id = v_user;
|
||||
-- Legacy successful seeds ended by linking all three payroll vouchers.
|
||||
-- Older or partial demos without that evidence are conservatively replaced.
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.salary_runs sr
|
||||
JOIN public.journal_entries s ON s.id = sr.salary_entry_id AND s.company_id = sr.company_id AND s.status = 'posted'
|
||||
JOIN public.journal_entries a ON a.id = sr.avgifter_entry_id AND a.company_id = sr.company_id AND a.status = 'posted'
|
||||
JOIN public.journal_entries v ON v.id = sr.vacation_entry_id AND v.company_id = sr.company_id AND v.status = 'posted'
|
||||
WHERE sr.company_id = v_company AND sr.status = 'booked'
|
||||
) INTO v_complete;
|
||||
END IF;
|
||||
|
||||
IF v_company IS NOT NULL THEN
|
||||
-- Never archive, adopt or seed a real/shared company, even for an anonymous caller.
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM public.companies c JOIN public.company_members cm ON cm.company_id = c.id
|
||||
WHERE c.id = v_company AND c.created_by = v_user
|
||||
AND cm.user_id = v_user AND cm.role = 'owner'
|
||||
) OR EXISTS (
|
||||
SELECT 1 FROM public.company_members WHERE company_id = v_company AND user_id <> v_user
|
||||
) OR EXISTS (
|
||||
SELECT 1 FROM public.company_settings WHERE company_id = v_company AND is_sandbox IS NOT TRUE
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Not an exclusively owned sandbox company' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
IF v_complete AND EXISTS (
|
||||
SELECT 1 FROM public.companies c JOIN public.company_settings cs ON cs.company_id = c.id
|
||||
WHERE c.id = v_company AND c.archived_at IS NULL AND cs.is_sandbox
|
||||
) THEN
|
||||
INSERT INTO public.sandbox_seed_attempts(user_id, company_id, status, completed_at)
|
||||
VALUES (v_user, v_company, 'complete', now())
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
INSERT INTO public.user_preferences(user_id, active_company_id) VALUES (v_user, v_company)
|
||||
ON CONFLICT (user_id) DO UPDATE SET active_company_id = EXCLUDED.active_company_id;
|
||||
RETURN jsonb_build_object('status', 'complete', 'company_id', v_company);
|
||||
END IF;
|
||||
-- Preserve partial entries and documents; ordinary sandbox expiry owns deletion.
|
||||
UPDATE public.companies SET archived_at = now(), archived_by = v_user
|
||||
WHERE id = v_company AND archived_at IS NULL;
|
||||
END IF;
|
||||
|
||||
v_company := public.create_company_with_owner('Sandlådan Konsult', 'enskild_firma', true);
|
||||
-- Make even a crash before the first seed write discoverable by sandbox cleanup.
|
||||
INSERT INTO public.company_settings(user_id, company_id, is_sandbox)
|
||||
VALUES (v_user, v_company, true);
|
||||
INSERT INTO public.sandbox_seed_attempts(user_id, company_id, status)
|
||||
VALUES (v_user, v_company, 'running')
|
||||
ON CONFLICT (user_id) DO UPDATE SET company_id = EXCLUDED.company_id,
|
||||
attempt_id = gen_random_uuid(), status = 'running', started_at = now(), completed_at = NULL
|
||||
RETURNING * INTO v_attempt;
|
||||
RETURN jsonb_build_object('status', 'running', 'company_id', v_company, 'attempt_id', v_attempt.attempt_id);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION public.finish_sandbox_seed(p_attempt_id uuid, p_success boolean)
|
||||
RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
IF auth.uid() IS NULL OR NOT EXISTS (
|
||||
SELECT 1 FROM auth.users WHERE id = auth.uid() AND is_anonymous IS TRUE
|
||||
) THEN
|
||||
RAISE EXCEPTION 'Sandbox requires an anonymous user' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
UPDATE public.sandbox_seed_attempts
|
||||
SET status = CASE WHEN p_success THEN 'complete' ELSE 'failed' END,
|
||||
completed_at = CASE WHEN p_success THEN now() ELSE NULL END
|
||||
WHERE user_id = auth.uid() AND attempt_id = p_attempt_id AND status = 'running';
|
||||
RETURN FOUND;
|
||||
END;
|
||||
$$;
|
||||
REVOKE ALL ON FUNCTION public.claim_sandbox_seed() FROM PUBLIC, anon;
|
||||
REVOKE ALL ON FUNCTION public.finish_sandbox_seed(uuid, boolean) FROM PUBLIC, anon;
|
||||
GRANT EXECUTE ON FUNCTION public.claim_sandbox_seed() TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.finish_sandbox_seed(uuid, boolean) TO authenticated;
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user