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
  }
}