feat: reorganize supabase config - flat db init structure, add edge functions, mcp, kong api config
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# Supabase Edge Functions
|
||||
|
||||
This document describes the available Edge Functions in this self-hosted Supabase instance.
|
||||
|
||||
## institute-geocoder
|
||||
|
||||
Institute address geocoding using SearXNG/OpenStreetMap
|
||||
|
||||
**Endpoints:**
|
||||
- `/functions/v1/institute-geocoder`
|
||||
- `/functions/v1/institute-geocoder/batch`
|
||||
|
||||
**Usage:** POST with institute_id and optional address data
|
||||
|
||||
**Dependencies:** SearXNG service, OpenStreetMap data
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Follow this setup guide to integrate the Deno language server with your editor:
|
||||
// https://deno.land/manual/getting_started/setup_your_environment
|
||||
// This enables autocomplete, go to definition, etc.
|
||||
|
||||
import { serve } from "https://deno.land/[email protected]/http/server.ts"
|
||||
|
||||
serve(async () => {
|
||||
return new Response(
|
||||
`"Hello from Edge Functions!"`,
|
||||
{ headers: { "Content-Type": "application/json" } },
|
||||
)
|
||||
})
|
||||
|
||||
// To invoke:
|
||||
// curl 'http://localhost:<KONG_HTTP_PORT>/functions/v1/hello' \
|
||||
// --header 'Authorization: Bearer <anon/service_role API key>'
|
||||
@@ -0,0 +1,391 @@
|
||||
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
}
|
||||
|
||||
interface BatchGeocodingRequest {
|
||||
limit?: number
|
||||
force_refresh?: boolean
|
||||
institute_ids?: string[]
|
||||
}
|
||||
|
||||
interface GeocodingResult {
|
||||
institute_id: string
|
||||
success: boolean
|
||||
message: string
|
||||
coordinates?: {
|
||||
latitude: number
|
||||
longitude: number
|
||||
boundingbox: string[]
|
||||
geojson?: any
|
||||
osm?: any
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
serve(async (req: Request) => {
|
||||
// Handle CORS preflight requests
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
try {
|
||||
// Get environment variables
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')
|
||||
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
|
||||
const searxngUrl = Deno.env.get('SEARXNG_URL') || 'https://search.kevlarai.com'
|
||||
|
||||
if (!supabaseUrl || !supabaseServiceKey) {
|
||||
throw new Error('Missing required environment variables')
|
||||
}
|
||||
|
||||
// Create Supabase client
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey)
|
||||
|
||||
// Parse request body
|
||||
const body: BatchGeocodingRequest = await req.json()
|
||||
const limit = body.limit || 10
|
||||
const forceRefresh = body.force_refresh || false
|
||||
|
||||
// Get institutes that need geocoding
|
||||
let query = supabase
|
||||
.from('institutes')
|
||||
.select('id, name, address, geo_coordinates')
|
||||
.not('import_id', 'is', null)
|
||||
|
||||
if (!forceRefresh) {
|
||||
// Only get institutes without coordinates or with empty coordinates
|
||||
query = query.or('geo_coordinates.is.null,geo_coordinates.eq.{}')
|
||||
}
|
||||
|
||||
if (body.institute_ids && body.institute_ids.length > 0) {
|
||||
query = query.in('id', body.institute_ids)
|
||||
}
|
||||
|
||||
const { data: institutes, error: fetchError } = await query.limit(limit)
|
||||
|
||||
if (fetchError) {
|
||||
throw new Error(`Failed to fetch institutes: ${fetchError.message}`)
|
||||
}
|
||||
|
||||
if (!institutes || institutes.length === 0) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: 'No institutes found that need geocoding',
|
||||
processed: 0
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`Processing ${institutes.length} institutes for geocoding`)
|
||||
|
||||
const results: GeocodingResult[] = []
|
||||
let successCount = 0
|
||||
let errorCount = 0
|
||||
|
||||
// Process institutes sequentially to avoid overwhelming the SearXNG service
|
||||
let processedCount = 0
|
||||
for (const institute of institutes) {
|
||||
try {
|
||||
const address = institute.address as any
|
||||
if (!address) {
|
||||
results.push({
|
||||
institute_id: institute.id,
|
||||
success: false,
|
||||
message: 'No address information available',
|
||||
error: 'Missing address data'
|
||||
})
|
||||
errorCount++
|
||||
processedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// Build search query from address components
|
||||
const addressParts = [
|
||||
address.street,
|
||||
address.town,
|
||||
address.county,
|
||||
address.postcode,
|
||||
address.country
|
||||
].filter(Boolean)
|
||||
|
||||
if (addressParts.length === 0) {
|
||||
results.push({
|
||||
institute_id: institute.id,
|
||||
success: false,
|
||||
message: 'No valid address components found',
|
||||
error: 'Empty address parts'
|
||||
})
|
||||
errorCount++
|
||||
processedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
const searchQuery = addressParts.join(', ')
|
||||
console.log(`Geocoding institute ${institute.id}: ${searchQuery}`)
|
||||
|
||||
// Query SearXNG for geocoding with fallback strategy
|
||||
const geocodingResult = await geocodeAddressWithFallback(address, searxngUrl)
|
||||
|
||||
if (geocodingResult.success && geocodingResult.coordinates) {
|
||||
// Update institute with geospatial coordinates
|
||||
const { error: updateError } = await supabase
|
||||
.from('institutes')
|
||||
.update({
|
||||
geo_coordinates: {
|
||||
latitude: geocodingResult.coordinates.latitude,
|
||||
longitude: geocodingResult.coordinates.longitude,
|
||||
boundingbox: geocodingResult.coordinates.boundingbox,
|
||||
geojson: geocodingResult.coordinates.geojson,
|
||||
osm: geocodingResult.coordinates.osm,
|
||||
search_query: searchQuery,
|
||||
geocoded_at: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
.eq('id', institute.id)
|
||||
|
||||
if (updateError) {
|
||||
throw new Error(`Failed to update institute: ${updateError.message}`)
|
||||
}
|
||||
|
||||
results.push({
|
||||
institute_id: institute.id,
|
||||
success: true,
|
||||
message: 'Successfully geocoded',
|
||||
coordinates: geocodingResult.coordinates
|
||||
})
|
||||
successCount++
|
||||
|
||||
// Log the successful geocoding
|
||||
await supabase
|
||||
.from('function_logs')
|
||||
.insert({
|
||||
file_id: null,
|
||||
step: 'batch_geocoding',
|
||||
message: 'Successfully geocoded institute address in batch',
|
||||
data: {
|
||||
institute_id: institute.id,
|
||||
search_query: searchQuery,
|
||||
coordinates: geocodingResult.coordinates
|
||||
}
|
||||
})
|
||||
|
||||
} else {
|
||||
results.push({
|
||||
institute_id: institute.id,
|
||||
success: false,
|
||||
message: 'Geocoding failed',
|
||||
error: geocodingResult.error || 'Unknown error'
|
||||
})
|
||||
errorCount++
|
||||
}
|
||||
|
||||
processedCount++
|
||||
|
||||
// Add a small delay between requests to be respectful to the SearXNG service
|
||||
// Optimize delay based on batch size for better performance
|
||||
if (processedCount < institutes.length) { // Don't delay after the last institute
|
||||
const delay = institutes.length > 200 ? 50 : 100; // Faster processing for large batches
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error processing institute ${institute.id}:`, error)
|
||||
results.push({
|
||||
institute_id: institute.id,
|
||||
success: false,
|
||||
message: 'Processing error',
|
||||
error: error.message
|
||||
})
|
||||
errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Log the batch operation
|
||||
await supabase
|
||||
.from('function_logs')
|
||||
.insert({
|
||||
file_id: null,
|
||||
step: 'batch_geocoding_complete',
|
||||
message: 'Batch geocoding operation completed',
|
||||
data: {
|
||||
total_processed: institutes.length,
|
||||
successful: successCount,
|
||||
failed: errorCount,
|
||||
results: results
|
||||
}
|
||||
})
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: 'Batch geocoding completed',
|
||||
summary: {
|
||||
total_processed: institutes.length,
|
||||
successful: successCount,
|
||||
failed: errorCount
|
||||
},
|
||||
results: results
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in batch institute geocoder:', error)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Internal server error',
|
||||
details: error.message
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
async function geocodeAddress(searchQuery: string, searxngUrl: string): Promise<{
|
||||
success: boolean
|
||||
coordinates?: {
|
||||
latitude: number
|
||||
longitude: number
|
||||
boundingbox: string[]
|
||||
geojson?: any
|
||||
osm?: any
|
||||
}
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
// Format search query for OSM
|
||||
const osmQuery = `!osm ${searchQuery}`
|
||||
const searchUrl = `${searxngUrl}/search?q=${encodeURIComponent(osmQuery)}&format=json`
|
||||
|
||||
const response = await fetch(searchUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'ClassroomCopilot-BatchGeocoder/1.0'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`SearXNG request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Check if we have results - the number_of_results field might be unreliable
|
||||
// so we check the results array directly
|
||||
if (!data.results || data.results.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No results returned from SearXNG'
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.results[0]
|
||||
|
||||
if (!result.latitude || !result.longitude) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Missing latitude or longitude in SearXNG response'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
coordinates: {
|
||||
latitude: parseFloat(result.latitude),
|
||||
longitude: parseFloat(result.longitude),
|
||||
boundingbox: result.boundingbox || [],
|
||||
geojson: result.geojson,
|
||||
osm: result.osm
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Geocoding error:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function geocodeAddressWithFallback(address: any, searxngUrl: string): Promise<{
|
||||
success: boolean
|
||||
coordinates?: {
|
||||
latitude: number
|
||||
longitude: number
|
||||
boundingbox: string[]
|
||||
geojson?: any
|
||||
osm?: any
|
||||
}
|
||||
error?: string
|
||||
}> {
|
||||
// Strategy 1: Try full address (street + town + county + postcode)
|
||||
if (address.street && address.town && address.county && address.postcode) {
|
||||
const fullQuery = `${address.street}, ${address.town}, ${address.county}, ${address.postcode}`
|
||||
console.log(`Trying full address: ${fullQuery}`)
|
||||
|
||||
const result = await geocodeAddress(fullQuery, searxngUrl)
|
||||
if (result.success && result.coordinates) {
|
||||
console.log('Full address geocoding successful')
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Try town + county + postcode
|
||||
if (address.town && address.county && address.postcode) {
|
||||
const mediumQuery = `${address.town}, ${address.county}, ${address.postcode}`
|
||||
console.log(`Trying medium address: ${mediumQuery}`)
|
||||
|
||||
const result = await geocodeAddress(mediumQuery, searxngUrl)
|
||||
if (result.success && result.coordinates) {
|
||||
console.log('Medium address geocoding successful')
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Try just postcode
|
||||
if (address.postcode) {
|
||||
console.log(`Trying postcode only: ${address.postcode}`)
|
||||
|
||||
const result = await geocodeAddress(address.postcode, searxngUrl)
|
||||
if (result.success && result.coordinates) {
|
||||
console.log('Postcode geocoding successful')
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 4: Try town + postcode
|
||||
if (address.town && address.postcode) {
|
||||
const simpleQuery = `${address.town}, ${address.postcode}`
|
||||
console.log(`Trying simple address: ${simpleQuery}`)
|
||||
|
||||
const result = await geocodeAddress(simpleQuery, searxngUrl)
|
||||
if (result.success && result.coordinates) {
|
||||
console.log('Simple address geocoding successful')
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// All strategies failed
|
||||
return {
|
||||
success: false,
|
||||
error: 'No coordinates found with any address combination'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
}
|
||||
|
||||
interface BatchGeocodingRequest {
|
||||
limit?: number
|
||||
force_refresh?: boolean
|
||||
institute_ids?: string[]
|
||||
}
|
||||
|
||||
interface GeocodingResult {
|
||||
institute_id: string
|
||||
success: boolean
|
||||
message: string
|
||||
coordinates?: {
|
||||
latitude: number
|
||||
longitude: number
|
||||
boundingbox: string[]
|
||||
geojson?: any
|
||||
osm?: any
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
serve(async (req: Request) => {
|
||||
// Handle CORS preflight requests
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
try {
|
||||
// Get environment variables
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')
|
||||
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_RATE_KEY')
|
||||
const searxngUrl = Deno.env.get('SEARXNG_URL') || 'https://search.kevlarai.com'
|
||||
|
||||
if (!supabaseUrl || !supabaseServiceKey) {
|
||||
throw new Error('Missing required environment variables')
|
||||
}
|
||||
|
||||
// Create Supabase client
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey)
|
||||
|
||||
// Parse request body
|
||||
const body: BatchGeocodingRequest = await req.json()
|
||||
const limit = body.limit || 10
|
||||
const forceRefresh = body.force_refresh || false
|
||||
|
||||
// Get institutes that need geocoding
|
||||
let query = supabase
|
||||
.from('institutes')
|
||||
.select('id, name, address, geo_coordinates')
|
||||
.not('import_id', 'is', null)
|
||||
|
||||
if (!forceRefresh) {
|
||||
// Only get institutes without coordinates or with empty coordinates
|
||||
query = query.or('geo_coordinates.is.null,geo_coordinates.eq.{}')
|
||||
}
|
||||
|
||||
if (body.institute_ids && body.institute_ids.length > 0) {
|
||||
query = query.in('id', body.institute_ids)
|
||||
}
|
||||
|
||||
const { data: institutes, error: fetchError } = await query.limit(limit)
|
||||
|
||||
if (fetchError) {
|
||||
throw new Error(`Failed to fetch institutes: ${fetchError.message}`)
|
||||
}
|
||||
|
||||
if (!institutes || institutes.length === 0) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: 'No institutes found that need geocoding',
|
||||
processed: 0
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`Processing ${institutes.length} institutes for geocoding`)
|
||||
|
||||
const results: GeocodingResult[] = []
|
||||
let successCount = 0
|
||||
let errorCount = 0
|
||||
|
||||
// Process institutes sequentially to avoid overwhelming the SearXNG service
|
||||
for (const institute of institutes) {
|
||||
try {
|
||||
const address = institute.address as any
|
||||
if (!address) {
|
||||
results.push({
|
||||
institute_id: institute.id,
|
||||
success: false,
|
||||
message: 'No address information available',
|
||||
error: 'Missing address data'
|
||||
})
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// Build search query from address components
|
||||
const addressParts = [
|
||||
address.street,
|
||||
address.town,
|
||||
address.county,
|
||||
address.postcode,
|
||||
address.country
|
||||
].filter(Boolean)
|
||||
|
||||
if (addressParts.length === 0) {
|
||||
results.push({
|
||||
institute_id: institute.id,
|
||||
success: false,
|
||||
message: 'No valid address components found',
|
||||
error: 'Empty address parts'
|
||||
})
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
const searchQuery = addressParts.join(', ')
|
||||
console.log(`Geocoding institute ${institute.id}: ${searchQuery}`)
|
||||
|
||||
// Query SearXNG for geocoding
|
||||
const geocodingResult = await geocodeAddress(searchQuery, searxngUrl)
|
||||
|
||||
if (geocodingResult.success && geocodingResult.coordinates) {
|
||||
// Update institute with geospatial coordinates
|
||||
const { error: updateError } = await supabase
|
||||
.from('institutes')
|
||||
.update({
|
||||
geo_coordinates: {
|
||||
latitude: geocodingResult.coordinates.latitude,
|
||||
longitude: geocodingResult.coordinates.longitude,
|
||||
boundingbox: geocodingResult.coordinates.boundingbox,
|
||||
geojson: geocodingResult.coordinates.geojson,
|
||||
osm: geocodingResult.coordinates.osm,
|
||||
search_query: searchQuery,
|
||||
geocoded_at: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
.eq('id', institute.id)
|
||||
|
||||
if (updateError) {
|
||||
throw new Error(`Failed to update institute: ${updateError.message}`)
|
||||
}
|
||||
|
||||
results.push({
|
||||
institute_id: institute.id,
|
||||
success: true,
|
||||
message: 'Successfully geocoded',
|
||||
coordinates: geocodingResult.coordinates
|
||||
})
|
||||
successCount++
|
||||
|
||||
// Log the successful geocoding
|
||||
await supabase
|
||||
.from('function_logs')
|
||||
.insert({
|
||||
file_id: null,
|
||||
step: 'batch_geocoding',
|
||||
message: 'Successfully geocoded institute address in batch',
|
||||
data: {
|
||||
institute_id: institute.id,
|
||||
search_query: searchQuery,
|
||||
coordinates: geocodingResult.coordinates
|
||||
}
|
||||
})
|
||||
|
||||
} else {
|
||||
results.push({
|
||||
institute_id: institute.id,
|
||||
success: false,
|
||||
message: 'Geocoding failed',
|
||||
error: geocodingResult.error || 'Unknown error'
|
||||
})
|
||||
errorCount++
|
||||
}
|
||||
|
||||
// Add a small delay between requests to be respectful to the SearXNG service
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error processing institute ${institute.id}:`, error)
|
||||
results.push({
|
||||
institute_id: institute.id,
|
||||
success: false,
|
||||
message: 'Processing error',
|
||||
error: error.message
|
||||
})
|
||||
errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Log the batch operation
|
||||
await supabase
|
||||
.from('function_logs')
|
||||
.insert({
|
||||
file_id: null,
|
||||
step: 'batch_geocoding_complete',
|
||||
message: 'Batch geocoding operation completed',
|
||||
data: {
|
||||
total_processed: institutes.length,
|
||||
successful: successCount,
|
||||
failed: errorCount,
|
||||
results: results
|
||||
}
|
||||
})
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: 'Batch geocoding completed',
|
||||
summary: {
|
||||
total_processed: institutes.length,
|
||||
successful: successCount,
|
||||
failed: errorCount
|
||||
},
|
||||
results: results
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in batch institute geocoder:', error)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Internal server error',
|
||||
details: error.message
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
async function geocodeAddress(searchQuery: string, searxngUrl: string): Promise<{
|
||||
success: boolean
|
||||
coordinates?: {
|
||||
latitude: number
|
||||
longitude: number
|
||||
boundingbox: string[]
|
||||
geojson?: any
|
||||
osm?: any
|
||||
}
|
||||
error?: string
|
||||
}> {
|
||||
try {
|
||||
// Format search query for OSM
|
||||
const osmQuery = `!osm ${searchQuery}`
|
||||
const searchUrl = `${searxngUrl}/search?q=${encodeURIComponent(osmQuery)}&format=json`
|
||||
|
||||
const response = await fetch(searchUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'ClassroomCopilot-BatchGeocoder/1.0'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`SearXNG request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Check if we have results - the number_of_results field might be unreliable
|
||||
// so we check the results array directly
|
||||
if (!data.results || data.results.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'No results returned from SearXNG'
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.results[0]
|
||||
|
||||
if (!result.latitude || !result.longitude) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Missing latitude or longitude in SearXNG response'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
coordinates: {
|
||||
latitude: parseFloat(result.latitude),
|
||||
longitude: parseFloat(result.longitude),
|
||||
boundingbox: result.boundingbox || [],
|
||||
geojson: result.geojson,
|
||||
osm: result.osm
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Geocoding error:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
// Example usage of Institute Geocoder functions
|
||||
// This file demonstrates how to integrate the geocoding functions in your frontend
|
||||
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
// Initialize Supabase client
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
|
||||
// Types for institute data
|
||||
interface Institute {
|
||||
id: string
|
||||
name: string
|
||||
address: {
|
||||
street?: string
|
||||
town?: string
|
||||
county?: string
|
||||
postcode?: string
|
||||
country?: string
|
||||
}
|
||||
geo_coordinates?: {
|
||||
latitude: number
|
||||
longitude: number
|
||||
boundingbox: string[]
|
||||
search_query: string
|
||||
geocoded_at: string
|
||||
}
|
||||
}
|
||||
|
||||
interface GeocodingResult {
|
||||
success: boolean
|
||||
message: string
|
||||
coordinates?: {
|
||||
latitude: number
|
||||
longitude: number
|
||||
boundingbox: string[]
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
// 1. Geocode a single institute
|
||||
export async function geocodeInstitute(instituteId: string): Promise<GeocodingResult> {
|
||||
try {
|
||||
const { data, error } = await supabase.functions.invoke('institute-geocoder', {
|
||||
body: { institute_id: instituteId }
|
||||
})
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Geocoding failed:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Geocoding failed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Batch geocode multiple institutes
|
||||
export async function batchGeocodeInstitutes(
|
||||
limit: number = 10,
|
||||
forceRefresh: boolean = false
|
||||
): Promise<any> {
|
||||
try {
|
||||
const { data, error } = await supabase.functions.invoke('institute-geocoder/batch', {
|
||||
body: {
|
||||
limit,
|
||||
force_refresh: forceRefresh
|
||||
}
|
||||
})
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Batch geocoding failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Get institutes that need geocoding
|
||||
export async function getInstitutesNeedingGeocoding(): Promise<Institute[]> {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('institutes')
|
||||
.select('id, name, address, geo_coordinates')
|
||||
.or('geo_coordinates.is.null,geo_coordinates.eq.{}')
|
||||
.not('import_id', 'is', null)
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
|
||||
return data || []
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch institutes:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Display institute on a map (example with Leaflet)
|
||||
export function displayInstituteOnMap(
|
||||
institute: Institute,
|
||||
mapElement: HTMLElement
|
||||
): void {
|
||||
if (!institute.geo_coordinates) {
|
||||
console.warn('Institute has no coordinates:', institute.name)
|
||||
return
|
||||
}
|
||||
|
||||
// This is a placeholder - you'd need to implement actual map rendering
|
||||
// For example, using Leaflet, Mapbox, or Google Maps
|
||||
const { latitude, longitude } = institute.geo_coordinates
|
||||
|
||||
console.log(`Displaying ${institute.name} at ${latitude}, ${longitude}`)
|
||||
|
||||
// Example map implementation:
|
||||
// const map = L.map(mapElement).setView([latitude, longitude], 13)
|
||||
// L.marker([latitude, longitude]).addTo(map).bindPopup(institute.name)
|
||||
}
|
||||
|
||||
// 5. React component example
|
||||
export function InstituteGeocoder() {
|
||||
const [institutes, setInstitutes] = useState<Institute[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [geocodingProgress, setGeocodingProgress] = useState(0)
|
||||
|
||||
// Load institutes that need geocoding
|
||||
useEffect(() => {
|
||||
loadInstitutes()
|
||||
}, [])
|
||||
|
||||
async function loadInstitutes() {
|
||||
const data = await getInstitutesNeedingGeocoding()
|
||||
setInstitutes(data)
|
||||
}
|
||||
|
||||
// Geocode all institutes
|
||||
async function geocodeAllInstitutes() {
|
||||
setLoading(true)
|
||||
setGeocodingProgress(0)
|
||||
|
||||
try {
|
||||
const result = await batchGeocodeInstitutes(institutes.length, false)
|
||||
|
||||
if (result.success) {
|
||||
setGeocodingProgress(100)
|
||||
// Reload institutes to show updated coordinates
|
||||
await loadInstitutes()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Batch geocoding failed:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Geocode single institute
|
||||
async function geocodeSingleInstitute(instituteId: string) {
|
||||
try {
|
||||
const result = await geocodeInstitute(instituteId)
|
||||
if (result.success) {
|
||||
// Reload institutes to show updated coordinates
|
||||
await loadInstitutes()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Single geocoding failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="institute-geocoder">
|
||||
<h2>Institute Geocoding</h2>
|
||||
|
||||
<div className="controls">
|
||||
<button
|
||||
onClick={geocodeAllInstitutes}
|
||||
disabled={loading || institutes.length === 0}
|
||||
>
|
||||
{loading ? 'Geocoding...' : `Geocode All (${institutes.length})`}
|
||||
</button>
|
||||
|
||||
{loading && (
|
||||
<div className="progress">
|
||||
<div
|
||||
className="progress-bar"
|
||||
style={{ width: `${geocodingProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="institutes-list">
|
||||
{institutes.map(institute => (
|
||||
<div key={institute.id} className="institute-item">
|
||||
<h3>{institute.name}</h3>
|
||||
<p>
|
||||
{institute.address.street && `${institute.address.street}, `}
|
||||
{institute.address.town && `${institute.address.town}, `}
|
||||
{institute.address.county && `${institute.address.county}, `}
|
||||
{institute.address.postcode}
|
||||
</p>
|
||||
|
||||
{institute.geo_coordinates ? (
|
||||
<div className="coordinates">
|
||||
<span>📍 {institute.geo_coordinates.latitude}, {institute.geo_coordinates.longitude}</span>
|
||||
<span>Geocoded: {new Date(institute.geo_coordinates.geocoded_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => geocodeSingleInstitute(institute.id)}
|
||||
disabled={loading}
|
||||
>
|
||||
Geocode
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 6. Utility functions for working with coordinates
|
||||
export class CoordinateUtils {
|
||||
// Calculate distance between two points (Haversine formula)
|
||||
static calculateDistance(
|
||||
lat1: number,
|
||||
lon1: number,
|
||||
lat2: number,
|
||||
lon2: number
|
||||
): number {
|
||||
const R = 6371 // Earth's radius in kilometers
|
||||
const dLat = this.toRadians(lat2 - lat1)
|
||||
const dLon = this.toRadians(lon2 - lon1)
|
||||
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
|
||||
Math.sin(dLon / 2) * Math.sin(dLon / 2)
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||||
return R * c
|
||||
}
|
||||
|
||||
// Convert degrees to radians
|
||||
private static toRadians(degrees: number): number {
|
||||
return degrees * (Math.PI / 180)
|
||||
}
|
||||
|
||||
// Check if coordinates are within a bounding box
|
||||
static isWithinBounds(
|
||||
lat: number,
|
||||
lon: number,
|
||||
bounds: [number, number, number, number] // [minLat, maxLat, minLon, maxLon]
|
||||
): boolean {
|
||||
return lat >= bounds[0] && lat <= bounds[1] &&
|
||||
lon >= bounds[2] && lon <= bounds[3]
|
||||
}
|
||||
|
||||
// Format coordinates for display
|
||||
static formatCoordinates(lat: number, lon: number): string {
|
||||
const latDir = lat >= 0 ? 'N' : 'S'
|
||||
const lonDir = lon >= 0 ? 'E' : 'W'
|
||||
return `${Math.abs(lat).toFixed(6)}°${latDir}, ${Math.abs(lon).toFixed(6)}°${lonDir}`
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Example of using coordinates in Neo4j queries
|
||||
export const neo4jQueries = {
|
||||
// Create institute node with location
|
||||
createInstituteWithLocation: `
|
||||
CREATE (i:Institute {
|
||||
id: $institute_id,
|
||||
name: $name,
|
||||
location: point({latitude: $latitude, longitude: $longitude})
|
||||
})
|
||||
RETURN i
|
||||
`,
|
||||
|
||||
// Find institutes within radius
|
||||
findInstitutesWithinRadius: `
|
||||
MATCH (i:Institute)
|
||||
WHERE distance(i.location, point({latitude: $centerLat, longitude: $centerLon})) < $radiusMeters
|
||||
RETURN i, distance(i.location, point({latitude: $centerLat, longitude: $centerLon})) as distance
|
||||
ORDER BY distance
|
||||
`,
|
||||
|
||||
// Find institutes in bounding box
|
||||
findInstitutesInBounds: `
|
||||
MATCH (i:Institute)
|
||||
WHERE i.location.latitude >= $minLat
|
||||
AND i.location.latitude <= $maxLat
|
||||
AND i.location.longitude >= $minLon
|
||||
AND i.location.longitude <= $maxLon
|
||||
RETURN i
|
||||
`
|
||||
}
|
||||
|
||||
export default {
|
||||
geocodeInstitute,
|
||||
batchGeocodeInstitutes,
|
||||
getInstitutesNeedingGeocoding,
|
||||
displayInstituteOnMap,
|
||||
InstituteGeocoder,
|
||||
CoordinateUtils,
|
||||
neo4jQueries
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
}
|
||||
|
||||
interface GeocodingRequest {
|
||||
institute_id: string
|
||||
address?: string
|
||||
street?: string
|
||||
town?: string
|
||||
county?: string
|
||||
postcode?: string
|
||||
country?: string
|
||||
}
|
||||
|
||||
interface SearXNGResponse {
|
||||
query: string
|
||||
number_of_results: number
|
||||
results: Array<{
|
||||
title: string
|
||||
longitude: string
|
||||
latitude: string
|
||||
boundingbox: string[]
|
||||
geojson?: any
|
||||
osm?: any
|
||||
}>
|
||||
}
|
||||
|
||||
interface GeocodingResult {
|
||||
success: boolean
|
||||
message: string
|
||||
coordinates?: {
|
||||
latitude: number
|
||||
longitude: number
|
||||
boundingbox: string[]
|
||||
geojson?: any
|
||||
osm?: any
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
serve(async (req: Request) => {
|
||||
// Handle CORS preflight requests
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
try {
|
||||
// Get environment variables
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')
|
||||
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
|
||||
const searxngUrl = Deno.env.get('SEARXNG_URL') || 'https://search.kevlarai.com'
|
||||
|
||||
if (!supabaseUrl || !supabaseServiceKey) {
|
||||
throw new Error('Missing required environment variables')
|
||||
}
|
||||
|
||||
// Create Supabase client
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey)
|
||||
|
||||
// Parse request body
|
||||
const body: GeocodingRequest = await req.json()
|
||||
|
||||
if (!body.institute_id) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'institute_id is required' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Get institute data from database
|
||||
const { data: institute, error: fetchError } = await supabase
|
||||
.from('institutes')
|
||||
.select('*')
|
||||
.eq('id', body.institute_id)
|
||||
.single()
|
||||
|
||||
if (fetchError || !institute) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Institute not found' }),
|
||||
{
|
||||
status: 404,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Build search query from address components
|
||||
let searchQuery = ''
|
||||
if (body.address) {
|
||||
searchQuery = body.address
|
||||
} else {
|
||||
const addressParts = [
|
||||
body.street,
|
||||
body.town,
|
||||
body.county,
|
||||
body.postcode,
|
||||
body.country
|
||||
].filter(Boolean)
|
||||
searchQuery = addressParts.join(', ')
|
||||
}
|
||||
|
||||
// If no search query provided, try to build from institute data
|
||||
if (!searchQuery && institute.address) {
|
||||
const address = institute.address as any
|
||||
const addressParts = [
|
||||
address.street,
|
||||
address.town,
|
||||
address.county,
|
||||
address.postcode,
|
||||
address.country
|
||||
].filter(Boolean)
|
||||
searchQuery = addressParts.join(', ')
|
||||
}
|
||||
|
||||
if (!searchQuery) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No address information available for geocoding' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Query SearXNG for geocoding
|
||||
const geocodingResult = await geocodeAddressWithFallback(institute.address, searxngUrl)
|
||||
|
||||
if (!geocodingResult.success) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Geocoding failed',
|
||||
details: geocodingResult.error
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Update institute with geospatial coordinates
|
||||
const { error: updateError } = await supabase
|
||||
.from('institutes')
|
||||
.update({
|
||||
geo_coordinates: {
|
||||
latitude: geocodingResult.coordinates!.latitude,
|
||||
longitude: geocodingResult.coordinates!.longitude,
|
||||
boundingbox: geocodingResult.coordinates!.boundingbox,
|
||||
geojson: geocodingResult.coordinates!.geojson,
|
||||
osm: geocodingResult.coordinates!.osm,
|
||||
search_query: searchQuery,
|
||||
geocoded_at: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
.eq('id', body.institute_id)
|
||||
|
||||
if (updateError) {
|
||||
throw new Error(`Failed to update institute: ${updateError.message}`)
|
||||
}
|
||||
|
||||
// Log the geocoding operation
|
||||
await supabase
|
||||
.from('function_logs')
|
||||
.insert({
|
||||
file_id: null,
|
||||
step: 'geocoding',
|
||||
message: 'Successfully geocoded institute address',
|
||||
data: {
|
||||
institute_id: body.institute_id,
|
||||
search_query: searchQuery,
|
||||
coordinates: geocodingResult.coordinates
|
||||
}
|
||||
})
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: 'Institute geocoded successfully',
|
||||
institute_id: body.institute_id,
|
||||
coordinates: geocodingResult.coordinates
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in institute geocoder:', error)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Internal server error',
|
||||
details: error.message
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
async function geocodeAddress(searchQuery: string, searxngUrl: string): Promise<GeocodingResult> {
|
||||
try {
|
||||
console.log(`Geocoding address: ${searchQuery}`)
|
||||
|
||||
// Build the SearXNG query
|
||||
const query = `!osm ${searchQuery}`
|
||||
const url = `${searxngUrl}/search?q=${encodeURIComponent(query)}&format=json`
|
||||
|
||||
console.log(`SearXNG URL: ${url}`)
|
||||
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`SearXNG request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data: SearXNGResponse = await response.json()
|
||||
console.log(`SearXNG response: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
// Check if we have results
|
||||
if (!data.results || data.results.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'No results returned from SearXNG',
|
||||
error: 'No results returned from SearXNG'
|
||||
}
|
||||
}
|
||||
|
||||
// Get the best result (first one)
|
||||
const bestResult = data.results[0]
|
||||
|
||||
if (!bestResult.latitude || !bestResult.longitude) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Result missing coordinates',
|
||||
error: 'Result missing coordinates'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Geocoding successful',
|
||||
coordinates: {
|
||||
latitude: parseFloat(bestResult.latitude),
|
||||
longitude: parseFloat(bestResult.longitude),
|
||||
boundingbox: bestResult.boundingbox || [],
|
||||
geojson: bestResult.geojson || null,
|
||||
osm: bestResult.osm || null
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in geocodeAddress:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Geocoding failed',
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function geocodeAddressWithFallback(address: any, searxngUrl: string): Promise<GeocodingResult> {
|
||||
// Strategy 1: Try full address (street + town + county + postcode)
|
||||
if (address.street && address.town && address.county && address.postcode) {
|
||||
const fullQuery = `${address.street}, ${address.town}, ${address.county}, ${address.postcode}`
|
||||
console.log(`Trying full address: ${fullQuery}`)
|
||||
|
||||
const result = await geocodeAddress(fullQuery, searxngUrl)
|
||||
if (result.success) {
|
||||
console.log('Full address geocoding successful')
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Try town + county + postcode
|
||||
if (address.town && address.county && address.postcode) {
|
||||
const mediumQuery = `${address.town}, ${address.county}, ${address.postcode}`
|
||||
console.log(`Trying medium address: ${mediumQuery}`)
|
||||
|
||||
const result = await geocodeAddress(mediumQuery, searxngUrl)
|
||||
if (result.success) {
|
||||
console.log('Medium address geocoding successful')
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Try just postcode
|
||||
if (address.postcode) {
|
||||
console.log(`Trying postcode only: ${address.postcode}`)
|
||||
|
||||
const result = await geocodeAddress(address.postcode, searxngUrl)
|
||||
if (result.success) {
|
||||
console.log('Postcode geocoding successful')
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 4: Try town + postcode
|
||||
if (address.town && address.postcode) {
|
||||
const simpleQuery = `${address.town}, ${address.postcode}`
|
||||
console.log(`Trying simple address: ${simpleQuery}`)
|
||||
|
||||
const result = await geocodeAddress(simpleQuery, searxngUrl)
|
||||
if (result.success) {
|
||||
console.log('Simple address geocoding successful')
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// All strategies failed
|
||||
return {
|
||||
success: false,
|
||||
message: 'All geocoding strategies failed',
|
||||
error: 'No coordinates found with any address combination'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Test script for institute geocoder functions
|
||||
// This can be run in the browser console or as a standalone test
|
||||
|
||||
interface TestCase {
|
||||
name: string
|
||||
address: string
|
||||
expected_coords?: {
|
||||
latitude: number
|
||||
longitude: number
|
||||
}
|
||||
}
|
||||
|
||||
const testCases: TestCase[] = [
|
||||
{
|
||||
name: "10 Downing Street, London",
|
||||
address: "10 Downing Street, London",
|
||||
expected_coords: {
|
||||
latitude: 51.5034878,
|
||||
longitude: -0.1276965
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Buckingham Palace, London",
|
||||
address: "Buckingham Palace, London",
|
||||
expected_coords: {
|
||||
latitude: 51.501364,
|
||||
longitude: -0.124432
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Big Ben, London",
|
||||
address: "Big Ben, London",
|
||||
expected_coords: {
|
||||
latitude: 51.499479,
|
||||
longitude: -0.124809
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
async function testGeocoding() {
|
||||
console.log("🧪 Starting Institute Geocoder Tests...")
|
||||
|
||||
for (const testCase of testCases) {
|
||||
console.log(`\n📍 Testing: ${testCase.name}`)
|
||||
|
||||
try {
|
||||
// Test the SearXNG service directly
|
||||
const searchQuery = `!osm ${testCase.address}`
|
||||
const searchUrl = `https://search.kevlarai.com/search?q=${encodeURIComponent(searchQuery)}&format=json`
|
||||
|
||||
console.log(`🔍 Searching: ${searchUrl}`)
|
||||
|
||||
const response = await fetch(searchUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log(`📊 Results: ${data.number_of_results} found`)
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
const result = data.results[0]
|
||||
const coords = {
|
||||
latitude: parseFloat(result.latitude),
|
||||
longitude: parseFloat(result.longitude)
|
||||
}
|
||||
|
||||
console.log(`✅ Coordinates: ${coords.latitude}, ${coords.longitude}`)
|
||||
|
||||
if (testCase.expected_coords) {
|
||||
const latDiff = Math.abs(coords.latitude - testCase.expected_coords.latitude)
|
||||
const lonDiff = Math.abs(coords.longitude - testCase.expected_coords.longitude)
|
||||
|
||||
if (latDiff < 0.01 && lonDiff < 0.01) {
|
||||
console.log(`🎯 Accuracy: High (within 0.01 degrees)`)
|
||||
} else if (latDiff < 0.1 && lonDiff < 0.1) {
|
||||
console.log(`🎯 Accuracy: Medium (within 0.1 degrees)`)
|
||||
} else {
|
||||
console.log(`⚠️ Accuracy: Low (difference > 0.1 degrees)`)
|
||||
}
|
||||
}
|
||||
|
||||
if (result.boundingbox) {
|
||||
console.log(`🗺️ Bounding Box: ${result.boundingbox.join(', ')}`)
|
||||
}
|
||||
|
||||
if (result.geojson) {
|
||||
console.log(`🗺️ GeoJSON: ${result.geojson.type} with ${result.geojson.coordinates?.[0]?.length || 0} points`)
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log(`❌ No results found`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Test failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n🏁 Testing completed!")
|
||||
}
|
||||
|
||||
// Test address parsing function
|
||||
function testAddressParsing() {
|
||||
console.log("\n🔧 Testing Address Parsing...")
|
||||
|
||||
const testAddresses = [
|
||||
{
|
||||
street: "10 Downing Street",
|
||||
town: "London",
|
||||
county: "Greater London",
|
||||
postcode: "SW1A 2AA",
|
||||
country: "United Kingdom"
|
||||
},
|
||||
{
|
||||
street: "Buckingham Palace",
|
||||
town: "London",
|
||||
county: "Greater London",
|
||||
postcode: "SW1A 1AA",
|
||||
country: "United Kingdom"
|
||||
}
|
||||
]
|
||||
|
||||
for (const addr of testAddresses) {
|
||||
const parts = [addr.street, addr.town, addr.county, addr.postcode, addr.country].filter(Boolean)
|
||||
const searchQuery = parts.join(', ')
|
||||
console.log(`📍 Address: ${searchQuery}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests if this script is executed directly
|
||||
if (typeof window !== 'undefined') {
|
||||
// Browser environment
|
||||
window.testGeocoding = testGeocoding
|
||||
window.testAddressParsing = testAddressParsing
|
||||
console.log("🧪 Institute Geocoder tests loaded. Run testGeocoding() or testAddressParsing() to test.")
|
||||
} else {
|
||||
// Node.js environment
|
||||
console.log("🧪 Institute Geocoder tests loaded.")
|
||||
}
|
||||
|
||||
export { testGeocoding, testAddressParsing }
|
||||
@@ -0,0 +1,94 @@
|
||||
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
|
||||
import * as jose from 'https://deno.land/x/[email protected]/index.ts'
|
||||
|
||||
console.log('main function started')
|
||||
|
||||
const JWT_SECRET = Deno.env.get('JWT_SECRET')
|
||||
const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true'
|
||||
|
||||
function getAuthToken(req: Request) {
|
||||
const authHeader = req.headers.get('authorization')
|
||||
if (!authHeader) {
|
||||
throw new Error('Missing authorization header')
|
||||
}
|
||||
const [bearer, token] = authHeader.split(' ')
|
||||
if (bearer !== 'Bearer') {
|
||||
throw new Error(`Auth header is not 'Bearer {token}'`)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
async function verifyJWT(jwt: string): Promise<boolean> {
|
||||
const encoder = new TextEncoder()
|
||||
const secretKey = encoder.encode(JWT_SECRET)
|
||||
try {
|
||||
await jose.jwtVerify(jwt, secretKey)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
serve(async (req: Request) => {
|
||||
if (req.method !== 'OPTIONS' && VERIFY_JWT) {
|
||||
try {
|
||||
const token = getAuthToken(req)
|
||||
const isValidJWT = await verifyJWT(token)
|
||||
|
||||
if (!isValidJWT) {
|
||||
return new Response(JSON.stringify({ msg: 'Invalid JWT' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return new Response(JSON.stringify({ msg: e.toString() }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const url = new URL(req.url)
|
||||
const { pathname } = url
|
||||
const path_parts = pathname.split('/')
|
||||
const service_name = path_parts[1]
|
||||
|
||||
if (!service_name || service_name === '') {
|
||||
const error = { msg: 'missing function name in request' }
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const servicePath = `/home/deno/functions/${service_name}`
|
||||
console.error(`serving the request with ${servicePath}`)
|
||||
|
||||
const memoryLimitMb = 150
|
||||
const workerTimeoutMs = 1 * 60 * 1000
|
||||
const noModuleCache = false
|
||||
const importMapPath = null
|
||||
const envVarsObj = Deno.env.toObject()
|
||||
const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]])
|
||||
|
||||
try {
|
||||
const worker = await EdgeRuntime.userWorkers.create({
|
||||
servicePath,
|
||||
memoryLimitMb,
|
||||
workerTimeoutMs,
|
||||
noModuleCache,
|
||||
importMapPath,
|
||||
envVars,
|
||||
})
|
||||
return await worker.fetch(req)
|
||||
} catch (e) {
|
||||
const error = { msg: e.toString() }
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user