Initial commit: Vuelato - buscador de vuelos
Some checks failed
ci / ci (22, ubuntu-latest) (push) Has been cancelled
Some checks failed
ci / ci (22, ubuntu-latest) (push) Has been cancelled
Nuxt 4 + Supabase + Flightics API. Incluye búsqueda de vuelos, inspiraciones, watchlist, tracking de precios y mapa interactivo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
22
server/api/tracking/[id].delete.ts
Normal file
22
server/api/tracking/[id].delete.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { serverSupabaseServiceRole, serverSupabaseClient } from '#supabase/server'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const client = await serverSupabaseClient(event)
|
||||
const { data: { user } } = await client.auth.getUser()
|
||||
if (!user) throw createError({ statusCode: 401, message: 'No autenticado' })
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
if (!id) throw createError({ statusCode: 400, message: 'ID requerido' })
|
||||
|
||||
const supabase = serverSupabaseServiceRole(event)
|
||||
|
||||
const { error } = await supabase
|
||||
.from('tracked_searches')
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
.eq('user_id', user.id)
|
||||
|
||||
if (error) throw createError({ statusCode: 500, message: error.message })
|
||||
|
||||
return { ok: true }
|
||||
})
|
||||
46
server/api/tracking/[id].patch.ts
Normal file
46
server/api/tracking/[id].patch.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { serverSupabaseServiceRole, serverSupabaseClient } from '#supabase/server'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const client = await serverSupabaseClient(event)
|
||||
const { data: { user } } = await client.auth.getUser()
|
||||
if (!user) throw createError({ statusCode: 401, message: 'No autenticado' })
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
if (!id) throw createError({ statusCode: 400, message: 'ID requerido' })
|
||||
|
||||
const body = await readBody(event)
|
||||
const allowed = ['name', 'interval_hours', 'is_active', 'expires_at', 'search_params', 'route_summary']
|
||||
const patch: Record<string, unknown> = {}
|
||||
|
||||
for (const key of allowed) {
|
||||
if (body[key] !== undefined) patch[key] = body[key]
|
||||
}
|
||||
|
||||
if (patch.interval_hours) {
|
||||
patch.interval_hours = Math.min(Math.max(patch.interval_hours as number, 6), 168)
|
||||
}
|
||||
|
||||
if (Object.keys(patch).length === 0) {
|
||||
throw createError({ statusCode: 400, message: 'Nada que actualizar' })
|
||||
}
|
||||
|
||||
// Si cambian los parametros de busqueda, forzar ejecucion inmediata
|
||||
if (patch.search_params) {
|
||||
patch.next_run_at = new Date().toISOString()
|
||||
}
|
||||
|
||||
const supabase = serverSupabaseServiceRole(event)
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('tracked_searches')
|
||||
.update(patch as never)
|
||||
.eq('id', id)
|
||||
.eq('user_id', user.id)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw createError({ statusCode: 500, message: error.message })
|
||||
if (!data) throw createError({ statusCode: 404, message: 'Busqueda no encontrada' })
|
||||
|
||||
return data
|
||||
})
|
||||
37
server/api/tracking/[id]/history.get.ts
Normal file
37
server/api/tracking/[id]/history.get.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { serverSupabaseServiceRole, serverSupabaseClient } from '#supabase/server'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const client = await serverSupabaseClient(event)
|
||||
const { data: { user } } = await client.auth.getUser()
|
||||
if (!user) throw createError({ statusCode: 401, message: 'No autenticado' })
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
if (!id) throw createError({ statusCode: 400, message: 'ID requerido' })
|
||||
|
||||
const query = getQuery(event)
|
||||
const days = Number(query.days) || 30
|
||||
|
||||
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString()
|
||||
|
||||
const supabase = serverSupabaseServiceRole(event)
|
||||
|
||||
const { data: search } = await supabase
|
||||
.from('tracked_searches')
|
||||
.select('id')
|
||||
.eq('id', id)
|
||||
.eq('user_id', user.id)
|
||||
.single()
|
||||
|
||||
if (!search) throw createError({ statusCode: 404, message: 'No encontrado' })
|
||||
|
||||
const { data: snapshots, error } = await supabase
|
||||
.from('price_snapshots')
|
||||
.select('cheapest_price, avg_price, median_price, total_results, recorded_at')
|
||||
.eq('tracked_search_id', id)
|
||||
.gte('recorded_at', since)
|
||||
.order('recorded_at', { ascending: true })
|
||||
|
||||
if (error) throw createError({ statusCode: 500, message: error.message })
|
||||
|
||||
return snapshots || []
|
||||
})
|
||||
36
server/api/tracking/[id]/runs.get.ts
Normal file
36
server/api/tracking/[id]/runs.get.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { serverSupabaseServiceRole, serverSupabaseClient } from '#supabase/server'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const client = await serverSupabaseClient(event)
|
||||
const { data: { user } } = await client.auth.getUser()
|
||||
if (!user) throw createError({ statusCode: 401, message: 'No autenticado' })
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
if (!id) throw createError({ statusCode: 400, message: 'ID requerido' })
|
||||
|
||||
const query = getQuery(event)
|
||||
const limit = Math.min(Number(query.limit) || 20, 100)
|
||||
const offset = Number(query.offset) || 0
|
||||
|
||||
const supabase = serverSupabaseServiceRole(event)
|
||||
|
||||
const { data: search } = await supabase
|
||||
.from('tracked_searches')
|
||||
.select('id')
|
||||
.eq('id', id)
|
||||
.eq('user_id', user.id)
|
||||
.single()
|
||||
|
||||
if (!search) throw createError({ statusCode: 404, message: 'No encontrado' })
|
||||
|
||||
const { data: runs, error } = await supabase
|
||||
.from('search_runs')
|
||||
.select('*')
|
||||
.eq('tracked_search_id', id)
|
||||
.order('created_at', { ascending: false })
|
||||
.range(offset, offset + limit - 1)
|
||||
|
||||
if (error) throw createError({ statusCode: 500, message: error.message })
|
||||
|
||||
return runs || []
|
||||
})
|
||||
33
server/api/tracking/index.get.ts
Normal file
33
server/api/tracking/index.get.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { serverSupabaseServiceRole, serverSupabaseClient } from '#supabase/server'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const client = await serverSupabaseClient(event)
|
||||
const { data: { user } } = await client.auth.getUser()
|
||||
if (!user) throw createError({ statusCode: 401, message: 'No autenticado' })
|
||||
|
||||
const supabase = serverSupabaseServiceRole(event)
|
||||
|
||||
const { data: searches, error } = await supabase
|
||||
.from('tracked_searches')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
if (error) throw createError({ statusCode: 500, message: error.message })
|
||||
|
||||
const searchesWithSnapshot = await Promise.all(
|
||||
((searches as Record<string, unknown>[]) || []).map(async (s) => {
|
||||
const { data: snapshot } = await supabase
|
||||
.from('price_snapshots')
|
||||
.select('cheapest_price, avg_price, median_price, total_results, recorded_at')
|
||||
.eq('tracked_search_id', s.id as string)
|
||||
.order('recorded_at', { ascending: false })
|
||||
.limit(1)
|
||||
.single()
|
||||
|
||||
return { ...s, latest_snapshot: snapshot || null }
|
||||
})
|
||||
)
|
||||
|
||||
return searchesWithSnapshot
|
||||
})
|
||||
36
server/api/tracking/index.post.ts
Normal file
36
server/api/tracking/index.post.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { serverSupabaseServiceRole, serverSupabaseClient } from '#supabase/server'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const client = await serverSupabaseClient(event)
|
||||
const { data: { user } } = await client.auth.getUser()
|
||||
if (!user) throw createError({ statusCode: 401, message: 'No autenticado' })
|
||||
|
||||
const body = await readBody(event)
|
||||
const { name, searchParams, routeSummary, intervalHours, expiresAt } = body
|
||||
|
||||
if (!name || !searchParams || !routeSummary) {
|
||||
throw createError({ statusCode: 400, message: 'Faltan campos obligatorios: name, searchParams, routeSummary' })
|
||||
}
|
||||
|
||||
const interval = Math.min(Math.max(intervalHours || 24, 6), 168)
|
||||
|
||||
const supabase = serverSupabaseServiceRole(event)
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('tracked_searches')
|
||||
.insert({
|
||||
user_id: user.id,
|
||||
name,
|
||||
search_params: searchParams,
|
||||
route_summary: routeSummary,
|
||||
interval_hours: interval,
|
||||
next_run_at: new Date().toISOString(),
|
||||
expires_at: expiresAt || null
|
||||
} as never)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw createError({ statusCode: 500, message: error.message })
|
||||
|
||||
return data
|
||||
})
|
||||
Reference in New Issue
Block a user