| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { EdgeFunction, ServerFunction, Secret, ScheduledFunction } from '../types'; |
| import cronParser from 'cron-parser'; |
|
|
| |
| |
| |
|
|
| export interface EdgeFunctionFileData { |
| name: string; |
| method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'ANY'; |
| description?: string; |
| enabled: boolean; |
| timeoutMs: number; |
| code: string; |
| } |
|
|
| export interface ServerFunctionFileData { |
| name: string; |
| description?: string; |
| enabled: boolean; |
| code: string; |
| } |
|
|
| export interface SecretFileData { |
| name: string; |
| description?: string; |
| hasValue?: boolean; |
| } |
|
|
| export interface ScheduledFunctionFileData { |
| name: string; |
| description?: string; |
| functionName: string; |
| cronExpression: string; |
| timezone: string; |
| enabled: boolean; |
| config: Record<string, unknown>; |
| lastRunAt?: string; |
| nextRunAt?: string; |
| lastStatus?: string; |
| } |
|
|
| export interface ServerContextMetadata { |
| projectId: string; |
| runtimeDeploymentId?: string; |
| hasDatabase: boolean; |
| edgeFunctionCount: number; |
| serverFunctionCount: number; |
| secretCount: number; |
| scheduledFunctionCount: number; |
| } |
|
|
| export interface ValidationResult { |
| valid: boolean; |
| errors: string[]; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| export function generateEdgeFunctionFile(fn: EdgeFunction): string { |
| const data: EdgeFunctionFileData = { |
| name: fn.name, |
| method: fn.method, |
| description: fn.description, |
| enabled: fn.enabled, |
| timeoutMs: fn.timeoutMs || 5000, |
| code: fn.code, |
| }; |
| return JSON.stringify(data, null, 2); |
| } |
|
|
| |
| |
| |
| export function generateServerFunctionFile(fn: ServerFunction): string { |
| const data: ServerFunctionFileData = { |
| name: fn.name, |
| description: fn.description, |
| enabled: fn.enabled, |
| code: fn.code, |
| }; |
| return JSON.stringify(data, null, 2); |
| } |
|
|
| |
| |
| |
| export function generateSecretFile(secret: Secret): string { |
| const data: SecretFileData = { |
| name: secret.name, |
| description: secret.description || undefined, |
| hasValue: secret.hasValue, |
| }; |
| return JSON.stringify(data, null, 2); |
| } |
|
|
| |
| |
| |
| export function generateScheduledFunctionFile( |
| fn: ScheduledFunction, |
| edgeFunctionName: string |
| ): string { |
| const data: ScheduledFunctionFileData = { |
| name: fn.name, |
| description: fn.description, |
| functionName: edgeFunctionName, |
| cronExpression: fn.cronExpression, |
| timezone: fn.timezone, |
| enabled: fn.enabled, |
| config: fn.config, |
| lastRunAt: fn.lastRunAt?.toISOString(), |
| nextRunAt: fn.nextRunAt?.toISOString(), |
| lastStatus: fn.lastStatus, |
| }; |
| return JSON.stringify(data, null, 2); |
| } |
|
|
| |
| |
| |
| export function generateServerContextMetadata( |
| projectId: string, |
| edgeFunctions: EdgeFunction[], |
| serverFunctions: ServerFunction[], |
| secrets: Secret[], |
| scheduledFunctions?: ScheduledFunction[], |
| runtimeDeploymentId?: string |
| ): ServerContextMetadata { |
| return { |
| projectId, |
| runtimeDeploymentId, |
| hasDatabase: !!runtimeDeploymentId, |
| edgeFunctionCount: edgeFunctions.filter(f => f.enabled).length, |
| serverFunctionCount: serverFunctions.filter(f => f.enabled).length, |
| secretCount: secrets.length, |
| scheduledFunctionCount: scheduledFunctions ? scheduledFunctions.filter(f => f.enabled).length : 0, |
| }; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| const RESERVED_SERVER_FUNCTION_NAMES = [ |
| 'db', 'fetch', 'console', 'args', 'request', 'Response', 'server', 'secrets', 'atob', 'btoa' |
| ]; |
|
|
| |
| |
| |
| export function validateEdgeFunctionData(data: unknown): ValidationResult { |
| const errors: string[] = []; |
|
|
| if (!data || typeof data !== 'object') { |
| return { valid: false, errors: ['Invalid JSON: expected an object'] }; |
| } |
|
|
| const fn = data as Record<string, unknown>; |
|
|
| |
| if (!fn.name || typeof fn.name !== 'string') { |
| errors.push('Missing or invalid "name" field'); |
| } else if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(fn.name)) { |
| errors.push('Name must be lowercase letters, numbers, and hyphens only (e.g., "get-users")'); |
| } |
|
|
| |
| const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'ANY']; |
| if (!fn.method || typeof fn.method !== 'string') { |
| errors.push('Missing or invalid "method" field'); |
| } else if (!validMethods.includes(fn.method)) { |
| errors.push(`Method must be one of: ${validMethods.join(', ')}`); |
| } |
|
|
| |
| if (!fn.code || typeof fn.code !== 'string') { |
| errors.push('Missing or invalid "code" field'); |
| } else { |
| try { |
| new Function(fn.code); |
| } catch (e: unknown) { |
| const message = e instanceof Error ? e.message : String(e); |
| errors.push(`JavaScript syntax error: ${message}`); |
| } |
| } |
|
|
| |
| if (fn.enabled !== undefined && typeof fn.enabled !== 'boolean') { |
| errors.push('"enabled" must be a boolean'); |
| } |
|
|
| |
| if (fn.timeoutMs !== undefined) { |
| if (typeof fn.timeoutMs !== 'number') { |
| errors.push('"timeoutMs" must be a number'); |
| } else if (fn.timeoutMs < 1000 || fn.timeoutMs > 30000) { |
| errors.push('Timeout must be between 1000 and 30000 ms'); |
| } |
| } |
|
|
| return { valid: errors.length === 0, errors }; |
| } |
|
|
| |
| |
| |
| export function validateServerFunctionData(data: unknown): ValidationResult { |
| const errors: string[] = []; |
|
|
| if (!data || typeof data !== 'object') { |
| return { valid: false, errors: ['Invalid JSON: expected an object'] }; |
| } |
|
|
| const fn = data as Record<string, unknown>; |
|
|
| |
| if (!fn.name || typeof fn.name !== 'string') { |
| errors.push('Missing or invalid "name" field'); |
| } else if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(fn.name)) { |
| errors.push('Name must be a valid JavaScript identifier (e.g., "validateAuth", "formatPrice")'); |
| } else if (RESERVED_SERVER_FUNCTION_NAMES.includes(fn.name)) { |
| errors.push(`Cannot use reserved name: ${fn.name}`); |
| } |
|
|
| |
| if (!fn.code || typeof fn.code !== 'string') { |
| errors.push('Missing or invalid "code" field'); |
| } else { |
| try { |
| new Function('args', 'db', 'fetch', 'console', fn.code); |
| } catch (e: unknown) { |
| const message = e instanceof Error ? e.message : String(e); |
| errors.push(`JavaScript syntax error: ${message}`); |
| } |
| } |
|
|
| |
| if (fn.enabled !== undefined && typeof fn.enabled !== 'boolean') { |
| errors.push('"enabled" must be a boolean'); |
| } |
|
|
| return { valid: errors.length === 0, errors }; |
| } |
|
|
| |
| |
| |
| export function validateSecretData(data: unknown): ValidationResult { |
| const errors: string[] = []; |
|
|
| if (!data || typeof data !== 'object') { |
| return { valid: false, errors: ['Invalid JSON: expected an object'] }; |
| } |
|
|
| const secret = data as Record<string, unknown>; |
|
|
| |
| if (!secret.name || typeof secret.name !== 'string') { |
| errors.push('Missing or invalid "name" field'); |
| } else if (!/^[A-Z][A-Z0-9_]*$/.test(secret.name)) { |
| errors.push('Name must be SCREAMING_SNAKE_CASE (e.g., MY_API_KEY, SMTP_PASSWORD)'); |
| } else if (secret.name.length > 64) { |
| errors.push('Name must be 64 characters or less'); |
| } |
|
|
| |
| if (secret.description !== undefined && typeof secret.description !== 'string') { |
| errors.push('"description" must be a string'); |
| } |
|
|
| return { valid: errors.length === 0, errors }; |
| } |
|
|
| |
| |
| |
| export function validateScheduledFunctionData(data: unknown): ValidationResult { |
| const errors: string[] = []; |
|
|
| if (!data || typeof data !== 'object') { |
| return { valid: false, errors: ['Invalid JSON: expected an object'] }; |
| } |
|
|
| const fn = data as Record<string, unknown>; |
|
|
| |
| if (!fn.name || typeof fn.name !== 'string') { |
| errors.push('Missing or invalid "name" field'); |
| } else if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(fn.name)) { |
| errors.push('Name must be lowercase letters, numbers, and hyphens only'); |
| } |
|
|
| |
| if (!fn.functionName || typeof fn.functionName !== 'string') { |
| errors.push('Missing or invalid "functionName" field'); |
| } |
|
|
| |
| if (!fn.cronExpression || typeof fn.cronExpression !== 'string') { |
| errors.push('Missing or invalid "cronExpression" field'); |
| } else { |
| try { |
| cronParser.parseExpression(fn.cronExpression); |
| } catch { |
| errors.push('Invalid cron expression'); |
| } |
| } |
|
|
| |
| if (fn.timezone !== undefined) { |
| if (typeof fn.timezone !== 'string') { |
| errors.push('"timezone" must be a string'); |
| } else { |
| try { |
| Intl.DateTimeFormat(undefined, { timeZone: fn.timezone }); |
| } catch { |
| errors.push(`Invalid timezone: ${fn.timezone}`); |
| } |
| } |
| } |
|
|
| |
| if (fn.enabled !== undefined && typeof fn.enabled !== 'boolean') { |
| errors.push('"enabled" must be a boolean'); |
| } |
|
|
| |
| if (fn.config !== undefined) { |
| if (typeof fn.config !== 'object' || fn.config === null || Array.isArray(fn.config)) { |
| errors.push('"config" must be a plain object'); |
| } |
| } |
|
|
| return { valid: errors.length === 0, errors }; |
| } |
|
|