File size: 5,934 Bytes
eeb9404 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | /**
* Edge Function Invocation API
*
* Public endpoint for invoking edge functions
* URL: /api/deployments/{deploymentId}/functions/{functionName}[/additional/path]
*/
import { NextRequest, NextResponse } from 'next/server';
import { getSQLiteAdapter } from '@/lib/vfs/adapters/server';
import { executeFunction } from '@/lib/edge-functions/executor';
import { FunctionRequest } from '@/lib/edge-functions/types';
import { logger } from '@/lib/utils';
interface RouteParams {
params: Promise<{
id: string; // Deployment ID
path: string[]; // [functionName, ...additionalPath]
}>;
}
/**
* Handle edge function invocation
*/
async function handleRequest(
request: NextRequest,
{ params }: RouteParams
): Promise<NextResponse> {
const startTime = Date.now();
try {
const { id: deploymentId, path } = await params;
// Extract function name and additional path
const functionName = path[0];
const additionalPath = path.slice(1).join('/');
if (!functionName) {
return NextResponse.json(
{ error: 'Function name is required' },
{ status: 400 }
);
}
// Get adapter and initialize
const adapter = getSQLiteAdapter();
await adapter.init();
// Check if deployment exists — try UUID first, then slug
let deployment = await adapter.getDeployment?.(deploymentId) ?? null;
if (!deployment && adapter.getDeploymentBySlug) {
deployment = await adapter.getDeploymentBySlug(deploymentId);
}
if (!deployment) {
return NextResponse.json(
{ error: 'Deployment not found' },
{ status: 404 }
);
}
// Use the actual deployment ID for subsequent lookups (in case we matched by slug)
const resolvedDeploymentId = deployment.id;
if (!deployment.databaseEnabled) {
return NextResponse.json(
{ error: 'Edge functions not enabled for this deployment' },
{ status: 404 }
);
}
// Get the deployment database
const deploymentDb = adapter.getDeploymentDatabaseForAnalytics(resolvedDeploymentId);
if (!deploymentDb) {
return NextResponse.json(
{ error: 'Deployment database not available' },
{ status: 500 }
);
}
// Get the function by name
const fn = deploymentDb.getFunctionByName(functionName);
if (!fn) {
return NextResponse.json(
{ error: `Function "${functionName}" not found` },
{ status: 404 }
);
}
if (!fn.enabled) {
return NextResponse.json(
{ error: `Function "${functionName}" is disabled` },
{ status: 503 }
);
}
// Check HTTP method
if (fn.method !== 'ANY' && fn.method !== request.method) {
return NextResponse.json(
{ error: `Method ${request.method} not allowed for this function` },
{ status: 405 }
);
}
// Parse request body for non-GET requests
let body: unknown = null;
if (request.method !== 'GET' && request.method !== 'HEAD') {
const contentType = request.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
try {
body = await request.json();
} catch {
body = null;
}
} else if (contentType.includes('text/')) {
body = await request.text();
}
}
// Build query params
const url = new URL(request.url);
const query: Record<string, string> = {};
url.searchParams.forEach((value, key) => {
query[key] = value;
});
// Build headers (filter sensitive ones)
const headers: Record<string, string> = {};
const safeHeaders = [
'accept',
'accept-language',
'content-type',
'content-length',
'origin',
'referer',
'user-agent',
'x-requested-with',
'cookie',
];
request.headers.forEach((value, key) => {
if (safeHeaders.includes(key.toLowerCase())) {
headers[key.toLowerCase()] = value;
}
});
// Build function request
const functionRequest: FunctionRequest = {
method: request.method,
headers,
body,
params: { path: additionalPath },
query,
path: `/${functionName}${additionalPath ? '/' + additionalPath : ''}`,
};
// Execute function
const result = await executeFunction(fn, functionRequest, deploymentDb);
// Log execution (async, don't await)
try {
deploymentDb.logFunctionExecution(fn.id, {
method: request.method,
path: functionRequest.path,
statusCode: result.response.status,
durationMs: result.durationMs,
error: result.error,
});
} catch (logError) {
logger.error('[Edge Functions] Failed to log execution:', logError);
}
// Build response
const responseHeaders = new Headers();
Object.entries(result.response.headers).forEach(([key, value]) => {
responseHeaders.set(key, value);
});
// Add timing header
responseHeaders.set('X-Execution-Time', `${Date.now() - startTime}ms`);
// Serialize body
let responseBody: string;
if (typeof result.response.body === 'object') {
responseBody = JSON.stringify(result.response.body);
if (!responseHeaders.has('Content-Type')) {
responseHeaders.set('Content-Type', 'application/json');
}
} else {
responseBody = result.response.body;
}
return new NextResponse(responseBody, {
status: result.response.status,
headers: responseHeaders,
});
} catch (error) {
logger.error('[Edge Functions] Unexpected error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// Export handlers for all HTTP methods
export const GET = handleRequest;
export const POST = handleRequest;
export const PUT = handleRequest;
export const DELETE = handleRequest;
export const PATCH = handleRequest;
|