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:
@@ -26,6 +26,8 @@ npx playwright test # Run E2E tests (Playwright)
|
|||||||
> For direct PG mode: `pg` and `dotenv` are already in devDependencies.
|
> For direct PG mode: `pg` and `dotenv` are already in devDependencies.
|
||||||
> If `get_brand_settings` migration fails with "cannot change return type", the function signature changed — drop and recreate it first.
|
> If `get_brand_settings` migration fails with "cannot change return type", the function signature changed — drop and recreate it first.
|
||||||
|
|
||||||
|
**Recent migration work is documented in `MEMORY.md`** (Supabase login + link process, updates to `push-migrations.js` for modern CLI, specific SQL patches made to 091/145/148/200/201 so they would apply cleanly, and which migrations were pushed in the session). Cat `MEMORY.md` for details.
|
||||||
|
|
||||||
No test suite currently exists. E2E tests use Playwright (`tests/` or `test-e2e.ts`).
|
No test suite currently exists. E2E tests use Playwright (`tests/` or `test-e2e.ts`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
# MEMORY.md — Persistent Session Context
|
||||||
|
|
||||||
|
This file captures key context, decisions, fixes, and state from recent work so it survives across conversations.
|
||||||
|
|
||||||
|
**Last updated:** 2026-06-03 (during Supabase migration apply session)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supabase CLI + Migrations Tooling
|
||||||
|
|
||||||
|
### Login + Link (done in this session)
|
||||||
|
- User ran `supabase login`
|
||||||
|
- We ran: `supabase link --project-ref wnzkhezyhnfzhkhiflrp`
|
||||||
|
- Link succeeded. Project appears as **LINKED** (●) in `supabase projects list`.
|
||||||
|
- Link state lives under `supabase/.temp/` (project-ref, linked-project.json, pooler-url, etc.). **Not** the legacy `.supabase/config.toml` at project root.
|
||||||
|
- This enables `supabase db query --linked`, `supabase migration list`, etc.
|
||||||
|
|
||||||
|
### Important Environment Note
|
||||||
|
- Direct Postgres connections (`db.wnzkhezyhnfzhkhiflrp.supabase.co:5432` using service role key as password) **do not work** from this workspace:
|
||||||
|
- `getaddrinfo ENOTFOUND`
|
||||||
|
- IPv6 "network is unreachable" in some cases
|
||||||
|
- HTTPS / Supabase REST / Management API work fine.
|
||||||
|
- Therefore the `push-migrations.js` **CLI path** (using linked project) is the only reliable way to apply migrations here.
|
||||||
|
|
||||||
|
### Updated Migration Script
|
||||||
|
File: `supabase/push-migrations.js`
|
||||||
|
|
||||||
|
Key changes:
|
||||||
|
- Detection logic now recognizes modern link: `supabase/.temp/project-ref` (in addition to old `.supabase/config.toml`).
|
||||||
|
- `pushWithCli()` rewritten to apply files one-by-one using:
|
||||||
|
```bash
|
||||||
|
supabase db query --linked --file "supabase/migrations/NNN_foo.sql"
|
||||||
|
```
|
||||||
|
(Previously tried `db push --db-url ...` which used direct connection and failed here.)
|
||||||
|
- Falls back to direct `pg` only if CLI path fails.
|
||||||
|
- Header comments updated with current recommended workflow.
|
||||||
|
|
||||||
|
**Recommended commands now:**
|
||||||
|
```bash
|
||||||
|
supabase login
|
||||||
|
supabase link --project-ref wnzkhezyhnfzhkhiflrp
|
||||||
|
node supabase/push-migrations.js 148 # or any prefix
|
||||||
|
# or
|
||||||
|
npm run migrate:one 148
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm run migrate` (no arg) will push every `*.sql` in order (use with caution).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Files Patched in This Session
|
||||||
|
|
||||||
|
These changes were required to make the files actually apply successfully against the live remote schema (column drift, syntax errors, non-idempotent statements, pre-existing tables).
|
||||||
|
|
||||||
|
### 091_brand_plan_tier.sql
|
||||||
|
- Fixed syntax error: extra `)` in `get_brand_plan_info`:
|
||||||
|
```sql
|
||||||
|
... date >= date_trunc('month', now())); -- was broken
|
||||||
|
```
|
||||||
|
→ `now());`
|
||||||
|
|
||||||
|
### 145_create_product_images_bucket.sql
|
||||||
|
- Column name: `allowedMimeTypes` → `allowed_mime_types` (current Supabase storage.buckets schema uses snake_case).
|
||||||
|
- Made idempotent:
|
||||||
|
- Bucket insert now uses `WHERE NOT EXISTS (...)` (handles both id and name unique constraints).
|
||||||
|
- Added `DROP POLICY IF EXISTS ...` before each `CREATE POLICY` (so re-runs don't fail).
|
||||||
|
|
||||||
|
Policies created:
|
||||||
|
- "Public can view product-images"
|
||||||
|
- "Admins can upload product-images"
|
||||||
|
|
||||||
|
### 148_public_stops_rpc.sql
|
||||||
|
- `time` is a reserved word in Postgres.
|
||||||
|
- Quoted the output column and the source reference:
|
||||||
|
```sql
|
||||||
|
RETURNS TABLE ( ..., "time" TEXT, ... )
|
||||||
|
...
|
||||||
|
s."time",
|
||||||
|
```
|
||||||
|
- GRANTs to anon/authenticated/service_role added at bottom.
|
||||||
|
|
||||||
|
### 200_production_features.sql
|
||||||
|
- `user_activity_logs` table originated in `036_user_activity_logs.sql` (no `brand_id`, different column names: `activity_type` + `details`).
|
||||||
|
- 200's `CREATE TABLE IF NOT EXISTS` was a no-op.
|
||||||
|
- Its policies + indexes assumed `brand_id`, `action`, `resource_type`, `resource_id`, `metadata`.
|
||||||
|
- Added after the CREATE TABLE block:
|
||||||
|
```sql
|
||||||
|
ALTER TABLE user_activity_logs
|
||||||
|
ADD COLUMN IF NOT EXISTS brand_id UUID REFERENCES brands(id) ON DELETE SET NULL,
|
||||||
|
... (other columns) ...
|
||||||
|
```
|
||||||
|
- This unblocked policy creation and indexes.
|
||||||
|
|
||||||
|
Also added many new tables (referral_*, changelogs, onboarding_progress, api_keys, notification_preferences) + RLS policies + indexes.
|
||||||
|
|
||||||
|
### 201_seed_data.sql
|
||||||
|
- Heavily truncated.
|
||||||
|
- Original contained large demo INSERTs for products, stops, orders, order_items, wholesale_customers, communication_contacts, water_*, admin_users, etc.
|
||||||
|
- These used outdated column lists vs. the live DB (e.g. products: `unit`/`category`/`sku`/`is_active` vs current `type`/`active`/`pickup_type`/`is_taxable`; stops used `name`/`scheduled_at`/`postal_code` vs `location`/`date`+`time`/`zip`/`slug`).
|
||||||
|
- Kept only the 3 demo brands INSERT (now works because 091 added `plan_tier` + limit columns).
|
||||||
|
- Added explanatory comment.
|
||||||
|
- File now ends cleanly after `COMMIT;`.
|
||||||
|
|
||||||
|
(The demo brands with enterprise/farm/starter + plan limits were successfully inserted.)
|
||||||
|
|
||||||
|
### Other notes on 147 / 202
|
||||||
|
- `147_admin_create_stop_rpcs.sql` and `202_fix_admin_create_stop.sql` were also present/modified in the working tree (related to admin stop creation fixes mentioned in CLAUDE.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Successfully Applied (this session, via updated script)
|
||||||
|
|
||||||
|
Batches included (not exhaustive):
|
||||||
|
- 084 (shipments)
|
||||||
|
- 091 (plan tier + get_brand_plan_info)
|
||||||
|
- 142 (integration_credentials / resend + twilio RPCs)
|
||||||
|
- 143 (enable_route_trace via set_brand_feature)
|
||||||
|
- 144 (time_tracking_worker_number_rls)
|
||||||
|
- 145 (product-images bucket + policies)
|
||||||
|
- 146 (sitemap stops RPC)
|
||||||
|
- 147 (admin create stop RPCs)
|
||||||
|
- 148 (public stops RPC) — verified working
|
||||||
|
- 200 (production features)
|
||||||
|
- 201 (brands seed only)
|
||||||
|
- 202 (admin create stop fix)
|
||||||
|
|
||||||
|
Verification queries (post-apply) confirmed:
|
||||||
|
- shipments table exists
|
||||||
|
- get_resend_credentials / get_public_stops_for_brand / get_active_stops_with_brand functions exist
|
||||||
|
- product-images bucket + its two policies exist
|
||||||
|
- demo brand (sunrise-farms) has plan_tier = 'enterprise'
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current State / Gotchas
|
||||||
|
|
||||||
|
- `supabase migration list` will show a lot of "pending" because the project's custom numeric migrations (00x_*.sql) were historically applied via the raw-SQL push script, not registered in Supabase's `schema_migrations` tracking table. Only early ones (001/002) + many timestamped migrations from other activity are tracked.
|
||||||
|
- Many migrations are intentionally written to be re-runnable. Re-pushing a prefix is the supported workflow for this project.
|
||||||
|
- When adding **new** migrations in the future, prefer the standard `supabase migration new` flow if possible, but the custom `push-migrations.js` + numeric prefix style is still the established pattern here.
|
||||||
|
- Storage policies, RLS, SECURITY DEFINER functions, and brand-scoped data are all over the place — test carefully after big applies.
|
||||||
|
- CLAUDE.md already documents the overall migrate story and the `get_brand_settings` gotcha.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Use This Memory
|
||||||
|
|
||||||
|
- Cat this file at the start of future sessions if context is needed: `cat MEMORY.md`
|
||||||
|
- Update this file with new key facts, applied migrations, or new gotchas.
|
||||||
|
- Feel free to add dated sections.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unrelated / Other Changes in Tree (as of last git status)
|
||||||
|
|
||||||
|
(See `git status --short` and `git diff` for full picture. Includes stop-related action + RPC fixes, various debug scripts in `scripts/`, `supabase/ADMIN_CREATE_STOP_FIX.sql`, a pricing assessment doc, etc.)
|
||||||
|
|
||||||
|
This MEMORY.md focuses on the Supabase login / link / migration tooling + SQL repair work from the immediate request.
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { revalidateTag } from "next/cache";
|
||||||
import { getAdminUser } from "@/lib/admin-permissions";
|
import { getAdminUser } from "@/lib/admin-permissions";
|
||||||
import { svcHeaders } from "@/lib/svc-headers";
|
import { svcHeaders } from "@/lib/svc-headers";
|
||||||
import { getMockTableData } from "@/lib/mock-data";
|
import { getMockTableData } from "@/lib/mock-data";
|
||||||
@@ -38,33 +39,86 @@ export async function createStop(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||||
// Use anon key — the RPC is SECURITY DEFINER so it bypasses RLS regardless
|
// Preferred path: anon key + SECURITY DEFINER RPC (bypasses RLS, works even if
|
||||||
// of caller. This also means a missing SUPABASE_SERVICE_ROLE_KEY in
|
// service role key is absent at runtime). See:
|
||||||
// production no longer breaks stop creation.
|
// supabase/migrations/202_fix_admin_create_stop.sql
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
// scripts/apply-admin-create-stop.js (run this locally with the keys you have)
|
||||||
|
// supabase/ADMIN_CREATE_STOP_FIX.sql (exact prompt SQL for SQL editor)
|
||||||
|
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||||
|
|
||||||
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/admin_create_stop`, {
|
const res = await fetch(`${supabaseUrl}/rest/v1/rpc/admin_create_stop`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
headers: { ...svcHeaders(anonKey), "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
p_active: data.active ?? false,
|
||||||
|
p_address: data.address || null,
|
||||||
p_brand_id: brandId,
|
p_brand_id: brandId,
|
||||||
p_city: data.city,
|
p_city: data.city,
|
||||||
p_state: data.state,
|
p_cutoff_time: data.cutoff_time || null,
|
||||||
p_location: data.location,
|
|
||||||
p_date: data.date,
|
p_date: data.date,
|
||||||
|
p_location: data.location,
|
||||||
|
p_state: data.state,
|
||||||
p_time: data.time,
|
p_time: data.time,
|
||||||
p_address: data.address ?? null,
|
p_zip: data.zip || null,
|
||||||
p_zip: data.zip ?? null,
|
|
||||||
p_cutoff_time: data.cutoff_time ?? null,
|
|
||||||
p_active: data.active ?? false,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let usedFallback = false;
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.text();
|
const errText = await res.text();
|
||||||
return { success: false, error: `Failed: ${err}` };
|
const lower = errText.toLowerCase();
|
||||||
|
const looksLikeMissingFn =
|
||||||
|
lower.includes("pgrst202") ||
|
||||||
|
lower.includes("admin_create_stop") ||
|
||||||
|
lower.includes("could not find the function") ||
|
||||||
|
lower.includes("function not found");
|
||||||
|
|
||||||
|
if (looksLikeMissingFn) {
|
||||||
|
usedFallback = true;
|
||||||
|
} else {
|
||||||
|
return { success: false, error: `Failed: ${errText}` };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const inserted = await res.json().catch(() => ({} as any));
|
||||||
|
if (inserted && inserted.success === false) {
|
||||||
|
// Our RPC returns structured errors as 200 + {success:false}
|
||||||
|
const errMsg = inserted.error || "Failed to create stop";
|
||||||
|
const lower = errMsg.toLowerCase();
|
||||||
|
if (lower.includes("function") || lower.includes("not found") || lower.includes("pgrst")) {
|
||||||
|
usedFallback = true;
|
||||||
|
} else {
|
||||||
|
return { success: false, error: errMsg };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Happy path from RPC (either old {id,slug} or new {success, stop_id})
|
||||||
|
const stopId = inserted?.stop_id || inserted?.id || "";
|
||||||
|
const effectiveBrandId = brandId || adminUser.brand_id;
|
||||||
|
if (effectiveBrandId) {
|
||||||
|
revalidateTag("stops", "default");
|
||||||
|
revalidateTag(`brand:${effectiveBrandId}:stops`, "default");
|
||||||
|
}
|
||||||
|
return { success: true, id: stopId };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inserted = await res.json();
|
if (usedFallback) {
|
||||||
return { success: true, id: inserted?.id ?? "" };
|
// We cannot bypass the block_stops_mutations RLS policy via REST even with the service key.
|
||||||
|
// The only reliable path is the SECURITY DEFINER RPC created by migration 202.
|
||||||
|
// Tell the user exactly how to install it using only the keys they already have.
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
"The admin_create_stop database function is missing (this is why Add New Stop fails with function not found).\n\n" +
|
||||||
|
"Fix using only the keys in your .env.local (no Supabase dashboard needed):\n" +
|
||||||
|
" 1. On a machine that can reach your database (normal laptop usually works):\n" +
|
||||||
|
" node scripts/apply-admin-create-stop.js\n" +
|
||||||
|
" 2. Or: npm run migrate:one 202\n" +
|
||||||
|
" 3. Or apply supabase/migrations/202_fix_admin_create_stop.sql via psql / Supabase CLI with --db-url.\n\n" +
|
||||||
|
"After it succeeds, restart your dev server and Add New Stop will work.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not reach here
|
||||||
|
return { success: false, error: "Unexpected state creating stop" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
-- 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_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 COUNT(*) INTO v_product_count FROM products WHERE brand_id = p_brand_id AND deleted_at IS NULL;
|
||||||
|
|
||||||
SELECT jsonb_build_object(
|
SELECT jsonb_build_object(
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
-- Create product-images bucket for product images
|
-- Create product-images bucket for product images (idempotent)
|
||||||
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowedMimeTypes)
|
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||||
VALUES ('product-images', 'product-images', true, 5242880, ARRAY['image/png', 'image/jpeg', 'image/webp'])
|
SELECT 'product-images', 'product-images', true, 5242880, ARRAY['image/png', 'image/jpeg', 'image/webp']
|
||||||
ON CONFLICT (id) DO NOTHING;
|
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"
|
CREATE POLICY "Public can view product-images"
|
||||||
ON storage.objects FOR SELECT
|
ON storage.objects FOR SELECT
|
||||||
USING (bucket_id = 'product-images');
|
USING (bucket_id = 'product-images');
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Admins can upload product-images" ON storage.objects;
|
||||||
CREATE POLICY "Admins can upload product-images"
|
CREATE POLICY "Admins can upload product-images"
|
||||||
ON storage.objects FOR INSERT
|
ON storage.objects FOR INSERT
|
||||||
WITH CHECK (bucket_id = 'product-images');
|
WITH CHECK (bucket_id = 'product-images');
|
||||||
@@ -16,14 +16,36 @@
|
|||||||
-- p_brand_id for explicit brand scoping. The application layer
|
-- p_brand_id for explicit brand scoping. The application layer
|
||||||
-- (server actions) still validates the caller has can_manage_stops
|
-- (server actions) still validates the caller has can_manage_stops
|
||||||
-- via getAdminUser() before invoking the RPC.
|
-- 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;
|
BEGIN;
|
||||||
|
|
||||||
-- ── 1. admin_create_stop ───────────────────────────────────────────────────
|
-- ── 1. admin_create_stop ───────────────────────────────────────────────────
|
||||||
-- Inserts a single stop. Slug is derived from city + date; if a stop with
|
-- 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.
|
-- 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_brand_id UUID,
|
||||||
p_city TEXT,
|
p_city TEXT,
|
||||||
p_state TEXT,
|
p_state TEXT,
|
||||||
@@ -37,13 +59,16 @@ CREATE OR REPLACE FUNCTION admin_create_stop(
|
|||||||
)
|
)
|
||||||
RETURNS JSONB
|
RETURNS JSONB
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
SECURITY DEFINER SET search_path = public
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
AS $$
|
AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
v_slug TEXT;
|
v_date_value TIMESTAMPTZ;
|
||||||
v_slug_base TEXT;
|
v_cutoff_val TIMESTAMPTZ;
|
||||||
v_id UUID;
|
v_slug TEXT;
|
||||||
v_counter INT := 0;
|
v_slug_base TEXT;
|
||||||
|
v_id UUID;
|
||||||
|
v_counter INT := 0;
|
||||||
BEGIN
|
BEGIN
|
||||||
IF p_brand_id IS NULL THEN
|
IF p_brand_id IS NULL THEN
|
||||||
RAISE EXCEPTION 'brand_id is required';
|
RAISE EXCEPTION 'brand_id is required';
|
||||||
@@ -52,57 +77,83 @@ BEGIN
|
|||||||
RAISE EXCEPTION 'city is required';
|
RAISE EXCEPTION 'city is required';
|
||||||
END IF;
|
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'))
|
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;
|
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
|
WHILE EXISTS (SELECT 1 FROM stops WHERE brand_id = p_brand_id AND slug = v_slug) LOOP
|
||||||
v_counter := v_counter + 1;
|
v_counter := v_counter + 1;
|
||||||
v_slug := v_slug_base || '-' || v_counter;
|
v_slug := v_slug_base || '-' || v_counter;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
|
|
||||||
|
-- ── Insert ───────────────────────────────────────────────────────────────
|
||||||
INSERT INTO stops (
|
INSERT INTO stops (
|
||||||
brand_id, city, state, location, date, time, slug,
|
brand_id, city, state, location, date, time, slug,
|
||||||
address, zip, cutoff_time, active, status
|
address, zip, cutoff_time, active, status
|
||||||
) VALUES (
|
) VALUES (
|
||||||
p_brand_id, p_city, p_state, p_location, p_date, p_time, v_slug,
|
p_brand_id, p_city, p_state, p_location,
|
||||||
p_address, p_zip, p_cutoff_time, p_active, 'draft'
|
v_date_value, p_time, v_slug,
|
||||||
|
p_address, p_zip, v_cutoff_val, p_active, 'draft'
|
||||||
)
|
)
|
||||||
RETURNING id INTO v_id;
|
RETURNING id INTO v_id;
|
||||||
|
|
||||||
RETURN jsonb_build_object('id', v_id, 'slug', v_slug);
|
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;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
-- ── 2. admin_create_stops_batch ────────────────────────────────────────────
|
-- ── 2. admin_create_stops_batch ────────────────────────────────────────────
|
||||||
-- Inserts many stops in one call. Returns JSONB with created IDs + slugs.
|
-- Inserts many stops in one call. Returns JSONB with created IDs + slugs.
|
||||||
-- On any per-row failure the whole batch rolls back (transactional).
|
-- 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_brand_id UUID,
|
||||||
p_stops JSONB -- array of {city, state, location, date, time, address?, zip?, cutoff_time?, active?}
|
p_stops JSONB -- array of {city, state, location, date, time, address?, zip?, cutoff_time?, active?}
|
||||||
)
|
)
|
||||||
RETURNS JSONB
|
RETURNS JSONB
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
SECURITY DEFINER SET search_path = public
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
AS $$
|
AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
v_row JSONB;
|
v_row JSONB;
|
||||||
v_result JSONB := '[]'::JSONB;
|
v_result JSONB := '[]'::JSONB;
|
||||||
v_slug_base TEXT;
|
v_slug_base TEXT;
|
||||||
v_slug TEXT;
|
v_slug TEXT;
|
||||||
v_counter INT;
|
v_counter INT;
|
||||||
v_id UUID;
|
v_id UUID;
|
||||||
v_city TEXT;
|
v_city TEXT;
|
||||||
v_state TEXT;
|
v_state TEXT;
|
||||||
v_location TEXT;
|
v_location TEXT;
|
||||||
v_date TEXT;
|
v_date_text TEXT;
|
||||||
v_time TEXT;
|
v_time TEXT;
|
||||||
v_address TEXT;
|
v_address TEXT;
|
||||||
v_zip TEXT;
|
v_zip TEXT;
|
||||||
v_cutoff TEXT;
|
v_cutoff_txt TEXT;
|
||||||
v_active BOOLEAN;
|
v_active BOOLEAN;
|
||||||
|
v_date_value TIMESTAMPTZ;
|
||||||
|
v_cutoff_val TIMESTAMPTZ;
|
||||||
BEGIN
|
BEGIN
|
||||||
IF p_brand_id IS NULL THEN
|
IF p_brand_id IS NULL THEN
|
||||||
RAISE EXCEPTION 'brand_id is required';
|
RAISE EXCEPTION 'brand_id is required';
|
||||||
@@ -113,22 +164,37 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
FOR v_row IN SELECT * FROM jsonb_array_elements(p_stops) LOOP
|
FOR v_row IN SELECT * FROM jsonb_array_elements(p_stops) LOOP
|
||||||
v_city := v_row->>'city';
|
v_city := v_row->>'city';
|
||||||
v_state := v_row->>'state';
|
v_state := v_row->>'state';
|
||||||
v_location := v_row->>'location';
|
v_location := v_row->>'location';
|
||||||
v_date := v_row->>'date';
|
v_date_text := v_row->>'date';
|
||||||
v_time := v_row->>'time';
|
v_time := v_row->>'time';
|
||||||
v_address := v_row->>'address';
|
v_address := v_row->>'address';
|
||||||
v_zip := v_row->>'zip';
|
v_zip := v_row->>'zip';
|
||||||
v_cutoff := v_row->>'cutoff_time';
|
v_cutoff_txt := v_row->>'cutoff_time';
|
||||||
v_active := COALESCE((v_row->>'active')::BOOLEAN, false);
|
v_active := COALESCE((v_row->>'active')::BOOLEAN, false);
|
||||||
|
|
||||||
IF v_city IS NULL OR length(trim(v_city)) = 0 THEN
|
IF v_city IS NULL OR length(trim(v_city)) = 0 THEN
|
||||||
RAISE EXCEPTION 'city is required for all stops';
|
RAISE EXCEPTION 'city is required for all stops';
|
||||||
END IF;
|
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'))
|
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_slug := v_slug_base;
|
||||||
v_counter := 0;
|
v_counter := 0;
|
||||||
WHILE EXISTS (SELECT 1 FROM stops WHERE brand_id = p_brand_id AND slug = v_slug) LOOP
|
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,
|
brand_id, city, state, location, date, time, slug,
|
||||||
address, zip, cutoff_time, active, status
|
address, zip, cutoff_time, active, status
|
||||||
) VALUES (
|
) VALUES (
|
||||||
p_brand_id, v_city, v_state, v_location, v_date, v_time, v_slug,
|
p_brand_id, v_city, v_state, v_location,
|
||||||
v_address, v_zip, v_cutoff, v_active, 'draft'
|
v_date_value, v_time, v_slug,
|
||||||
|
v_address, v_zip, v_cutoff_val, v_active, 'draft'
|
||||||
)
|
)
|
||||||
RETURNING id INTO v_id;
|
RETURNING id INTO v_id;
|
||||||
|
|
||||||
@@ -149,7 +216,29 @@ BEGIN
|
|||||||
END LOOP;
|
END LOOP;
|
||||||
|
|
||||||
RETURN v_result;
|
RETURN v_result;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE EXCEPTION 'admin_create_stops_batch failed: % (SQLSTATE %)', SQLERRM, SQLSTATE;
|
||||||
END;
|
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;
|
COMMIT;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ RETURNS TABLE (
|
|||||||
city TEXT,
|
city TEXT,
|
||||||
state TEXT,
|
state TEXT,
|
||||||
date TIMESTAMPTZ,
|
date TIMESTAMPTZ,
|
||||||
time TEXT,
|
"time" TEXT,
|
||||||
location TEXT,
|
location TEXT,
|
||||||
address TEXT,
|
address TEXT,
|
||||||
slug TEXT,
|
slug TEXT,
|
||||||
@@ -30,7 +30,7 @@ BEGIN
|
|||||||
s.city,
|
s.city,
|
||||||
s.state,
|
s.state,
|
||||||
s.date,
|
s.date,
|
||||||
s.time,
|
s."time",
|
||||||
s.location,
|
s.location,
|
||||||
s.address,
|
s.address,
|
||||||
s.slug,
|
s.slug,
|
||||||
|
|||||||
@@ -103,6 +103,14 @@ CREATE TABLE IF NOT EXISTS user_activity_logs (
|
|||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
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
|
-- 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())
|
('c3d4e5f6-a7b8-9012-cdef-123456789012', 'Orchard Fresh', 'orchard-fresh', 'starter', 2, 10, 50, 'cus_demo_orchard', NOW())
|
||||||
ON CONFLICT (id) DO NOTHING;
|
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)
|
-- NOTE: The remainder of the original seed data (products, stops, orders, water logs, etc.)
|
||||||
VALUES
|
-- has been removed from this migration because the column layouts in the live DB
|
||||||
('p0010001-0000-0000-0000-000000000001', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
-- (shaped by many prior migrations) no longer match the INSERTs here.
|
||||||
'Honeycrisp Apples', 'Premium organic Honeycrisp apples, sweet and crisp', 3.99, 'lb', 'Apples', 'APP-HC-001', true, true, NULL, NOW()),
|
-- The 3 demo brands above will be inserted (now that plan_tier/max_* columns exist).
|
||||||
('p0010002-0000-0000-0000-000000000002', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
-- Use the admin UI or separate scripts to populate test data as needed.
|
||||||
'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;
|
|
||||||
|
|
||||||
-- =============================================================================
|
COMMIT;
|
||||||
-- 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;
|
|
||||||
|
|||||||
@@ -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
|
* push-migrations.js
|
||||||
*
|
*
|
||||||
* Pushes migration files to the linked Supabase project.
|
* 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`.
|
* otherwise falls back to direct PostgreSQL connection via `pg`.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node supabase/push-migrations.js # push all pending migrations
|
* node supabase/push-migrations.js # push all migration files
|
||||||
* node supabase/push-migrations.js 082 # push only migration 082
|
* node supabase/push-migrations.js 148 # push only migrations matching 148_*.sql
|
||||||
*
|
*
|
||||||
* Prerequisites for CLI mode:
|
* Prerequisites for CLI mode (preferred, works even without direct 5432 access):
|
||||||
* brew install supabase/tap/supabase # install CLI
|
* supabase login
|
||||||
* supabase link --project-ref <ref> # link project once
|
* supabase link --project-ref wnzkhezyhnfzhkhiflrp
|
||||||
*
|
*
|
||||||
* Prerequisites for direct PG mode:
|
* 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");
|
const { Client } = require("pg");
|
||||||
@@ -35,17 +37,25 @@ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|||||||
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||||
|
|
||||||
async function pushWithCli(files) {
|
async function pushWithCli(files) {
|
||||||
console.log("Using Supabase CLI (supabase db push)...\n");
|
console.log("Using Supabase CLI (supabase db query --linked --file)...\n");
|
||||||
const joined = files.map((f) => `supabase/migrations/${f}`).join(" ");
|
for (const f of files) {
|
||||||
try {
|
const filePath = `supabase/migrations/${f}`;
|
||||||
execSync(
|
process.stdout.write(` Applying ${f}... `);
|
||||||
`supabase db push --db-url "postgresql://postgres:${serviceRoleKey}@db.wnzkhezyhnfzhkhiflrp.supabase.co:5432/postgres" ${joined}`,
|
try {
|
||||||
{ stdio: "inherit", cwd: path.resolve(__dirname, "..") }
|
execSync(
|
||||||
);
|
`supabase db query --linked --file "${filePath}"`,
|
||||||
return true;
|
{ stdio: "inherit", cwd: path.resolve(__dirname, "..") }
|
||||||
} catch (err) {
|
);
|
||||||
return false;
|
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) {
|
async function pushWithPg(sql, migrationFile) {
|
||||||
@@ -102,14 +112,18 @@ async function main() {
|
|||||||
|
|
||||||
let ok = false;
|
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;
|
let hasSupabaseCli = false;
|
||||||
try {
|
try {
|
||||||
hasSupabaseCli = !!execSync("which supabase", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
hasSupabaseCli = !!execSync("which supabase", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
||||||
} catch {
|
} catch {
|
||||||
hasSupabaseCli = false;
|
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) {
|
if (hasSupabaseCli && isLinked) {
|
||||||
console.log("Supabase CLI detected and project is linked.\n");
|
console.log("Supabase CLI detected and project is linked.\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user