File size: 5,987 Bytes
5c98911 | 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 | import { supabase } from './supabase'
// --- Strangler Pattern Configuration Switches ---
const USE_GO_GATEWAY: Record<string, boolean> = {
'verify-receipt': false,
'nature-letter': false,
'embed-text': false,
'explain-methodology': false,
'mock-vcs': false,
}
let GO_GATEWAY_URL = 'http://localhost:8080'
const GO_GATEWAY_ENDPOINTS: Record<string, (payload: unknown) => string> = {
'verify-receipt': (payload) => {
const submissionId = getPayloadString(payload, 'submissionId') ?? getPayloadString(payload, 'id')
return submissionId ? `/submissions/${encodeURIComponent(submissionId)}/verify` : '/verify-receipt'
},
'nature-letter': () => '/nature/letter',
'embed-text': () => '/methodologies/reseed',
'explain-methodology': () => '/methodologies/match',
'mock-vcs': () => '/registries/vcs/submit',
}
export function setUseGoGateway(functionName: string, use: boolean): void {
if (functionName in USE_GO_GATEWAY) {
USE_GO_GATEWAY[functionName] = use
}
}
export function setGoGatewayUrl(url: string): void {
GO_GATEWAY_URL = url
}
function getPayloadString(payload: unknown, key: string): string | undefined {
if (!payload || typeof payload !== 'object') return undefined
const value = (payload as Record<string, unknown>)[key]
return typeof value === 'string' && value.trim().length > 0 ? value : undefined
}
function isEnvelopeResponse(value: unknown): value is { data: unknown; error?: unknown } {
return (
!!value &&
typeof value === 'object' &&
'data' in value &&
'meta' in value &&
'error' in value
)
}
function unwrapEnvelope(functionName: string, response: unknown): unknown {
if (!isEnvelopeResponse(response)) return response
if (response.error) {
throw new Error(`[apiAdapter] ${functionName} Gateway μλ΅ μ€λ₯: ${String(response.error)}`)
}
return response.data
}
// --- Data Contract Verification Guard (Contract Assertions) ---
/**
* Go Gatewayλ‘ μ΄μ£Όνκ±°λ Deno ν¨μ ν΅μ μ λ°μ΄ν° μ€ν€λ§ κΉ¨μ§(Contract Broken)μΌλ‘ μΈν
* νλ‘ νΈμλ νμ¬λ₯Ό λ§λ λ°νμ νμ
κ²μ¦ κ°λ
*/
function assertDataContract(functionName: string, data: unknown): void {
if (!data) {
throw new Error(`[apiAdapter] API κ³μ½ κ²μ¦ μ€ν¨: ${functionName} μλ΅μ΄ λΉμ΄μμ΅λλ€.`)
}
const record = data as Record<string, unknown>
switch (functionName) {
case 'verify-receipt':
if (record.success === undefined && record.status === undefined) {
throw new Error(`[apiAdapter] API κ³μ½ κ²μ¦ μ€ν¨: verify-receipt μλ΅μ νμ νλ(success)κ° λλ½λμμ΅λλ€.`)
}
if (record.co2Reduction === undefined && record.co2_reduction === undefined) {
throw new Error(`[apiAdapter] API κ³μ½ κ²μ¦ μ€ν¨: verify-receipt μλ΅μ νμ νλ(co2Reduction)κ° λλ½λμμ΅λλ€.`)
}
break;
case 'nature-letter':
if (!record.body) {
throw new Error(`[apiAdapter] API κ³μ½ κ²μ¦ μ€ν¨: nature-letter μλ΅μ νμ νλ(body)κ° λλ½λμμ΅λλ€.`)
}
break;
case 'embed-text':
if (!Array.isArray(record.embedding)) {
throw new Error(`[apiAdapter] API κ³μ½ κ²μ¦ μ€ν¨: embed-text μλ΅μ νμ νλ(embedding)κ° λλ½λμμ΅λλ€.`)
}
break;
case 'explain-methodology':
if (record.eligibilityScore === undefined && record.eligibility_score === undefined) {
throw new Error(`[apiAdapter] API κ³μ½ κ²μ¦ μ€ν¨: explain-methodology μλ΅μ νμ νλ(eligibilityScore)κ° λλ½λμμ΅λλ€.`)
}
if (!record.explanation) {
throw new Error(`[apiAdapter] API κ³μ½ κ²μ¦ μ€ν¨: explain-methodology μλ΅μ νμ νλ(explanation)κ° λλ½λμμ΅λλ€.`)
}
break;
case 'mock-vcs':
if (record.success === undefined) {
throw new Error(`[apiAdapter] API κ³μ½ κ²μ¦ μ€ν¨: mock-vcs μλ΅μ νμ νλ(success)κ° λλ½λμμ΅λλ€.`)
}
if (!record.externalId && !record.external_id) {
throw new Error(`[apiAdapter] API κ³μ½ κ²μ¦ μ€ν¨: mock-vcs μλ΅μ νμ νλ(externalId)κ° λλ½λμμ΅λλ€.`)
}
break;
default:
// Unrecognized function, bypass contract validation
break;
}
}
// --- Unified API Adapter Caller ---
/**
* 5λ Supabase Deno Edge Functions νΈμΆμ λ¨μΌ μΆμννμ¬ νΈμΆνκ³
* Go API κ²μ΄νΈμ¨μ΄ μ곡 μ λΌμ°ν
μ μ μ§μ μΌλ‘ μμ μ°ν(Strangler Pattern)μν΅λλ€.
*/
export async function invokeEdgeFunction<TReq, TRes>(
functionName: string,
payload: TReq
): Promise<TRes> {
const useGo = USE_GO_GATEWAY[functionName] || false
if (useGo) {
// 1. Go API Gatewayλ‘ μ°ν (fetch API)
const endpoint = GO_GATEWAY_ENDPOINTS[functionName]?.(payload) ?? `/${functionName}`
const targetUrl = `${GO_GATEWAY_URL}/api/v1${endpoint}`
const response = await fetch(targetUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
if (!response.ok) {
const errText = await response.text()
throw new Error(`[apiAdapter] Go Gateway API νΈμΆ μ€ν¨ (${response.status}): ${errText}`)
}
const rawData = await response.json()
const resData = unwrapEnvelope(functionName, rawData)
// λ°νμ λ°μ΄ν° κ³μ½ κ²μ¦
assertDataContract(functionName, resData)
return resData as TRes
} else {
// 2. κΈ°μ‘΄ Supabase Deno Edge Function νΈμΆ
const { data, error } = await supabase.functions.invoke(functionName, {
body: payload as Record<string, unknown>,
})
if (error) {
throw new Error(error.message || String(error))
}
// λ°νμ λ°μ΄ν° κ³μ½ κ²μ¦
const resData = unwrapEnvelope(functionName, data)
assertDataContract(functionName, resData)
return resData as TRes
}
}
|