mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
Lets flows attach an image, video, or document (e.g. PDF invoice / receipt) to a customer message — the most-requested gap in v1 was that nodes were text-only. Single node with a `media_type` discriminator rather than three node types: Meta's wire payload differs only in the top-level key, so three types would triple the surface area (forms, engine cases, menu entries) for no behavioural difference. Files are uploaded directly from the builder to a new public `flow-media` Supabase Storage bucket (per-user RLS, 16 MB cap, MIME allowlist) and the resulting public URL is what Meta fetches at send time. Same trade-off as the existing avatars bucket. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
115 lines
4.0 KiB
SQL
115 lines
4.0 KiB
SQL
-- ============================================================
|
|
-- 016_flow_media.sql
|
|
--
|
|
-- Adds support for media nodes in conversational flows:
|
|
--
|
|
-- 1. New 'send_media' value on `flow_nodes.node_type` CHECK
|
|
-- constraint. Mirrors the same drop-and-recreate pattern migration
|
|
-- 010 used to land the original list. The node config lives in
|
|
-- JSONB and is shape-checked by the validator + TS types, not the
|
|
-- DB.
|
|
--
|
|
-- 2. `flow-media` Supabase Storage bucket where the builder uploads
|
|
-- the file the customer will receive. Public bucket so Meta can
|
|
-- pull the URL without auth — same trade-off as the avatars
|
|
-- bucket (see migration 008). Per-user RLS on writes scopes the
|
|
-- bucket so one tenant can't read/overwrite another's media.
|
|
--
|
|
-- Path convention:
|
|
-- flow-media/{auth.uid()}/<timestamp>-<basename>.<ext>
|
|
-- First path segment must equal auth.uid()::text — same shape
|
|
-- migration 008 uses for avatars so the policy code reads the
|
|
-- same.
|
|
--
|
|
-- Size limit 16 MB — Meta's WhatsApp Cloud API caps documents at
|
|
-- 100 MB but videos at 16 MB and images at 5 MB; we pick the
|
|
-- tightest universal cap that still works for the document case
|
|
-- that prompted this feature (PDF invoices / receipts).
|
|
--
|
|
-- Idempotent — safe to re-run.
|
|
-- ============================================================
|
|
|
|
-- ============================================================
|
|
-- 1. flow_nodes.node_type — add 'send_media'
|
|
-- ============================================================
|
|
ALTER TABLE flow_nodes
|
|
DROP CONSTRAINT IF EXISTS flow_nodes_node_type_check;
|
|
|
|
ALTER TABLE flow_nodes
|
|
ADD CONSTRAINT flow_nodes_node_type_check
|
|
CHECK (node_type IN (
|
|
'start',
|
|
'send_buttons',
|
|
'send_list',
|
|
'send_message',
|
|
'send_media',
|
|
'collect_input',
|
|
'condition',
|
|
'set_tag',
|
|
'handoff',
|
|
'http_fetch',
|
|
'end'
|
|
));
|
|
|
|
-- ============================================================
|
|
-- 2. flow-media storage bucket
|
|
-- ============================================================
|
|
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
|
VALUES (
|
|
'flow-media',
|
|
'flow-media',
|
|
TRUE,
|
|
16777216, -- 16 MB (Meta video cap; documents/images fit under this)
|
|
ARRAY[
|
|
-- Images
|
|
'image/png', 'image/jpeg', 'image/webp',
|
|
-- Videos
|
|
'video/mp4', 'video/3gpp',
|
|
-- Documents
|
|
'application/pdf',
|
|
'application/vnd.ms-powerpoint',
|
|
'application/msword',
|
|
'application/vnd.ms-excel',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'text/plain'
|
|
]
|
|
)
|
|
ON CONFLICT (id) DO UPDATE
|
|
SET
|
|
public = EXCLUDED.public,
|
|
file_size_limit = EXCLUDED.file_size_limit,
|
|
allowed_mime_types = EXCLUDED.allowed_mime_types;
|
|
|
|
-- Policies live on storage.objects. Same drop-then-create pattern as
|
|
-- migration 008 (no CREATE POLICY IF NOT EXISTS in Postgres).
|
|
DROP POLICY IF EXISTS "Flow media is publicly readable" ON storage.objects;
|
|
CREATE POLICY "Flow media is publicly readable"
|
|
ON storage.objects FOR SELECT
|
|
USING (bucket_id = 'flow-media');
|
|
|
|
DROP POLICY IF EXISTS "Users can upload their own flow media" ON storage.objects;
|
|
CREATE POLICY "Users can upload their own flow media"
|
|
ON storage.objects FOR INSERT
|
|
WITH CHECK (
|
|
bucket_id = 'flow-media'
|
|
AND auth.uid()::text = (storage.foldername(name))[1]
|
|
);
|
|
|
|
DROP POLICY IF EXISTS "Users can update their own flow media" ON storage.objects;
|
|
CREATE POLICY "Users can update their own flow media"
|
|
ON storage.objects FOR UPDATE
|
|
USING (
|
|
bucket_id = 'flow-media'
|
|
AND auth.uid()::text = (storage.foldername(name))[1]
|
|
);
|
|
|
|
DROP POLICY IF EXISTS "Users can delete their own flow media" ON storage.objects;
|
|
CREATE POLICY "Users can delete their own flow media"
|
|
ON storage.objects FOR DELETE
|
|
USING (
|
|
bucket_id = 'flow-media'
|
|
AND auth.uid()::text = (storage.foldername(name))[1]
|
|
);
|