chore: improve Supabase migrations via CLI after login and fix compatibility issues
- Update supabase/push-migrations.js: - Detect modern Supabase CLI link state (supabase/.temp/project-ref) - Prefer `supabase db query --linked --file` (works in this env where direct postgres connections fail with ENOTFOUND/network unreachable) - Updated docs/comments for `supabase login` + `supabase link --project-ref wnzkhezyhnfzhkhiflrp` workflow - Add MEMORY.md capturing the Supabase login/link session, tooling changes, migration patches applied, and gotchas - Patch migrations for current remote schema (column drift, syntax, idempotency, pre-existing tables): - 091_brand_plan_tier.sql: fix extra paren in get_brand_plan_info - 145_create_product_images_bucket.sql: allowed_mime_types + DROP POLICY IF EXISTS + safer bucket insert - 148_public_stops_rpc.sql: quote reserved "time" column in RETURNS TABLE + SELECT - 200_production_features.sql: add ALTER TABLE for user_activity_logs (originated in 036) so policies/indexes validate - 201_seed_data.sql: trim demo seeds with outdated column lists (products/stops/etc.); keep only compatible brands insert - Include 202_fix_admin_create_stop.sql and related stop creation updates (src/actions/stops/create-stop.ts, 147_admin_create_stop_rpcs.sql, ADMIN_CREATE_STOP_FIX.sql) - Update CLAUDE.md with pointer to MEMORY.md for recent migration work Applied via the new flow (after supabase login + link): 084, 091, 142–148, 200–202 etc. Refs: Supabase CLI now linked; use `node supabase/push-migrations.js <prefix>` or `npm run migrate:one NNN`
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
-- ============================================================================
|
||||
-- Grok Fix Prompt: admin_create_stop (copy-paste ready for Supabase SQL Editor)
|
||||
-- ============================================================================
|
||||
-- This is the exact "Best Practice Version" from the fix prompt, with
|
||||
-- additional comments. Run this directly in the Supabase SQL Editor
|
||||
-- (https://supabase.com/dashboard/project/wnzkhezyhnfzhkhiflrp/sql) if the
|
||||
-- migration push cannot be used.
|
||||
--
|
||||
-- After running, PostgREST may need a schema reload:
|
||||
-- NOTIFY pgrst, 'reload schema';
|
||||
--
|
||||
-- Then test "Add New Stop" again.
|
||||
--
|
||||
-- The calling code (JS/TS) is in:
|
||||
-- src/actions/stops/create-stop.ts (the fetch to /rest/v1/rpc/admin_create_stop)
|
||||
-- src/components/admin/AddStopModal.tsx
|
||||
-- src/components/admin/NewStopForm.tsx
|
||||
-- (all go through the server action createStop which builds the p_* args)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.admin_create_stop(
|
||||
p_active boolean,
|
||||
p_address text,
|
||||
p_brand_id uuid,
|
||||
p_city text,
|
||||
p_cutoff_time time,
|
||||
p_date date,
|
||||
p_location text,
|
||||
p_state text,
|
||||
p_time time,
|
||||
p_zip text
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
new_stop_id uuid;
|
||||
BEGIN
|
||||
INSERT INTO stops (
|
||||
active,
|
||||
address,
|
||||
brand_id,
|
||||
city,
|
||||
cutoff_time,
|
||||
date,
|
||||
location,
|
||||
state,
|
||||
time,
|
||||
zip
|
||||
)
|
||||
VALUES (
|
||||
p_active,
|
||||
p_address,
|
||||
p_brand_id,
|
||||
p_city,
|
||||
p_cutoff_time,
|
||||
p_date,
|
||||
p_location,
|
||||
p_state,
|
||||
p_time,
|
||||
p_zip
|
||||
)
|
||||
RETURNING id INTO new_stop_id;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'success', true,
|
||||
'stop_id', new_stop_id,
|
||||
'message', 'Stop created successfully'
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN jsonb_build_object(
|
||||
'success', false,
|
||||
'error', SQLERRM
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Important: grant so PostgREST (anon key from server action) can invoke it.
|
||||
GRANT EXECUTE ON FUNCTION public.admin_create_stop(
|
||||
boolean, text, uuid, text, time, date, text, text, time, text
|
||||
) TO anon, authenticated, service_role;
|
||||
|
||||
-- Reload cache so the function is immediately visible (avoids PGRST202).
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
|
||||
-- NOTE: The above simple version may fail on INSERT because the stops table
|
||||
-- requires "slug" and "status" (NOT NULL, no defaults in all cases).
|
||||
-- Prefer the enhanced version in migrations/202_fix_admin_create_stop.sql
|
||||
-- which includes slug generation and status='draft' while using a compatible
|
||||
-- signature and the success/stop_id return shape.
|
||||
@@ -62,7 +62,7 @@ BEGIN
|
||||
|
||||
-- Count current usage
|
||||
SELECT COUNT(*) INTO v_user_count FROM admin_users WHERE brand_id = p_brand_id AND deleted_at IS NULL;
|
||||
SELECT COUNT(*) INTO v_active_stops FROM stops WHERE brand_id = p_brand_id AND active = true AND date >= date_trunc('month', now()));
|
||||
SELECT COUNT(*) INTO v_active_stops FROM stops WHERE brand_id = p_brand_id AND active = true AND date >= date_trunc('month', now());
|
||||
SELECT COUNT(*) INTO v_product_count FROM products WHERE brand_id = p_brand_id AND deleted_at IS NULL;
|
||||
|
||||
SELECT jsonb_build_object(
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
-- Create product-images bucket for product images
|
||||
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowedMimeTypes)
|
||||
VALUES ('product-images', 'product-images', true, 5242880, ARRAY['image/png', 'image/jpeg', 'image/webp'])
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
-- Create product-images bucket for product images (idempotent)
|
||||
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
SELECT 'product-images', 'product-images', true, 5242880, ARRAY['image/png', 'image/jpeg', 'image/webp']
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM storage.buckets WHERE id = 'product-images' OR name = 'product-images'
|
||||
);
|
||||
|
||||
-- Enable public access to product-images bucket
|
||||
-- Enable public access to product-images bucket (re-runnable)
|
||||
DROP POLICY IF EXISTS "Public can view product-images" ON storage.objects;
|
||||
CREATE POLICY "Public can view product-images"
|
||||
ON storage.objects FOR SELECT
|
||||
USING (bucket_id = 'product-images');
|
||||
|
||||
DROP POLICY IF EXISTS "Admins can upload product-images" ON storage.objects;
|
||||
CREATE POLICY "Admins can upload product-images"
|
||||
ON storage.objects FOR INSERT
|
||||
WITH CHECK (bucket_id = 'product-images');
|
||||
@@ -16,14 +16,36 @@
|
||||
-- p_brand_id for explicit brand scoping. The application layer
|
||||
-- (server actions) still validates the caller has can_manage_stops
|
||||
-- via getAdminUser() before invoking the RPC.
|
||||
--
|
||||
-- Table schema (relevant columns):
|
||||
-- id UUID PK
|
||||
-- brand_id UUID FK brands
|
||||
-- city TEXT
|
||||
-- state TEXT
|
||||
-- location TEXT
|
||||
-- date TIMESTAMPTZ
|
||||
-- time TEXT -- free-form ("8:00 AM – 2:00 PM")
|
||||
-- address TEXT
|
||||
-- zip TEXT
|
||||
-- cutoff_time TIMESTAMPTZ -- accepts ISO datetime OR "HH:MM"
|
||||
-- slug TEXT
|
||||
-- active BOOLEAN
|
||||
-- status TEXT -- 'draft' | 'active' | ...
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ── 1. admin_create_stop ───────────────────────────────────────────────────
|
||||
-- Inserts a single stop. Slug is derived from city + date; if a stop with
|
||||
-- the same slug already exists for the brand, a numeric suffix is appended.
|
||||
--
|
||||
-- Robust type handling:
|
||||
-- * p_date TEXT -> cast to TIMESTAMPTZ; NULLIF handles empty string
|
||||
-- * p_cutoff_time TEXT -> accepts full ISO datetime ("2025-01-01T08:00")
|
||||
-- OR a time-only value ("08:00" / "08:00:00") which is combined with
|
||||
-- p_date to form a TIMESTAMPTZ. NULLIF handles empty string.
|
||||
-- * p_time TEXT is passed through as-is (the column is free-form text).
|
||||
|
||||
CREATE OR REPLACE FUNCTION admin_create_stop(
|
||||
CREATE OR REPLACE FUNCTION public.admin_create_stop(
|
||||
p_brand_id UUID,
|
||||
p_city TEXT,
|
||||
p_state TEXT,
|
||||
@@ -37,13 +59,16 @@ CREATE OR REPLACE FUNCTION admin_create_stop(
|
||||
)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER SET search_path = public
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_slug TEXT;
|
||||
v_slug_base TEXT;
|
||||
v_id UUID;
|
||||
v_counter INT := 0;
|
||||
v_date_value TIMESTAMPTZ;
|
||||
v_cutoff_val TIMESTAMPTZ;
|
||||
v_slug TEXT;
|
||||
v_slug_base TEXT;
|
||||
v_id UUID;
|
||||
v_counter INT := 0;
|
||||
BEGIN
|
||||
IF p_brand_id IS NULL THEN
|
||||
RAISE EXCEPTION 'brand_id is required';
|
||||
@@ -52,57 +77,83 @@ BEGIN
|
||||
RAISE EXCEPTION 'city is required';
|
||||
END IF;
|
||||
|
||||
-- ── Parse p_date ─────────────────────────────────────────────────────────
|
||||
v_date_value := NULLIF(trim(p_date), '')::TIMESTAMPTZ;
|
||||
IF v_date_value IS NULL THEN
|
||||
RAISE EXCEPTION 'date is required';
|
||||
END IF;
|
||||
|
||||
-- ── Parse p_cutoff_time (accepts ISO datetime OR "HH:MM[:SS]") ───────────
|
||||
IF p_cutoff_time IS NULL OR length(trim(p_cutoff_time)) = 0 THEN
|
||||
v_cutoff_val := NULL;
|
||||
ELSIF p_cutoff_time ~ '^\d{1,2}:\d{2}(:\d{2})?$' THEN
|
||||
-- Time-only value — combine with the stop's date
|
||||
v_cutoff_val := (v_date_value::DATE + trim(p_cutoff_time)::TIME)::TIMESTAMPTZ;
|
||||
ELSE
|
||||
-- Assume a full ISO datetime string
|
||||
v_cutoff_val := trim(p_cutoff_time)::TIMESTAMPTZ;
|
||||
END IF;
|
||||
|
||||
-- ── Derive unique slug ───────────────────────────────────────────────────
|
||||
v_slug_base := lower(regexp_replace(trim(p_city), '\s+', '-', 'g'))
|
||||
|| '-' || COALESCE(NULLIF(trim(p_date), ''), CURRENT_DATE::TEXT);
|
||||
|| '-' || to_char(v_date_value, 'YYYY-MM-DD');
|
||||
v_slug := v_slug_base;
|
||||
|
||||
-- Ensure unique slug per brand by appending a counter if needed
|
||||
WHILE EXISTS (SELECT 1 FROM stops WHERE brand_id = p_brand_id AND slug = v_slug) LOOP
|
||||
v_counter := v_counter + 1;
|
||||
v_slug := v_slug_base || '-' || v_counter;
|
||||
END LOOP;
|
||||
|
||||
-- ── Insert ───────────────────────────────────────────────────────────────
|
||||
INSERT INTO stops (
|
||||
brand_id, city, state, location, date, time, slug,
|
||||
address, zip, cutoff_time, active, status
|
||||
) VALUES (
|
||||
p_brand_id, p_city, p_state, p_location, p_date, p_time, v_slug,
|
||||
p_address, p_zip, p_cutoff_time, p_active, 'draft'
|
||||
p_brand_id, p_city, p_state, p_location,
|
||||
v_date_value, p_time, v_slug,
|
||||
p_address, p_zip, v_cutoff_val, p_active, 'draft'
|
||||
)
|
||||
RETURNING id INTO v_id;
|
||||
|
||||
RETURN jsonb_build_object('id', v_id, 'slug', v_slug);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
-- Re-raise as a structured error so the client gets a useful message
|
||||
RAISE EXCEPTION 'admin_create_stop failed: % (SQLSTATE %)', SQLERRM, SQLSTATE;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- ── 2. admin_create_stops_batch ────────────────────────────────────────────
|
||||
-- Inserts many stops in one call. Returns JSONB with created IDs + slugs.
|
||||
-- On any per-row failure the whole batch rolls back (transactional).
|
||||
-- Same robust date / cutoff_time handling as the single-row RPC.
|
||||
|
||||
CREATE OR REPLACE FUNCTION admin_create_stops_batch(
|
||||
CREATE OR REPLACE FUNCTION public.admin_create_stops_batch(
|
||||
p_brand_id UUID,
|
||||
p_stops JSONB -- array of {city, state, location, date, time, address?, zip?, cutoff_time?, active?}
|
||||
)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER SET search_path = public
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_row JSONB;
|
||||
v_result JSONB := '[]'::JSONB;
|
||||
v_slug_base TEXT;
|
||||
v_slug TEXT;
|
||||
v_counter INT;
|
||||
v_id UUID;
|
||||
v_city TEXT;
|
||||
v_state TEXT;
|
||||
v_location TEXT;
|
||||
v_date TEXT;
|
||||
v_time TEXT;
|
||||
v_address TEXT;
|
||||
v_zip TEXT;
|
||||
v_cutoff TEXT;
|
||||
v_active BOOLEAN;
|
||||
v_row JSONB;
|
||||
v_result JSONB := '[]'::JSONB;
|
||||
v_slug_base TEXT;
|
||||
v_slug TEXT;
|
||||
v_counter INT;
|
||||
v_id UUID;
|
||||
v_city TEXT;
|
||||
v_state TEXT;
|
||||
v_location TEXT;
|
||||
v_date_text TEXT;
|
||||
v_time TEXT;
|
||||
v_address TEXT;
|
||||
v_zip TEXT;
|
||||
v_cutoff_txt TEXT;
|
||||
v_active BOOLEAN;
|
||||
v_date_value TIMESTAMPTZ;
|
||||
v_cutoff_val TIMESTAMPTZ;
|
||||
BEGIN
|
||||
IF p_brand_id IS NULL THEN
|
||||
RAISE EXCEPTION 'brand_id is required';
|
||||
@@ -113,22 +164,37 @@ BEGIN
|
||||
END IF;
|
||||
|
||||
FOR v_row IN SELECT * FROM jsonb_array_elements(p_stops) LOOP
|
||||
v_city := v_row->>'city';
|
||||
v_state := v_row->>'state';
|
||||
v_location := v_row->>'location';
|
||||
v_date := v_row->>'date';
|
||||
v_time := v_row->>'time';
|
||||
v_address := v_row->>'address';
|
||||
v_zip := v_row->>'zip';
|
||||
v_cutoff := v_row->>'cutoff_time';
|
||||
v_active := COALESCE((v_row->>'active')::BOOLEAN, false);
|
||||
v_city := v_row->>'city';
|
||||
v_state := v_row->>'state';
|
||||
v_location := v_row->>'location';
|
||||
v_date_text := v_row->>'date';
|
||||
v_time := v_row->>'time';
|
||||
v_address := v_row->>'address';
|
||||
v_zip := v_row->>'zip';
|
||||
v_cutoff_txt := v_row->>'cutoff_time';
|
||||
v_active := COALESCE((v_row->>'active')::BOOLEAN, false);
|
||||
|
||||
IF v_city IS NULL OR length(trim(v_city)) = 0 THEN
|
||||
RAISE EXCEPTION 'city is required for all stops';
|
||||
END IF;
|
||||
|
||||
-- Parse date
|
||||
v_date_value := NULLIF(trim(v_date_text), '')::TIMESTAMPTZ;
|
||||
IF v_date_value IS NULL THEN
|
||||
RAISE EXCEPTION 'date is required for all stops';
|
||||
END IF;
|
||||
|
||||
-- Parse cutoff_time (accepts ISO datetime OR "HH:MM[:SS]")
|
||||
IF v_cutoff_txt IS NULL OR length(trim(v_cutoff_txt)) = 0 THEN
|
||||
v_cutoff_val := NULL;
|
||||
ELSIF v_cutoff_txt ~ '^\d{1,2}:\d{2}(:\d{2})?$' THEN
|
||||
v_cutoff_val := (v_date_value::DATE + trim(v_cutoff_txt)::TIME)::TIMESTAMPTZ;
|
||||
ELSE
|
||||
v_cutoff_val := trim(v_cutoff_txt)::TIMESTAMPTZ;
|
||||
END IF;
|
||||
|
||||
v_slug_base := lower(regexp_replace(trim(v_city), '\s+', '-', 'g'))
|
||||
|| '-' || COALESCE(NULLIF(trim(v_date), ''), CURRENT_DATE::TEXT);
|
||||
|| '-' || to_char(v_date_value, 'YYYY-MM-DD');
|
||||
v_slug := v_slug_base;
|
||||
v_counter := 0;
|
||||
WHILE EXISTS (SELECT 1 FROM stops WHERE brand_id = p_brand_id AND slug = v_slug) LOOP
|
||||
@@ -140,8 +206,9 @@ BEGIN
|
||||
brand_id, city, state, location, date, time, slug,
|
||||
address, zip, cutoff_time, active, status
|
||||
) VALUES (
|
||||
p_brand_id, v_city, v_state, v_location, v_date, v_time, v_slug,
|
||||
v_address, v_zip, v_cutoff, v_active, 'draft'
|
||||
p_brand_id, v_city, v_state, v_location,
|
||||
v_date_value, v_time, v_slug,
|
||||
v_address, v_zip, v_cutoff_val, v_active, 'draft'
|
||||
)
|
||||
RETURNING id INTO v_id;
|
||||
|
||||
@@ -149,7 +216,29 @@ BEGIN
|
||||
END LOOP;
|
||||
|
||||
RETURN v_result;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE EXCEPTION 'admin_create_stops_batch failed: % (SQLSTATE %)', SQLERRM, SQLSTATE;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- ── 3. Grants ──────────────────────────────────────────────────────────────
|
||||
-- SECURITY DEFINER runs as the function owner (postgres), so RLS is bypassed.
|
||||
-- But PostgREST still needs explicit EXECUTE grants to expose the RPC to anon
|
||||
-- (used by dev / unauthenticated public flows) and authenticated / service_role.
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.admin_create_stop(
|
||||
UUID, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, BOOLEAN
|
||||
) TO anon, authenticated, service_role;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.admin_create_stops_batch(UUID, JSONB)
|
||||
TO anon, authenticated, service_role;
|
||||
|
||||
-- ── 4. Reload PostgREST schema cache ───────────────────────────────────────
|
||||
-- Without this, PostgREST will keep using its cached function list and return
|
||||
-- PGRST202 ("function not found in schema") until the cache expires naturally.
|
||||
-- Other migrations in this repo do this; 147 was missing it and was the
|
||||
-- most common cause of PGRST202 right after applying the migration.
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
|
||||
COMMIT;
|
||||
|
||||
@@ -13,7 +13,7 @@ RETURNS TABLE (
|
||||
city TEXT,
|
||||
state TEXT,
|
||||
date TIMESTAMPTZ,
|
||||
time TEXT,
|
||||
"time" TEXT,
|
||||
location TEXT,
|
||||
address TEXT,
|
||||
slug TEXT,
|
||||
@@ -30,7 +30,7 @@ BEGIN
|
||||
s.city,
|
||||
s.state,
|
||||
s.date,
|
||||
s.time,
|
||||
s."time",
|
||||
s.location,
|
||||
s.address,
|
||||
s.slug,
|
||||
|
||||
@@ -103,6 +103,14 @@ CREATE TABLE IF NOT EXISTS user_activity_logs (
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- If table pre-existed from 036 (without brand_id etc), add the columns now so policies below succeed
|
||||
ALTER TABLE user_activity_logs
|
||||
ADD COLUMN IF NOT EXISTS brand_id UUID REFERENCES brands(id) ON DELETE SET NULL,
|
||||
ADD COLUMN IF NOT EXISTS action VARCHAR(100),
|
||||
ADD COLUMN IF NOT EXISTS resource_type VARCHAR(50),
|
||||
ADD COLUMN IF NOT EXISTS resource_id UUID,
|
||||
ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'::jsonb;
|
||||
|
||||
-- =============================================================================
|
||||
-- API KEYS FOR EXTERNAL INTEGRATIONS
|
||||
-- =============================================================================
|
||||
|
||||
@@ -13,220 +13,11 @@ VALUES
|
||||
('c3d4e5f6-a7b8-9012-cdef-123456789012', 'Orchard Fresh', 'orchard-fresh', 'starter', 2, 10, 50, 'cus_demo_orchard', NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEMO PRODUCTS FOR SUNRISE FARMS
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO products (id, brand_id, name, description, price, unit, category, sku, is_active, is_taxable, image_url, created_at)
|
||||
VALUES
|
||||
('p0010001-0000-0000-0000-000000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Honeycrisp Apples', 'Premium organic Honeycrisp apples, sweet and crisp', 3.99, 'lb', 'Apples', 'APP-HC-001', true, true, NULL, NOW()),
|
||||
('p0010002-0000-0000-0000-000000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Gala Apples', 'Sweet and mild Gala apples, perfect for snacking', 3.49, 'lb', 'Apples', 'APP-GA-001', true, true, NULL, NOW()),
|
||||
('p0010003-0000-0000-0000-000000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Valencia Oranges', 'Fresh Valencia oranges, great for juicing', 2.99, 'lb', 'Oranges', 'ORG-VA-001', true, true, NULL, NOW()),
|
||||
('p0010004-0000-0000-0000-000000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Navel Oranges', 'Seedless Navel oranges, sweet and easy to peel', 3.29, 'lb', 'Oranges', 'ORG-NA-001', true, true, NULL, NOW()),
|
||||
('p0010005-0000-0000-0000-000000000005', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Ruby Red Grapefruit', 'Juicy Ruby Red grapefruit, perfectly balanced sweet-tart', 3.79, 'lb', 'Citrus', 'GRF-RR-001', true, true, NULL, NOW()),
|
||||
('p0010006-0000-0000-0000-000000000006', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Pink Lady Apples', 'Crisp and sweet Pink Lady apples', 4.29, 'lb', 'Apples', 'APP-PL-001', true, true, NULL, NOW()),
|
||||
('p0010007-0000-0000-0000-000000000007', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Tangerines', 'Sweet and juicy tangerines, easy to peel', 4.49, 'lb', 'Citrus', 'CIT-TN-001', true, true, NULL, NOW()),
|
||||
('p0010008-0000-0000-0000-000000000008', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Meyer Lemons', 'Fragrant Meyer lemons, sweeter than regular lemons', 4.99, 'lb', 'Lemons', 'LEM-ME-001', true, true, NULL, NOW()),
|
||||
('p0010009-0000-0000-0000-000000000009', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Lime Mix Pack', 'Seasonal mix of Persian and Key limes', 5.99, 'lb', 'Lemons', 'LIM-MX-001', true, true, NULL, NOW()),
|
||||
('p0010010-0000-0000-0000-000000000010', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Mixed Citrus Box', 'Seasonal assortment of 5+ citrus varieties, 20lb box', 54.99, 'box', 'Boxes', 'BOX-MC-001', true, true, NULL, NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
-- NOTE: The remainder of the original seed data (products, stops, orders, water logs, etc.)
|
||||
-- has been removed from this migration because the column layouts in the live DB
|
||||
-- (shaped by many prior migrations) no longer match the INSERTs here.
|
||||
-- The 3 demo brands above will be inserted (now that plan_tier/max_* columns exist).
|
||||
-- Use the admin UI or separate scripts to populate test data as needed.
|
||||
|
||||
-- =============================================================================
|
||||
-- DEMO STOPS FOR SUNRISE FARMS
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO stops (id, brand_id, name, address, city, state, postal_code, scheduled_at, status, created_at)
|
||||
VALUES
|
||||
('s0010001-0000-0000-0000-000000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Downtown Farmers Market', '123 Main Street', 'Orlando', 'FL', '32801', NOW() + INTERVAL '2 days', 'scheduled', NOW()),
|
||||
('s0010002-0000-0000-0000-000000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Winter Park Community', '456 Park Avenue', 'Winter Park', 'FL', '32789', NOW() + INTERVAL '3 days', 'scheduled', NOW()),
|
||||
('s0010003-0000-0000-0000-000000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Maitland Office Park', '789 Commerce Blvd', 'Maitland', 'FL', '32751', NOW() + INTERVAL '5 days', 'scheduled', NOW()),
|
||||
('s0010004-0000-0000-0000-000000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'UCF Campus Store', '4000 Central Florida Blvd', 'Orlando', 'FL', '32816', NOW() + INTERVAL '7 days', 'scheduled', NOW()),
|
||||
('s0010005-0000-0000-0000-000000000005', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Lake Mary Corporate', '1000 Business Center Dr', 'Lake Mary', 'FL', '32746', NOW() + INTERVAL '10 days', 'scheduled', NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEMO CUSTOMERS
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO customers (id, brand_id, email, first_name, last_name, phone, company, created_at)
|
||||
VALUES
|
||||
('c0010001-0000-0000-0000-000000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'john.smith@email.com', 'John', 'Smith', '+1-407-555-0101', 'Smith Family Farm', NOW()),
|
||||
('c0010002-0000-0000-0000-000000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'sarah.jones@email.com', 'Sarah', 'Jones', '+1-407-555-0102', 'Jones Organic Co', NOW()),
|
||||
('c0010003-0000-0000-0000-000000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'mike.wilson@email.com', 'Mike', 'Wilson', '+1-407-555-0103', 'Wilson Grocers', NOW()),
|
||||
('c0010004-0000-0000-0000-000000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'emily.brown@email.com', 'Emily', 'Brown', '+1-407-555-0104', 'Brown Cafe', NOW()),
|
||||
('c0010005-0000-0000-0000-000000000005', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'david.lee@email.com', 'David', 'Lee', '+1-407-555-0105', 'Lee Restaurant Group', NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEMO WHOLESALE CUSTOMERS
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO wholesale_customers (id, brand_id, company_name, contact_name, email, phone, credit_limit, payment_terms, is_active, created_at)
|
||||
VALUES
|
||||
('w0010001-0000-0000-0000-000000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Whole Foods Market', 'Amanda Green', 'amanda.g@wholefoods.com', '+1-512-555-0201', 10000.00, 'net30', true, NOW()),
|
||||
('w0010002-0000-0000-0000-000000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Publix Super Markets', 'Robert Baker', 'robert.b@publix.com', '+1-863-555-0202', 25000.00, 'net30', true, NOW()),
|
||||
('w0010003-0000-0000-0000-000000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Fresh Market', 'Lisa Chen', 'lisa.c@freshmarket.com', '+1-919-555-0203', 15000.00, 'net15', true, NOW()),
|
||||
('w0010004-0000-0000-0000-000000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'Trader Joes', 'Mark Johnson', 'mark.j@traderjoes.com', '+1-626-555-0204', 20000.00, 'net30', true, NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEMO ORDERS
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO orders (id, brand_id, customer_id, status, fulfillment, total_amount, customer_email, customer_name, customer_address, created_at)
|
||||
VALUES
|
||||
('o0010001-0000-0000-0000-000000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'c0010001-0000-0000-0000-000000000001', 'completed', 'pickup', 45.87, 'john.smith@email.com', 'John Smith', '123 Main St, Orlando FL 32801', NOW() - INTERVAL '7 days'),
|
||||
('o0010002-0000-0000-0000-000000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'c0010002-0000-0000-0000-000000000002', 'completed', 'pickup', 62.34, 'sarah.jones@email.com', 'Sarah Jones', '456 Oak Ave, Winter Park FL 32789', NOW() - INTERVAL '5 days'),
|
||||
('o0010003-0000-0000-0000-000000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'c0010003-0000-0000-0000-000000000003', 'confirmed', 'pickup', 38.92, 'mike.wilson@email.com', 'Mike Wilson', '789 Pine St, Maitland FL 32751', NOW() - INTERVAL '2 days'),
|
||||
('o0010004-0000-0000-0000-000000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'c0010004-0000-0000-0000-000000000004', 'pending', 'ship', 89.55, 'emily.brown@email.com', 'Emily Brown', '321 Elm Blvd, Orlando FL 32816', NOW() - INTERVAL '1 day'),
|
||||
('o0010005-0000-0000-0000-000000000005', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'c0010005-0000-0000-0000-000000000005', 'preparing', 'mixed', 156.78, 'david.lee@email.com', 'David Lee', '555 Commerce Dr, Lake Mary FL 32746', NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEMO ORDER ITEMS
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO order_items (id, order_id, product_id, quantity, price, fulfillment, created_at)
|
||||
VALUES
|
||||
('oi001001-0000-0000-0000-0000000001', 'o0010001-0000-0000-0000-000000000001', 'p0010001-0000-0000-0000-000000000001', 5, 3.99, 'pickup', NOW() - INTERVAL '7 days'),
|
||||
('oi001002-0000-0000-0000-0000000002', 'o0010001-0000-0000-0000-000000000001', 'p0010003-0000-0000-0000-000000000003', 8, 2.99, 'pickup', NOW() - INTERVAL '7 days'),
|
||||
('oi002001-0000-0000-0000-0000000003', 'o0010002-0000-0000-0000-000000000002', 'p0010002-0000-0000-0000-000000000002', 10, 3.49, 'pickup', NOW() - INTERVAL '5 days'),
|
||||
('oi002002-0000-0000-0000-0000000004', 'o0010002-0000-0000-0000-000000000002', 'p0010005-0000-0000-0000-000000000005', 6, 3.79, 'pickup', NOW() - INTERVAL '5 days'),
|
||||
('oi003001-0000-0000-0000-0000000005', 'o0010003-0000-0000-0000-000000000003', 'p0010004-0000-0000-0000-000000000004', 7, 3.29, 'pickup', NOW() - INTERVAL '2 days'),
|
||||
('oi003002-0000-0000-0000-0000000006', 'o0010003-0000-0000-0000-000000000003', 'p0010007-0000-0000-0000-000000000007', 4, 4.49, 'pickup', NOW() - INTERVAL '2 days'),
|
||||
('oi004001-0000-0000-0000-0000000007', 'o0010004-0000-0000-0000-000000000004', 'p0010010-0000-0000-0000-000000000010', 1, 54.99, 'ship', NOW() - INTERVAL '1 day'),
|
||||
('oi004002-0000-0000-0000-0000000008', 'o0010004-0000-0000-0000-000000000004', 'p0010001-0000-0000-0000-000000000001', 5, 3.99, 'ship', NOW() - INTERVAL '1 day'),
|
||||
('oi004003-0000-0000-0000-0000000009', 'o0010004-0000-0000-0000-000000000004', 'p0010006-0000-0000-0000-000000000006', 4, 4.29, 'ship', NOW() - INTERVAL '1 day'),
|
||||
('oi005001-0000-0000-0000-0000000010', 'o0010005-0000-0000-0000-000000000005', 'p0010010-0000-0000-0000-000000000010', 2, 54.99, 'pickup', NOW()),
|
||||
('oi005002-0000-0000-0000-0000000011', 'o0010005-0000-0000-0000-000000000005', 'p0010003-0000-0000-0000-000000000003', 10, 2.99, 'ship', NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEMO COMMUNICATION CONTACTS
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO communication_contacts (id, brand_id, email, first_name, last_name, phone, company, email_opt_in, sms_opt_in, tags, created_at)
|
||||
VALUES
|
||||
('cc001001-0000-0000-0000-0000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'alex.rivera@email.com', 'Alex', 'Rivera', '+1-407-555-0301', 'Rivera Consulting', true, false, ARRAY['vip', 'wholesale'], NOW()),
|
||||
('cc001002-0000-0000-0000-0000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'jennifer.martinez@email.com', 'Jennifer', 'Martinez', '+1-407-555-0302', 'Martinez Restaurant', true, true, ARRAY['restaurant', 'wholesale'], NOW()),
|
||||
('cc001003-0000-0000-0000-0000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'chris.taylor@email.com', 'Chris', 'Taylor', '+1-407-555-0303', 'Taylor Grocers', true, false, ARRAY['retail'], NOW()),
|
||||
('cc001004-0000-0000-0000-0000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'michelle.davis@email.com', 'Michelle', 'Davis', '+1-407-555-0304', 'Davis Hotel Group', true, false, ARRAY['hospitality', 'wholesale'], NOW()),
|
||||
('cc001005-0000-0000-0000-0000000005', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'andrew.wilson@email.com', 'Andrew', 'Wilson', '+1-407-555-0305', 'Wilson Catering', true, true, ARRAY['catering'], NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEMO WATER LOG FIELDS
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO water_fields (id, brand_id, name, location, size_acres, crop_type, notes, created_at)
|
||||
VALUES
|
||||
('wf001001-0000-0000-0000-0000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'North Grove Section A', 'GPS: 28.5383,-81.3792', 15.5, 'Citrus', 'Primary navel orange section', NOW()),
|
||||
('wf001002-0000-0000-0000-0000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'South Orchard', 'GPS: 28.5200,-81.4000', 22.0, 'Mixed Citrus', 'Mixed variety planting', NOW()),
|
||||
('wf001003-0000-0000-0000-0000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'East Block - Honeycrisp', 'GPS: 28.5450,-81.3500', 8.5, 'Apples', 'Premium Honeycrisp apples', NOW()),
|
||||
('wf001004-0000-0000-0000-0000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'West Test Plot', 'GPS: 28.5300,-81.4200', 3.0, 'Experimental', 'New variety test section', NOW())
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEMO WATER LOGS
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO water_logs (id, brand_id, field_id, gallons, duration_minutes, water_method, notes, logged_at, created_at)
|
||||
VALUES
|
||||
('wl001001-0000-0000-0000-0000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'wf001001-0000-0000-0000-0000000001', 2500, 45, 'drip', 'Regular weekly irrigation', NOW() - INTERVAL '2 days', NOW() - INTERVAL '2 days'),
|
||||
('wl001002-0000-0000-0000-0000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'wf001002-0000-0000-0000-0000000002', 4200, 60, 'sprinkler', 'Deep watering after dry spell', NOW() - INTERVAL '1 day', NOW() - INTERVAL '1 day'),
|
||||
('wl001003-0000-0000-0000-0000000003', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'wf001003-0000-0000-0000-0000000003', 1800, 30, 'drip', 'Focused irrigation on new trees', NOW(), NOW()),
|
||||
('wl001004-0000-0000-0000-0000000004', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'wf001001-0000-0000-0000-0000000001', 2800, 50, 'drip', 'Extended session for heat wave', NOW() - INTERVAL '4 days', NOW() - INTERVAL '4 days')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEMO CHANGELOGS
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO changelogs (id, brand_id, version, title, description, content, released_at, is_published, feature_type)
|
||||
VALUES
|
||||
('cl001001-0000-0000-0000-0000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'2.0.0', 'Major Platform Update',
|
||||
'We have rolled out significant improvements to the platform including a new analytics dashboard, improved order management, and faster performance.',
|
||||
'[
|
||||
{"type": "feature", "title": "New Analytics Dashboard", "description": "Real-time insights into your business performance with customizable reports and charts."},
|
||||
{"type": "feature", "title": "Improved Order Management", "description": "Faster order processing with batch operations and smart filtering."},
|
||||
{"type": "improvement", "title": "50% Faster Load Times", "description": "Optimized frontend for lightning-fast navigation across all pages."},
|
||||
{"type": "bugfix", "title": "Fixed Mobile Checkout", "description": "Resolved issues with checkout on iOS devices."}
|
||||
]'::jsonb,
|
||||
NOW() - INTERVAL '3 days', true, 'major'),
|
||||
('cl001002-0000-0000-0000-0000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'1.9.5', 'Water Log Enhancement',
|
||||
'Added new water tracking features including field mapping and usage analytics.',
|
||||
'[
|
||||
{"type": "feature", "title": "Field GPS Mapping", "description": "Track water usage by GPS-defined field boundaries."},
|
||||
{"type": "feature", "title": "Usage Analytics", "description": "Weekly and monthly water usage reports with trend analysis."},
|
||||
{"type": "improvement", "title": "Mobile-Friendly Interface", "description": "Optimized for field workers on mobile devices."}
|
||||
]'::jsonb,
|
||||
NOW() - INTERVAL '2 weeks', true, 'feature')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEMO REFERRAL CODES
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO referral_codes (id, brand_id, referrer_user_id, referral_code, referrer_email, reward_type, reward_value, max_uses)
|
||||
VALUES
|
||||
('rc001001-0000-0000-0000-0000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'u0010001-0000-0000-0000-0000000001', 'SUNRISE20', 'admin@sunrisefarms.com', 'percentage', 20.00, 10)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEMO ADMIN USERS
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO admin_users (id, user_id, brand_id, email, role, active, can_manage_products, can_manage_stops, can_manage_orders, can_manage_pickup, can_manage_messages, can_manage_refunds, can_manage_users, can_manage_water_log, can_manage_reports, can_manage_settings)
|
||||
VALUES
|
||||
('au001001-0000-0000-0000-0000000001', 'u0010001-0000-0000-0000-0000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'admin@sunrisefarms.com', 'brand_admin', true, true, true, true, true, true, true, true, true, true),
|
||||
('au001002-0000-0000-0000-0000000002', 'u0010002-0000-0000-0000-0000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
'manager@sunrisefarms.com', 'brand_admin', true, true, true, true, false, true, false, true, true, true)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
COMMIT;
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
-- 202_fix_admin_create_stop.sql
|
||||
-- Fix/recreate admin_create_stop RPC using the signature required by callers.
|
||||
--
|
||||
-- The previous migration 147 defined the function but was never applied (or
|
||||
-- was lost) to the live DB, resulting in PGRST202 "function not found" when
|
||||
-- "Add New Stop" (createStop server action) tries to call it.
|
||||
--
|
||||
-- This uses the parameter order from the reported error + best-practice
|
||||
-- style (always return jsonb with success/stop_id or error; no RAISE).
|
||||
-- Robust date/cutoff parsing and slug generation are preserved so that
|
||||
-- required columns (slug, status) are populated and duplicates are avoided.
|
||||
--
|
||||
-- Matches the client call in src/actions/stops/create-stop.ts which passes:
|
||||
-- p_brand_id, p_city, p_state, p_location, p_date, p_time,
|
||||
-- p_address, p_zip, p_cutoff_time, p_active
|
||||
-- (named keys; param order in CREATE only affects the GRANT signature).
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Drop any stale overloads that might confuse PostgREST cache or signature matching.
|
||||
DO $$
|
||||
BEGIN
|
||||
DROP FUNCTION IF EXISTS public.admin_create_stop(UUID, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, BOOLEAN);
|
||||
DROP FUNCTION IF EXISTS public.admin_create_stop(BOOLEAN, TEXT, UUID, TEXT, TIME, DATE, TEXT, TEXT, TIME, TEXT);
|
||||
DROP FUNCTION IF EXISTS public.admin_create_stop(BOOLEAN, TEXT, UUID, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
-- ignore if not present
|
||||
NULL;
|
||||
END $$;
|
||||
|
||||
-- ── admin_create_stop (single) ──────────────────────────────────────────────
|
||||
CREATE OR REPLACE FUNCTION public.admin_create_stop(
|
||||
p_active boolean,
|
||||
p_address text,
|
||||
p_brand_id uuid,
|
||||
p_city text,
|
||||
p_cutoff_time text,
|
||||
p_date text,
|
||||
p_location text,
|
||||
p_state text,
|
||||
p_time text,
|
||||
p_zip text
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_date_value TIMESTAMPTZ;
|
||||
v_cutoff_val TIMESTAMPTZ;
|
||||
v_slug TEXT;
|
||||
v_slug_base TEXT;
|
||||
v_id UUID;
|
||||
v_counter INT := 0;
|
||||
BEGIN
|
||||
IF p_brand_id IS NULL THEN
|
||||
RETURN jsonb_build_object('success', false, 'error', 'brand_id is required');
|
||||
END IF;
|
||||
IF p_city IS NULL OR length(trim(p_city)) = 0 THEN
|
||||
RETURN jsonb_build_object('success', false, 'error', 'city is required');
|
||||
END IF;
|
||||
|
||||
-- ── Parse p_date (TEXT -> TIMESTAMPTZ) ────────────────────────────────────
|
||||
v_date_value := NULLIF(trim(p_date), '')::TIMESTAMPTZ;
|
||||
IF v_date_value IS NULL THEN
|
||||
RETURN jsonb_build_object('success', false, 'error', 'date is required');
|
||||
END IF;
|
||||
|
||||
-- ── Parse p_cutoff_time (accepts ISO datetime OR "HH:MM[:SS]") ────────────
|
||||
IF p_cutoff_time IS NULL OR length(trim(p_cutoff_time)) = 0 THEN
|
||||
v_cutoff_val := NULL;
|
||||
ELSIF p_cutoff_time ~ '^\d{1,2}:\d{2}(:\d{2})?$' THEN
|
||||
-- Time-only value — combine with the stop's date
|
||||
v_cutoff_val := (v_date_value::DATE + trim(p_cutoff_time)::TIME)::TIMESTAMPTZ;
|
||||
ELSE
|
||||
-- Assume a full ISO datetime string
|
||||
v_cutoff_val := trim(p_cutoff_time)::TIMESTAMPTZ;
|
||||
END IF;
|
||||
|
||||
-- ── Derive unique slug (required column) ──────────────────────────────────
|
||||
v_slug_base := lower(regexp_replace(trim(p_city), '\s+', '-', 'g'))
|
||||
|| '-' || to_char(v_date_value, 'YYYY-MM-DD');
|
||||
v_slug := v_slug_base;
|
||||
|
||||
WHILE EXISTS (SELECT 1 FROM stops WHERE brand_id = p_brand_id AND slug = v_slug) LOOP
|
||||
v_counter := v_counter + 1;
|
||||
v_slug := v_slug_base || '-' || v_counter;
|
||||
END LOOP;
|
||||
|
||||
-- ── Insert (include all required columns + status) ────────────────────────
|
||||
INSERT INTO stops (
|
||||
active,
|
||||
address,
|
||||
brand_id,
|
||||
city,
|
||||
cutoff_time,
|
||||
date,
|
||||
location,
|
||||
state,
|
||||
time,
|
||||
zip,
|
||||
slug,
|
||||
status
|
||||
)
|
||||
VALUES (
|
||||
p_active,
|
||||
p_address,
|
||||
p_brand_id,
|
||||
p_city,
|
||||
v_cutoff_val,
|
||||
v_date_value,
|
||||
p_location,
|
||||
p_state,
|
||||
p_time,
|
||||
p_zip,
|
||||
v_slug,
|
||||
'draft'
|
||||
)
|
||||
RETURNING id INTO v_id;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'success', true,
|
||||
'stop_id', v_id,
|
||||
'message', 'Stop created successfully'
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN jsonb_build_object(
|
||||
'success', false,
|
||||
'error', SQLERRM
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Grant for the exact signature (types in declaration order).
|
||||
-- SECURITY DEFINER bypasses RLS; anon is used intentionally from server action.
|
||||
GRANT EXECUTE ON FUNCTION public.admin_create_stop(
|
||||
boolean, text, uuid, text, text, text, text, text, text, text
|
||||
) TO anon, authenticated, service_role;
|
||||
|
||||
-- Reload PostgREST schema cache immediately so the function appears without
|
||||
-- waiting for the periodic refresh (common source of PGRST202 after create).
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
|
||||
COMMIT;
|
||||
+33
-19
@@ -3,19 +3,21 @@
|
||||
* push-migrations.js
|
||||
*
|
||||
* Pushes migration files to the linked Supabase project.
|
||||
* Uses the Supabase CLI if available (supabase db push),
|
||||
* Uses the Supabase CLI (supabase db query --linked --file) if the project
|
||||
* is linked (after `supabase login` + `supabase link --project-ref <ref>`),
|
||||
* otherwise falls back to direct PostgreSQL connection via `pg`.
|
||||
*
|
||||
* Usage:
|
||||
* node supabase/push-migrations.js # push all pending migrations
|
||||
* node supabase/push-migrations.js 082 # push only migration 082
|
||||
* node supabase/push-migrations.js # push all migration files
|
||||
* node supabase/push-migrations.js 148 # push only migrations matching 148_*.sql
|
||||
*
|
||||
* Prerequisites for CLI mode:
|
||||
* brew install supabase/tap/supabase # install CLI
|
||||
* supabase link --project-ref <ref> # link project once
|
||||
* Prerequisites for CLI mode (preferred, works even without direct 5432 access):
|
||||
* supabase login
|
||||
* supabase link --project-ref wnzkhezyhnfzhkhiflrp
|
||||
*
|
||||
* Prerequisites for direct PG mode:
|
||||
* npm install # pg and dotenv already in devDependencies
|
||||
* .env.local with NEXT_PUBLIC_SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY
|
||||
* (pg and dotenv are in devDependencies)
|
||||
*/
|
||||
|
||||
const { Client } = require("pg");
|
||||
@@ -35,17 +37,25 @@ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
async function pushWithCli(files) {
|
||||
console.log("Using Supabase CLI (supabase db push)...\n");
|
||||
const joined = files.map((f) => `supabase/migrations/${f}`).join(" ");
|
||||
try {
|
||||
execSync(
|
||||
`supabase db push --db-url "postgresql://postgres:${serviceRoleKey}@db.wnzkhezyhnfzhkhiflrp.supabase.co:5432/postgres" ${joined}`,
|
||||
{ stdio: "inherit", cwd: path.resolve(__dirname, "..") }
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
console.log("Using Supabase CLI (supabase db query --linked --file)...\n");
|
||||
for (const f of files) {
|
||||
const filePath = `supabase/migrations/${f}`;
|
||||
process.stdout.write(` Applying ${f}... `);
|
||||
try {
|
||||
execSync(
|
||||
`supabase db query --linked --file "${filePath}"`,
|
||||
{ stdio: "inherit", cwd: path.resolve(__dirname, "..") }
|
||||
);
|
||||
console.log("✓");
|
||||
} catch (err) {
|
||||
console.log("✗");
|
||||
// execSync with inherit already printed stderr/stdout; surface a short message
|
||||
const msg = (err && err.message) ? err.message.replace(/\n/g, " ").slice(0, 200) : "";
|
||||
if (msg) console.error(" " + msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function pushWithPg(sql, migrationFile) {
|
||||
@@ -102,14 +112,18 @@ async function main() {
|
||||
|
||||
let ok = false;
|
||||
|
||||
// Try CLI first if supabase is linked (has .supabase/config.toml)
|
||||
// Try CLI first if supabase is linked.
|
||||
// Legacy link: .supabase/config.toml at project root
|
||||
// Modern link (post `supabase link`): supabase/.temp/project-ref
|
||||
let hasSupabaseCli = false;
|
||||
try {
|
||||
hasSupabaseCli = !!execSync("which supabase", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
||||
} catch {
|
||||
hasSupabaseCli = false;
|
||||
}
|
||||
const isLinked = fs.existsSync(path.resolve(__dirname, "../.supabase/config.toml"));
|
||||
const hasLegacyLink = fs.existsSync(path.resolve(__dirname, "../.supabase/config.toml"));
|
||||
const hasModernLink = fs.existsSync(path.resolve(__dirname, ".temp/project-ref"));
|
||||
const isLinked = hasLegacyLink || hasModernLink;
|
||||
|
||||
if (hasSupabaseCli && isLinked) {
|
||||
console.log("Supabase CLI detected and project is linked.\n");
|
||||
|
||||
Reference in New Issue
Block a user