Før simulatorer
This commit is contained in:
parent
a508db6071
commit
9af374adda
5 changed files with 249 additions and 188 deletions
419
backend/main.py
419
backend/main.py
|
|
@ -816,6 +816,7 @@ FACILITY_VIEW_SEARCH_FIELDS = {
|
||||||
'course_statuses', 'weather_forecast',
|
'course_statuses', 'weather_forecast',
|
||||||
}
|
}
|
||||||
FACILITY_VIEW_PLACE_FIELDS = FACILITY_VIEW_SEARCH_FIELDS | {
|
FACILITY_VIEW_PLACE_FIELDS = FACILITY_VIEW_SEARCH_FIELDS | {
|
||||||
|
'has_golfpakker',
|
||||||
'greenfee', 'standard_medlemskap', 'total_hole_count', 'hole_par_counts',
|
'greenfee', 'standard_medlemskap', 'total_hole_count', 'hole_par_counts',
|
||||||
'shortest_hole_meters', 'longest_hole_meters',
|
'shortest_hole_meters', 'longest_hole_meters',
|
||||||
}
|
}
|
||||||
|
|
@ -1063,6 +1064,29 @@ async def resolve_cooperating_club_slugs(
|
||||||
return resolved_slugs
|
return resolved_slugs
|
||||||
|
|
||||||
|
|
||||||
|
async def ensure_public_query_indexes(conn) -> None:
|
||||||
|
await conn.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS facilities_is_published_name_idx
|
||||||
|
ON facilities (is_published, name)
|
||||||
|
""")
|
||||||
|
await conn.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS courses_facility_id_idx
|
||||||
|
ON courses (facility_id)
|
||||||
|
""")
|
||||||
|
await conn.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS courses_facility_id_main_idx
|
||||||
|
ON courses (facility_id, is_main_course DESC, id ASC)
|
||||||
|
""")
|
||||||
|
await conn.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS holes_course_id_idx
|
||||||
|
ON holes (course_id)
|
||||||
|
""")
|
||||||
|
await conn.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS facility_weather_forecast_facility_day_idx
|
||||||
|
ON facility_weather_forecast (facility_id, day_offset)
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
async def get_table_columns(conn, table_name: str, schema_name: str = "public") -> set[str]:
|
async def get_table_columns(conn, table_name: str, schema_name: str = "public") -> set[str]:
|
||||||
rows = await conn.fetch(
|
rows = await conn.fetch(
|
||||||
"""
|
"""
|
||||||
|
|
@ -1779,7 +1803,7 @@ async def get_published_facility_by_slug(conn, slug: str):
|
||||||
SELECT id, name, slug
|
SELECT id, name, slug
|
||||||
FROM facilities
|
FROM facilities
|
||||||
WHERE slug = $1
|
WHERE slug = $1
|
||||||
AND COALESCE(is_published, TRUE) = TRUE
|
AND is_published IS DISTINCT FROM FALSE
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""",
|
""",
|
||||||
slug,
|
slug,
|
||||||
|
|
@ -2218,6 +2242,7 @@ async def lifespan(app: FastAPI):
|
||||||
await ensure_scrape_jobs_table(conn)
|
await ensure_scrape_jobs_table(conn)
|
||||||
await ensure_course_status_history_table(conn)
|
await ensure_course_status_history_table(conn)
|
||||||
await ensure_weather_forecast_table(conn)
|
await ensure_weather_forecast_table(conn)
|
||||||
|
await ensure_public_query_indexes(conn)
|
||||||
app.state.weather_sync_stop_event = asyncio.Event()
|
app.state.weather_sync_stop_event = asyncio.Event()
|
||||||
app.state.weather_sync_task = asyncio.create_task(
|
app.state.weather_sync_task = asyncio.create_task(
|
||||||
weather_sync_loop(app.state.pool, app.state.weather_sync_stop_event)
|
weather_sync_loop(app.state.pool, app.state.weather_sync_stop_event)
|
||||||
|
|
@ -2897,142 +2922,158 @@ async def upsert_facility_rating(request: Request, payload: FacilityRatingUpsert
|
||||||
|
|
||||||
def build_public_facilities_query(view: str | None) -> tuple[str, set[str] | None]:
|
def build_public_facilities_query(view: str | None) -> tuple[str, set[str] | None]:
|
||||||
normalized_view = (view or "").strip().lower()
|
normalized_view = (view or "").strip().lower()
|
||||||
|
published_facilities_cte = """
|
||||||
course_statuses_sql = """
|
published_facilities AS (
|
||||||
(
|
SELECT *
|
||||||
SELECT jsonb_agg(cs) FROM (
|
FROM facilities
|
||||||
SELECT id, name, status FROM courses
|
WHERE is_published IS DISTINCT FROM FALSE
|
||||||
WHERE facility_id = f.id AND status != 'finnes_ingen_bane_to'
|
)
|
||||||
ORDER BY is_main_course DESC, id ASC
|
|
||||||
) cs
|
|
||||||
) as course_statuses
|
|
||||||
"""
|
"""
|
||||||
weather_compact_sql = """
|
course_statuses_cte = """
|
||||||
(
|
course_statuses AS (
|
||||||
SELECT jsonb_agg(w_data ORDER BY w_data.day_offset ASC) FROM (
|
SELECT
|
||||||
SELECT
|
c.facility_id,
|
||||||
day_offset,
|
jsonb_agg(
|
||||||
dry_daylight
|
jsonb_build_object(
|
||||||
FROM facility_weather_forecast
|
'id', c.id,
|
||||||
WHERE facility_id = f.id
|
'name', c.name,
|
||||||
ORDER BY day_offset ASC
|
'status', c.status
|
||||||
) w_data
|
)
|
||||||
) as weather_forecast
|
ORDER BY c.is_main_course DESC, c.id ASC
|
||||||
|
) AS course_statuses
|
||||||
|
FROM courses c
|
||||||
|
JOIN published_facilities pf ON pf.id = c.facility_id
|
||||||
|
WHERE c.status != 'finnes_ingen_bane_to'
|
||||||
|
GROUP BY c.facility_id
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
weather_full_sql = """
|
weather_compact_cte = """
|
||||||
(
|
weather_compact AS (
|
||||||
SELECT jsonb_agg(w_data ORDER BY w_data.day_offset ASC) FROM (
|
SELECT
|
||||||
SELECT
|
facility_id,
|
||||||
forecast_date,
|
jsonb_agg(
|
||||||
day_offset,
|
jsonb_build_object(
|
||||||
dry_all_day,
|
'day_offset', day_offset,
|
||||||
dry_daylight,
|
'dry_daylight', dry_daylight
|
||||||
precip_mm,
|
)
|
||||||
precip_probability_max,
|
ORDER BY day_offset ASC
|
||||||
daylight_precip_mm,
|
) AS weather_forecast
|
||||||
daylight_precip_probability_max,
|
FROM facility_weather_forecast
|
||||||
confidence,
|
WHERE facility_id IN (SELECT id FROM published_facilities)
|
||||||
source_updated_at,
|
GROUP BY facility_id
|
||||||
source_expires_at,
|
)
|
||||||
calculated_at
|
"""
|
||||||
FROM facility_weather_forecast
|
weather_full_cte = """
|
||||||
WHERE facility_id = f.id
|
weather_full AS (
|
||||||
ORDER BY day_offset ASC
|
SELECT
|
||||||
) w_data
|
facility_id,
|
||||||
) as weather_forecast
|
jsonb_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'forecast_date', forecast_date,
|
||||||
|
'day_offset', day_offset,
|
||||||
|
'dry_all_day', dry_all_day,
|
||||||
|
'dry_daylight', dry_daylight,
|
||||||
|
'precip_mm', precip_mm,
|
||||||
|
'precip_probability_max', precip_probability_max,
|
||||||
|
'daylight_precip_mm', daylight_precip_mm,
|
||||||
|
'daylight_precip_probability_max', daylight_precip_probability_max,
|
||||||
|
'confidence', confidence,
|
||||||
|
'source_updated_at', source_updated_at,
|
||||||
|
'source_expires_at', source_expires_at,
|
||||||
|
'calculated_at', calculated_at
|
||||||
|
)
|
||||||
|
ORDER BY day_offset ASC
|
||||||
|
) AS weather_forecast
|
||||||
|
FROM facility_weather_forecast
|
||||||
|
WHERE facility_id IN (SELECT id FROM published_facilities)
|
||||||
|
GROUP BY facility_id
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
hole_counts_cte = """
|
||||||
|
hole_counts AS (
|
||||||
|
SELECT
|
||||||
|
c.facility_id,
|
||||||
|
COUNT(h.id) AS total_hole_count,
|
||||||
|
jsonb_build_object(
|
||||||
|
'3', COUNT(*) FILTER (WHERE h.par = 3),
|
||||||
|
'4', COUNT(*) FILTER (WHERE h.par = 4),
|
||||||
|
'5', COUNT(*) FILTER (WHERE h.par = 5),
|
||||||
|
'6', COUNT(*) FILTER (WHERE h.par = 6)
|
||||||
|
) AS hole_par_counts
|
||||||
|
FROM courses c
|
||||||
|
JOIN holes h ON h.course_id = c.id
|
||||||
|
JOIN published_facilities pf ON pf.id = c.facility_id
|
||||||
|
GROUP BY c.facility_id
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
hole_lengths_cte = """
|
||||||
|
hole_lengths AS (
|
||||||
|
SELECT
|
||||||
|
c.facility_id,
|
||||||
|
MIN((length_value.value)::int) AS shortest_hole_meters,
|
||||||
|
MAX((length_value.value)::int) AS longest_hole_meters
|
||||||
|
FROM courses c
|
||||||
|
JOIN holes h ON h.course_id = c.id
|
||||||
|
JOIN published_facilities pf ON pf.id = c.facility_id
|
||||||
|
CROSS JOIN LATERAL jsonb_each_text(COALESCE(h.lengths, '{}'::jsonb)) AS length_value(key, value)
|
||||||
|
WHERE length_value.key IN ('kortest', 'kort', 'mellomkort', 'mellomlang', 'lang', 'lengst')
|
||||||
|
AND length_value.value ~ '^[0-9]+$'
|
||||||
|
AND (length_value.value)::int BETWEEN 30 AND 900
|
||||||
|
GROUP BY c.facility_id
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
has_golfpakker_sql = """
|
has_golfpakker_sql = """
|
||||||
CASE
|
CASE
|
||||||
WHEN jsonb_typeof(f.golfpakker) = 'array' AND jsonb_array_length(f.golfpakker) > 0 THEN TRUE
|
WHEN jsonb_typeof(pf.golfpakker) = 'array' AND jsonb_array_length(pf.golfpakker) > 0 THEN TRUE
|
||||||
WHEN NULLIF(BTRIM(COALESCE(f.golfpakker_url, '')), '') IS NOT NULL THEN TRUE
|
WHEN NULLIF(BTRIM(COALESCE(pf.golfpakker_url, '')), '') IS NOT NULL THEN TRUE
|
||||||
ELSE FALSE
|
ELSE FALSE
|
||||||
END as has_golfpakker
|
END AS has_golfpakker
|
||||||
"""
|
|
||||||
total_hole_count_sql = """
|
|
||||||
(
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM holes h
|
|
||||||
JOIN courses c ON c.id = h.course_id
|
|
||||||
WHERE c.facility_id = f.id
|
|
||||||
) as total_hole_count
|
|
||||||
"""
|
|
||||||
hole_par_counts_sql = """
|
|
||||||
(
|
|
||||||
SELECT jsonb_build_object(
|
|
||||||
'3', COUNT(*) FILTER (WHERE h.par = 3),
|
|
||||||
'4', COUNT(*) FILTER (WHERE h.par = 4),
|
|
||||||
'5', COUNT(*) FILTER (WHERE h.par = 5),
|
|
||||||
'6', COUNT(*) FILTER (WHERE h.par = 6)
|
|
||||||
)
|
|
||||||
FROM holes h
|
|
||||||
JOIN courses c ON c.id = h.course_id
|
|
||||||
WHERE c.facility_id = f.id
|
|
||||||
) as hole_par_counts
|
|
||||||
"""
|
|
||||||
shortest_hole_sql = """
|
|
||||||
(
|
|
||||||
SELECT MIN((length_value.value)::int)
|
|
||||||
FROM holes h
|
|
||||||
JOIN courses c ON c.id = h.course_id
|
|
||||||
CROSS JOIN LATERAL jsonb_each_text(COALESCE(h.lengths, '{}'::jsonb)) AS length_value(key, value)
|
|
||||||
WHERE c.facility_id = f.id
|
|
||||||
AND length_value.key IN ('kortest', 'kort', 'mellomkort', 'mellomlang', 'lang', 'lengst')
|
|
||||||
AND length_value.value ~ '^[0-9]+$'
|
|
||||||
AND (length_value.value)::int BETWEEN 30 AND 900
|
|
||||||
) as shortest_hole_meters
|
|
||||||
"""
|
|
||||||
longest_hole_sql = """
|
|
||||||
(
|
|
||||||
SELECT MAX((length_value.value)::int)
|
|
||||||
FROM holes h
|
|
||||||
JOIN courses c ON c.id = h.course_id
|
|
||||||
CROSS JOIN LATERAL jsonb_each_text(COALESCE(h.lengths, '{}'::jsonb)) AS length_value(key, value)
|
|
||||||
WHERE c.facility_id = f.id
|
|
||||||
AND length_value.key IN ('kortest', 'kort', 'mellomkort', 'mellomlang', 'lang', 'lengst')
|
|
||||||
AND length_value.value ~ '^[0-9]+$'
|
|
||||||
AND (length_value.value)::int BETWEEN 30 AND 900
|
|
||||||
) as longest_hole_meters
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if normalized_view in {"search", "home"}:
|
if normalized_view in {"search", "home"}:
|
||||||
return (
|
return (
|
||||||
f"""
|
f"""
|
||||||
|
WITH
|
||||||
|
{published_facilities_cte},
|
||||||
|
{course_statuses_cte},
|
||||||
|
{weather_compact_cte}
|
||||||
SELECT
|
SELECT
|
||||||
f.id,
|
pf.id,
|
||||||
f.slug,
|
pf.slug,
|
||||||
f.name,
|
pf.name,
|
||||||
f.architect,
|
pf.architect,
|
||||||
f.description,
|
pf.description,
|
||||||
f.city,
|
pf.city,
|
||||||
f.county,
|
pf.county,
|
||||||
f.banetype,
|
pf.banetype,
|
||||||
f.image_url,
|
pf.image_url,
|
||||||
f.phone,
|
pf.phone,
|
||||||
f.website_url,
|
pf.website_url,
|
||||||
f.golfbox_booking_url,
|
pf.golfbox_booking_url,
|
||||||
f.golfbox_tournament_url,
|
pf.golfbox_tournament_url,
|
||||||
f.weather_url,
|
pf.weather_url,
|
||||||
f.lat,
|
pf.lat,
|
||||||
f.lng,
|
pf.lng,
|
||||||
f.golfamore,
|
pf.golfamore,
|
||||||
f.golfamore_url,
|
pf.golfamore_url,
|
||||||
f.nsg_url,
|
pf.nsg_url,
|
||||||
{has_golfpakker_sql},
|
{has_golfpakker_sql},
|
||||||
f.vtg_pris,
|
pf.vtg_pris,
|
||||||
f.vtg_lenke,
|
pf.vtg_lenke,
|
||||||
f.vtg_beskrivelse,
|
pf.vtg_beskrivelse,
|
||||||
f.amenities,
|
pf.amenities,
|
||||||
f.golfamore_data,
|
pf.golfamore_data,
|
||||||
f.nsg_data,
|
pf.nsg_data,
|
||||||
f.vtg_datoer,
|
pf.vtg_datoer,
|
||||||
f.footnote,
|
pf.footnote,
|
||||||
f.footnote_updated_at,
|
pf.footnote_updated_at,
|
||||||
f.status_updated_at,
|
pf.status_updated_at,
|
||||||
{course_statuses_sql},
|
COALESCE(cs.course_statuses, '[]'::jsonb) AS course_statuses,
|
||||||
{weather_compact_sql}
|
COALESCE(wc.weather_forecast, '[]'::jsonb) AS weather_forecast
|
||||||
FROM facilities f
|
FROM published_facilities pf
|
||||||
WHERE COALESCE(f.is_published, TRUE) = TRUE
|
LEFT JOIN course_statuses cs ON cs.facility_id = pf.id
|
||||||
ORDER BY f.name ASC
|
LEFT JOIN weather_compact wc ON wc.facility_id = pf.id
|
||||||
|
ORDER BY pf.name ASC
|
||||||
""",
|
""",
|
||||||
FACILITY_VIEW_SEARCH_FIELDS,
|
FACILITY_VIEW_SEARCH_FIELDS,
|
||||||
)
|
)
|
||||||
|
|
@ -3040,48 +3081,57 @@ def build_public_facilities_query(view: str | None) -> tuple[str, set[str] | Non
|
||||||
if normalized_view == "place":
|
if normalized_view == "place":
|
||||||
return (
|
return (
|
||||||
f"""
|
f"""
|
||||||
|
WITH
|
||||||
|
{published_facilities_cte},
|
||||||
|
{course_statuses_cte},
|
||||||
|
{weather_compact_cte},
|
||||||
|
{hole_counts_cte},
|
||||||
|
{hole_lengths_cte}
|
||||||
SELECT
|
SELECT
|
||||||
f.id,
|
pf.id,
|
||||||
f.slug,
|
pf.slug,
|
||||||
f.name,
|
pf.name,
|
||||||
f.architect,
|
pf.architect,
|
||||||
f.description,
|
pf.description,
|
||||||
f.city,
|
pf.city,
|
||||||
f.county,
|
pf.county,
|
||||||
f.banetype,
|
pf.banetype,
|
||||||
f.image_url,
|
pf.image_url,
|
||||||
f.phone,
|
pf.phone,
|
||||||
f.website_url,
|
pf.website_url,
|
||||||
f.golfbox_booking_url,
|
pf.golfbox_booking_url,
|
||||||
f.golfbox_tournament_url,
|
pf.golfbox_tournament_url,
|
||||||
f.weather_url,
|
pf.weather_url,
|
||||||
f.lat,
|
pf.lat,
|
||||||
f.lng,
|
pf.lng,
|
||||||
f.golfamore,
|
pf.golfamore,
|
||||||
f.golfamore_url,
|
pf.golfamore_url,
|
||||||
f.nsg_url,
|
pf.nsg_url,
|
||||||
{has_golfpakker_sql},
|
{has_golfpakker_sql},
|
||||||
f.greenfee,
|
pf.greenfee,
|
||||||
f.standard_medlemskap,
|
pf.standard_medlemskap,
|
||||||
f.vtg_pris,
|
pf.vtg_pris,
|
||||||
f.vtg_lenke,
|
pf.vtg_lenke,
|
||||||
f.vtg_beskrivelse,
|
pf.vtg_beskrivelse,
|
||||||
f.amenities,
|
pf.amenities,
|
||||||
f.golfamore_data,
|
pf.golfamore_data,
|
||||||
f.nsg_data,
|
pf.nsg_data,
|
||||||
f.vtg_datoer,
|
pf.vtg_datoer,
|
||||||
f.footnote,
|
pf.footnote,
|
||||||
f.footnote_updated_at,
|
pf.footnote_updated_at,
|
||||||
f.status_updated_at,
|
pf.status_updated_at,
|
||||||
{course_statuses_sql},
|
COALESCE(cs.course_statuses, '[]'::jsonb) AS course_statuses,
|
||||||
{total_hole_count_sql},
|
COALESCE(hc.total_hole_count, 0) AS total_hole_count,
|
||||||
{hole_par_counts_sql},
|
COALESCE(hc.hole_par_counts, jsonb_build_object('3', 0, '4', 0, '5', 0, '6', 0)) AS hole_par_counts,
|
||||||
{shortest_hole_sql},
|
hl.shortest_hole_meters,
|
||||||
{longest_hole_sql},
|
hl.longest_hole_meters,
|
||||||
{weather_compact_sql}
|
COALESCE(wc.weather_forecast, '[]'::jsonb) AS weather_forecast
|
||||||
FROM facilities f
|
FROM published_facilities pf
|
||||||
WHERE COALESCE(f.is_published, TRUE) = TRUE
|
LEFT JOIN course_statuses cs ON cs.facility_id = pf.id
|
||||||
ORDER BY f.name ASC
|
LEFT JOIN weather_compact wc ON wc.facility_id = pf.id
|
||||||
|
LEFT JOIN hole_counts hc ON hc.facility_id = pf.id
|
||||||
|
LEFT JOIN hole_lengths hl ON hl.facility_id = pf.id
|
||||||
|
ORDER BY pf.name ASC
|
||||||
""",
|
""",
|
||||||
FACILITY_VIEW_PLACE_FIELDS,
|
FACILITY_VIEW_PLACE_FIELDS,
|
||||||
)
|
)
|
||||||
|
|
@ -3103,7 +3153,7 @@ def build_public_facilities_query(view: str | None) -> tuple[str, set[str] | Non
|
||||||
f.navn_rimeligste_alternativ,
|
f.navn_rimeligste_alternativ,
|
||||||
f.rimeligste_alternativ
|
f.rimeligste_alternativ
|
||||||
FROM facilities f
|
FROM facilities f
|
||||||
WHERE COALESCE(f.is_published, TRUE) = TRUE
|
WHERE f.is_published IS DISTINCT FROM FALSE
|
||||||
ORDER BY f.name ASC
|
ORDER BY f.name ASC
|
||||||
""",
|
""",
|
||||||
FACILITY_VIEW_MEMBERSHIP_FIELDS,
|
FACILITY_VIEW_MEMBERSHIP_FIELDS,
|
||||||
|
|
@ -3127,7 +3177,7 @@ def build_public_facilities_query(view: str | None) -> tuple[str, set[str] | Non
|
||||||
f.vtg_datoer,
|
f.vtg_datoer,
|
||||||
f.vtg_updated_at
|
f.vtg_updated_at
|
||||||
FROM facilities f
|
FROM facilities f
|
||||||
WHERE COALESCE(f.is_published, TRUE) = TRUE
|
WHERE f.is_published IS DISTINCT FROM FALSE
|
||||||
ORDER BY f.name ASC
|
ORDER BY f.name ASC
|
||||||
""",
|
""",
|
||||||
FACILITY_VIEW_VTG_FIELDS,
|
FACILITY_VIEW_VTG_FIELDS,
|
||||||
|
|
@ -3144,7 +3194,7 @@ def build_public_facilities_query(view: str | None) -> tuple[str, set[str] | Non
|
||||||
f.county,
|
f.county,
|
||||||
f.ngf_number
|
f.ngf_number
|
||||||
FROM facilities f
|
FROM facilities f
|
||||||
WHERE COALESCE(f.is_published, TRUE) = TRUE
|
WHERE f.is_published IS DISTINCT FROM FALSE
|
||||||
ORDER BY f.name ASC
|
ORDER BY f.name ASC
|
||||||
""",
|
""",
|
||||||
FACILITY_VIEW_CLUBNUMBERS_FIELDS,
|
FACILITY_VIEW_CLUBNUMBERS_FIELDS,
|
||||||
|
|
@ -3158,7 +3208,7 @@ def build_public_facilities_query(view: str | None) -> tuple[str, set[str] | Non
|
||||||
f.status_updated_at,
|
f.status_updated_at,
|
||||||
f.vtg_updated_at
|
f.vtg_updated_at
|
||||||
FROM facilities f
|
FROM facilities f
|
||||||
WHERE COALESCE(f.is_published, TRUE) = TRUE
|
WHERE f.is_published IS DISTINCT FROM FALSE
|
||||||
ORDER BY f.name ASC
|
ORDER BY f.name ASC
|
||||||
""",
|
""",
|
||||||
FACILITY_VIEW_SITEMAP_FIELDS,
|
FACILITY_VIEW_SITEMAP_FIELDS,
|
||||||
|
|
@ -3171,7 +3221,7 @@ def build_public_facilities_query(view: str | None) -> tuple[str, set[str] | Non
|
||||||
f.slug,
|
f.slug,
|
||||||
f.name
|
f.name
|
||||||
FROM facilities f
|
FROM facilities f
|
||||||
WHERE COALESCE(f.is_published, TRUE) = TRUE
|
WHERE f.is_published IS DISTINCT FROM FALSE
|
||||||
ORDER BY f.name ASC
|
ORDER BY f.name ASC
|
||||||
""",
|
""",
|
||||||
FACILITY_VIEW_ALIASES_FIELDS,
|
FACILITY_VIEW_ALIASES_FIELDS,
|
||||||
|
|
@ -3179,17 +3229,26 @@ def build_public_facilities_query(view: str | None) -> tuple[str, set[str] | Non
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f"""
|
f"""
|
||||||
|
WITH
|
||||||
|
{published_facilities_cte},
|
||||||
|
{course_statuses_cte},
|
||||||
|
{weather_full_cte},
|
||||||
|
{hole_counts_cte},
|
||||||
|
{hole_lengths_cte}
|
||||||
SELECT
|
SELECT
|
||||||
f.*,
|
pf.*,
|
||||||
{course_statuses_sql},
|
COALESCE(cs.course_statuses, '[]'::jsonb) AS course_statuses,
|
||||||
{total_hole_count_sql},
|
COALESCE(hc.total_hole_count, 0) AS total_hole_count,
|
||||||
{hole_par_counts_sql},
|
COALESCE(hc.hole_par_counts, jsonb_build_object('3', 0, '4', 0, '5', 0, '6', 0)) AS hole_par_counts,
|
||||||
{shortest_hole_sql},
|
hl.shortest_hole_meters,
|
||||||
{longest_hole_sql},
|
hl.longest_hole_meters,
|
||||||
{weather_full_sql}
|
COALESCE(wf.weather_forecast, '[]'::jsonb) AS weather_forecast
|
||||||
FROM facilities f
|
FROM published_facilities pf
|
||||||
WHERE COALESCE(f.is_published, TRUE) = TRUE
|
LEFT JOIN course_statuses cs ON cs.facility_id = pf.id
|
||||||
ORDER BY f.name ASC
|
LEFT JOIN hole_counts hc ON hc.facility_id = pf.id
|
||||||
|
LEFT JOIN hole_lengths hl ON hl.facility_id = pf.id
|
||||||
|
LEFT JOIN weather_full wf ON wf.facility_id = pf.id
|
||||||
|
ORDER BY pf.name ASC
|
||||||
""",
|
""",
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
@ -3266,7 +3325,7 @@ async def get_facility(slug: str, response: Response):
|
||||||
) as weather_forecast
|
) as weather_forecast
|
||||||
FROM facilities f
|
FROM facilities f
|
||||||
WHERE f.slug = $1
|
WHERE f.slug = $1
|
||||||
AND COALESCE(f.is_published, TRUE) = TRUE
|
AND f.is_published IS DISTINCT FROM FALSE
|
||||||
""", slug)
|
""", slug)
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { API_URL } from "@/config/constants";
|
import { API_URL } from "@/config/constants";
|
||||||
import { getAvailablePlaceConfigs, slugify } from "@/app/facilityData";
|
import { getAvailablePlaceConfigs, slugify } from "@/app/facilityData";
|
||||||
|
import { cache } from "react";
|
||||||
|
|
||||||
type FacilityAliasSource = {
|
type FacilityAliasSource = {
|
||||||
slug?: string | null;
|
slug?: string | null;
|
||||||
|
|
@ -114,9 +115,9 @@ function buildFacilityAliasMap(facilities: FacilityAliasSource[]) {
|
||||||
return Object.fromEntries(aliases);
|
return Object.fromEntries(aliases);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFacilityAliasMap() {
|
const getFacilityAliasMap = cache(async () => {
|
||||||
const response = await fetch(`${API_URL}/facilities?view=aliases`, {
|
const response = await fetch(`${API_URL}/facilities?view=aliases`, {
|
||||||
next: { revalidate: 3600 },
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -126,7 +127,7 @@ async function getFacilityAliasMap() {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const facilities = Array.isArray(data) ? (data as FacilityAliasSource[]) : [];
|
const facilities = Array.isArray(data) ? (data as FacilityAliasSource[]) : [];
|
||||||
return buildFacilityAliasMap(facilities);
|
return buildFacilityAliasMap(facilities);
|
||||||
}
|
});
|
||||||
|
|
||||||
export async function resolveFacilityAlias(alias: string) {
|
export async function resolveFacilityAlias(alias: string) {
|
||||||
const normalizedAlias = slugify(alias);
|
const normalizedAlias = slugify(alias);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { notFound, permanentRedirect } from "next/navigation";
|
import { notFound, permanentRedirect } from "next/navigation";
|
||||||
|
import { cache } from "react";
|
||||||
import { API_URL } from "@/config/constants";
|
import { API_URL } from "@/config/constants";
|
||||||
import { resolveFacilityAlias } from "@/app/facilityAliases";
|
import { resolveFacilityAlias } from "@/app/facilityAliases";
|
||||||
import type { FacilityRecord } from "@/app/facilityData";
|
import type { FacilityRecord } from "@/app/facilityData";
|
||||||
|
|
@ -23,7 +24,7 @@ type FacilityPageData = FacilityRecord & {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getFacility(slug: string) {
|
const getFacility = cache(async (slug: string) => {
|
||||||
const res = await fetch(`${API_URL}/facilities/${slug}`, { cache: "no-store" });
|
const res = await fetch(`${API_URL}/facilities/${slug}`, { cache: "no-store" });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -35,7 +36,7 @@ async function getFacility(slug: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return facility as FacilityPageData;
|
return facility as FacilityPageData;
|
||||||
}
|
});
|
||||||
|
|
||||||
async function getCanonicalSlug(slug: string) {
|
async function getCanonicalSlug(slug: string) {
|
||||||
const canonicalSlug = await resolveFacilityAlias(slug);
|
const canonicalSlug = await resolveFacilityAlias(slug);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
export async function fetchPublicFacilities<T>(
|
export async function fetchPublicFacilities<T>(
|
||||||
view: string,
|
view: string,
|
||||||
revalidateSeconds: number,
|
_revalidateSeconds: number,
|
||||||
{
|
{
|
||||||
allowEmpty = false,
|
allowEmpty = false,
|
||||||
attempts = 3,
|
attempts = 3,
|
||||||
|
|
@ -18,7 +18,7 @@ export async function fetchPublicFacilities<T>(
|
||||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/facilities?view=${view}`, {
|
const res = await fetch(`${API_URL}/facilities?view=${view}`, {
|
||||||
next: { revalidate: revalidateSeconds },
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ export default async function PlacePage({ params }: { params: Promise<{ slug: st
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/place-pages/${slug}`, {
|
const res = await fetch(`${API_URL}/place-pages/${slug}`, {
|
||||||
next: { revalidate },
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue