jokull/supabase/functions/download-bus-stops/index.ts

162 lines
4.2 KiB
TypeScript

import { SupabaseClient, createClient } from "https://esm.sh/@supabase/supabase-js@2";
interface StraetoVariables {
[key: string]: number | string;
}
interface StraetoQuery {
operationName: string;
variables: StraetoVariables;
extensions: StraetoQueryExtensions;
}
interface StraetoQueryExtensions {
persistedQuery: StraetoPersistedQuery;
}
interface StraetoPersistedQuery {
version: number;
sha256Hash: string;
}
interface StraetoBusStop {
id: number;
name: string;
lat: number;
lon: number;
type: number;
rotation: number;
code: string | null;
isTerminal: boolean;
routes: Array<string>;
alerts: Array<string>;
__typename: string;
}
interface DbBusStop {
id: number;
stop_name: string;
coords: string; // TODO better geometry type representation
type: number;
rotation: number;
code: string | null;
is_terminal: boolean;
routes: Array<string>;
}
function toDbBusStop(straetoStop: StraetoBusStop): DbBusStop {
return {
id: straetoStop.id,
code: straetoStop.code,
stop_name: straetoStop.name,
coords: `POINT(${straetoStop.lat} ${straetoStop.lon})`,
type: straetoStop.type,
rotation: straetoStop.rotation,
is_terminal: straetoStop.isTerminal,
routes: straetoStop.routes,
};
}
function toQuerystring(obj: StraetoQuery) {
return Object.keys(obj).map(function (variableName) {
const variableValue = obj[variableName as keyof typeof obj];
if (typeof variableValue == "object") {
return encodeURIComponent(variableName) + "=" +
encodeURIComponent(JSON.stringify(variableValue));
} else {
return encodeURIComponent(variableName) + "=" +
encodeURIComponent(variableValue);
}
}).join("&");
}
const graphqlParams: StraetoQuery = {
operationName: "Stops",
variables: {},
extensions: {
persistedQuery: {
version: 1,
sha256Hash:
"6303de055cc42db47a4e1f1cc941b8c86d11c147903bed231e6d4bddcf0e1312",
},
},
};
async function getStops(): Promise<Array<StraetoBusStop>> {
const apiUrl = `https://straeto.is/graphql?${toQuerystring(graphqlParams)}`;
const opts = {
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const resp = await fetch(apiUrl, opts);
const body = await resp.json();
return body.data.GtfsStops.results;
}
async function verifyUserIsAdmin(userClient: SupabaseClient): Promise<boolean> {
const user = await userClient.auth.getUser();
const metadata = user.data.user?.app_metadata;
if (!metadata || (metadata && !metadata.claims_admin)) {
return false;
}
return true;
}
Deno.serve(async (req) => {
const authHeader = req.headers.get("Authorization")!;
const userClient = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
{ global: { headers: { Authorization: authHeader } } },
);
const secureClient = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
);
if (!(await verifyUserIsAdmin(userClient))) {
return new Response(
JSON.stringify({ status: "denied", error: "must be an admin" }),
{ status: 403, headers: { "Content-Type": "application/json" } },
);
}
const busStops = await getStops();
console.log("inserting", busStops.length, "rows");
const toInsert = busStops.map(toDbBusStop);
const insert = await secureClient.from("bus_stops")
.upsert(toInsert, { ignoreDuplicates: true });
console.log(insert.statusText, insert.error?.code, insert.error?.message);
return new Response(
JSON.stringify({
"status": insert.statusText,
"inserted": toInsert.length,
"error": insert.error,
}),
{ headers: { "Content-Type": "application/json" } },
);
});
/* To invoke locally:
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
2. Make an HTTP request:
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/download-bus-stops' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
--header 'Content-Type: application/json'
*/