Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions pkgs/cli/__tests__/commands/compile/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,45 @@ describe('fetchFlowSQL', () => {
).rejects.toThrow('Did you add it to supabase/functions/pgflow/index.ts');
});

it('should handle missing ControlPlane edge function (Supabase gateway 404)', async () => {
// Supabase gateway returns 404 with different error format when function doesn't exist
const mockResponse = {
ok: false,
status: 404,
json: async () => ({
// Supabase gateway error - not our ControlPlane's "Flow Not Found"
error: 'not_found',
message: 'Function not found',
}),
};

global.fetch = vi.fn().mockResolvedValue(mockResponse);

await expect(
fetchFlowSQL('test_flow', 'http://127.0.0.1:50621/functions/v1/pgflow', 'test-publishable-key')
).rejects.toThrow('ControlPlane edge function not found');
await expect(
fetchFlowSQL('test_flow', 'http://127.0.0.1:50621/functions/v1/pgflow', 'test-publishable-key')
).rejects.toThrow('npx pgflow install');
});

it('should handle 404 with non-JSON response (HTML error page)', async () => {
// Some gateways return HTML error pages
const mockResponse = {
ok: false,
status: 404,
json: async () => {
throw new Error('Unexpected token < in JSON');
},
};

global.fetch = vi.fn().mockResolvedValue(mockResponse);

await expect(
fetchFlowSQL('test_flow', 'http://127.0.0.1:50621/functions/v1/pgflow', 'test-publishable-key')
).rejects.toThrow('ControlPlane edge function not found');
});

it('should construct correct URL with flow slug', async () => {
const mockResponse = {
ok: true,
Expand Down
32 changes: 26 additions & 6 deletions pkgs/cli/src/commands/compile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,33 @@ export async function fetchFlowSQL(
});

if (response.status === 404) {
const errorData = await response.json();
let errorData: { error?: string; message?: string } = {};
try {
errorData = await response.json();
} catch {
// JSON parse failed - likely Supabase gateway error (HTML or plain text)
}

// Check if this is our ControlPlane's 404 (has 'Flow Not Found' error)
// vs Supabase gateway's 404 (function doesn't exist)
if (errorData.error === 'Flow Not Found') {
throw new Error(
`Flow '${flowSlug}' not found.\n\n` +
`${errorData.message || 'Did you add it to supabase/functions/pgflow/index.ts?'}\n\n` +
`Fix:\n` +
`1. Add your flow to supabase/functions/pgflow/index.ts\n` +
`2. Restart edge functions: supabase functions serve`
);
}

// ControlPlane edge function itself doesn't exist
throw new Error(
`Flow '${flowSlug}' not found.\n\n` +
`${errorData.message || 'Did you add it to supabase/functions/pgflow/index.ts?'}\n\n` +
`Fix:\n` +
`1. Add your flow to supabase/functions/pgflow/index.ts\n` +
`2. Restart edge functions: supabase functions serve`
'ControlPlane edge function not found.\n\n' +
'The pgflow edge function is not installed or not running.\n\n' +
'Fix:\n' +
'1. Run: npx pgflow install\n' +
'2. Start edge functions: supabase functions serve\n\n' +
'Or use previous version: npx pgflow@0.8.0 compile path/to/flow.ts'
);
}

Expand Down
12 changes: 12 additions & 0 deletions pkgs/edge-worker/tests/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ export const e2eConfig = {
get dbUrl() {
return 'postgresql://postgres:postgres@127.0.0.1:50322/postgres';
},

/** Max retries when waiting for server to be ready (default: 30) */
get serverReadyMaxRetries() {
const envValue = Deno.env.get('E2E_SERVER_READY_MAX_RETRIES');
return envValue ? parseInt(envValue, 10) : 30;
},

/** Delay between retries in ms (default: 1000) */
get serverReadyRetryDelayMs() {
const envValue = Deno.env.get('E2E_SERVER_READY_RETRY_DELAY_MS');
return envValue ? parseInt(envValue, 10) : 1000;
},
};

/**
Expand Down
4 changes: 2 additions & 2 deletions pkgs/edge-worker/tests/e2e/control-plane.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ const BASE_URL = `${API_URL}/functions/v1/pgflow`;
async function ensureServerReady() {
log('Ensuring pgflow function is ready...');

const maxRetries = 15;
const retryDelayMs = 1000;
const maxRetries = e2eConfig.serverReadyMaxRetries;
const retryDelayMs = e2eConfig.serverReadyRetryDelayMs;

for (let i = 0; i < maxRetries; i++) {
try {
Expand Down
34 changes: 26 additions & 8 deletions scripts/supabase-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,26 @@ NC='\033[0m' # No Color
# Required services for edge function development
# Note: Services like imgproxy, studio, inbucket, analytics, vector, pg_meta are optional
# Container names use project_id suffix from config.toml (e.g., supabase_db_cli for project_id="cli")
# We use pattern matching to handle different project suffixes
REQUIRED_SERVICES=(
# The project_id matches the "name" field in project.json
REQUIRED_SERVICE_PREFIXES=(
"supabase_db_"
"supabase_kong_"
"supabase_edge_runtime_"
"supabase_rest_"
"supabase_realtime_"
)

# Check if all required services are running via docker ps
# Check if all required services are running for a specific project
# This is more reliable than `supabase status` which returns 0 even with stopped services
# Args: $1 = project_name (e.g., "cli", "edge-worker")
check_required_services_running() {
local project_name="$1"
local running_containers
running_containers=$(docker ps --format '{{.Names}}' 2>/dev/null)

for service_prefix in "${REQUIRED_SERVICES[@]}"; do
if ! echo "$running_containers" | grep -q "^${service_prefix}"; then
for service_prefix in "${REQUIRED_SERVICE_PREFIXES[@]}"; do
local full_container_name="${service_prefix}${project_name}"
if ! echo "$running_containers" | grep -qF "$full_container_name"; then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The grep -qF performs substring matching which can cause false positives when project names are substrings of each other. For example, if projects 'cli' and 'cli_test' exist, checking for 'cli' would incorrectly match 'supabase_db_cli_test' when only 'cli_test' is running.

Fix:

if ! echo "$running_containers" | grep -qxF "$full_container_name"; then

The -x flag ensures exact line matching, preventing substring false positives.

Suggested change
if ! echo "$running_containers" | grep -qF "$full_container_name"; then
if ! echo "$running_containers" | grep -qxF "$full_container_name"; then

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

return 1
fi
done
Expand All @@ -72,15 +75,30 @@ if [ ! -d "$PROJECT_DIR" ]; then
exit 1
fi

# Convert to absolute path for consistent file lookups after cd
PROJECT_DIR=$(realpath "$PROJECT_DIR")

# Change to project directory (Supabase CLI uses current directory)
cd "$PROJECT_DIR"

echo -e "${YELLOW}Checking Supabase status in: $PROJECT_DIR${NC}"
# Extract project name from project.json (matches project_id in supabase/config.toml)
if [ ! -f "$PROJECT_DIR/project.json" ]; then
echo -e "${RED}Error: project.json not found in $PROJECT_DIR${NC}" >&2
exit 1
fi

PROJECT_NAME=$(jq -r '.name' "$PROJECT_DIR/project.json")
if [ -z "$PROJECT_NAME" ] || [ "$PROJECT_NAME" = "null" ]; then
echo -e "${RED}Error: Could not read 'name' from project.json${NC}" >&2
exit 1
fi

echo -e "${YELLOW}Checking Supabase status for project '$PROJECT_NAME' in: $PROJECT_DIR${NC}"

# Fast path: Check if all required Supabase services are running via docker ps
# This is more reliable than `supabase status` which returns 0 even with stopped services
if check_required_services_running; then
echo -e "${GREEN}✓ Supabase is already running (all required services up)${NC}"
if check_required_services_running "$PROJECT_NAME"; then
echo -e "${GREEN}✓ Supabase is already running for project '$PROJECT_NAME' (all required services up)${NC}"
exit 0
fi

Expand Down