Simple API gateway for webhooks

add country endpoint

finxol.io 569784f6 1b6bfad6

verified
+161
+104
src/sensors.ts
··· 1 1 import { Hono } from "hono" 2 2 import { validator } from "hono/validator" 3 3 import { z } from "zod" 4 + import { tryCatch } from "./util.ts" 4 5 5 6 const SensorsSchema = z.object({ 6 7 sensorData: z.array( ··· 63 64 deviceName: z.string(), 64 65 }) 65 66 67 + const ReverseGeocodingSchema = z.object({ 68 + type: z.literal("FeatureCollection"), 69 + features: z.array( 70 + z.object({ 71 + type: z.literal("Feature"), 72 + properties: z.object({ 73 + datasource: z.object({ 74 + sourcename: z.string(), 75 + attribution: z.string(), 76 + license: z.string(), 77 + url: z.string(), 78 + }).optional(), 79 + name: z.string().optional(), 80 + country: z.string(), 81 + country_code: z.string(), 82 + state: z.string().optional(), 83 + city: z.string().optional(), 84 + postcode: z.string().optional(), 85 + district: z.string().optional(), 86 + suburb: z.string().optional(), 87 + street: z.string().optional(), 88 + housenumber: z.string().optional(), 89 + iso3166_2: z.string().optional(), 90 + lon: z.number(), 91 + lat: z.number(), 92 + state_code: z.string().optional(), 93 + distance: z.number().optional(), 94 + result_type: z.string().optional(), 95 + formatted: z.string(), 96 + address_line1: z.string().optional(), 97 + address_line2: z.string().optional(), 98 + category: z.string().optional(), 99 + timezone: z.object({ 100 + name: z.string(), 101 + offset_STD: z.string(), 102 + offset_STD_seconds: z.number(), 103 + offset_DST: z.string(), 104 + offset_DST_seconds: z.number(), 105 + abbreviation_STD: z.string(), 106 + abbreviation_DST: z.string(), 107 + }).optional(), 108 + plus_code: z.string().optional(), 109 + rank: z.object({ 110 + importance: z.number(), 111 + popularity: z.number(), 112 + }).optional(), 113 + place_id: z.string().optional(), 114 + }), 115 + geometry: z.object({ 116 + type: z.literal("Point"), 117 + coordinates: z.tuple([z.number(), z.number()]), 118 + }), 119 + bbox: z.array(z.number()).optional(), 120 + }), 121 + ), 122 + query: z.object({ 123 + lat: z.number(), 124 + lon: z.number(), 125 + plus_code: z.string().optional(), 126 + }).optional(), 127 + }) 128 + 66 129 type Sensors = z.infer<typeof SensorsSchema> 67 130 68 131 const kv = await Deno.openKv() 69 132 70 133 const sensors = new Hono() 134 + .get("/country", async (c) => { 135 + const data = await kv.get<Sensors>(["sensors", "latest"]) 136 + if (!data.value) { 137 + return c.text("No data found", 404) 138 + } 139 + 140 + const location = data.value.sensorData.find((sensor) => 141 + sensor.sensorType === "location" 142 + )?.data 143 + 144 + if (!location) { 145 + return c.text("No location data found", 404) 146 + } 147 + 148 + const geocode = await tryCatch( 149 + fetch( 150 + `https://api.geoapify.com/v1/geocode/reverse?lat=${location.latitude}&lon=${location.longitude}&apiKey=${ 151 + Deno.env.get("GEOAPIFY_API_KEY") 152 + }`, 153 + ) 154 + .then((res) => res.json()) 155 + .then((data) => data.features[0].properties.country_code), 156 + ) 157 + 158 + if (!geocode.success) { 159 + console.error(geocode.error) 160 + return c.text("Geoapify API error", 404) 161 + } 162 + 163 + const country = ReverseGeocodingSchema.safeParse(geocode.value) 164 + 165 + if (!country.success) { 166 + console.error(country.error) 167 + return c.text("Invalid country data", 400) 168 + } 169 + 170 + return c.json({ 171 + country: country.data.features[0].properties.country, 172 + country_code: country.data.features[0].properties.country_code, 173 + }) 174 + }) 71 175 .get("/get", async (c) => { 72 176 const data = await kv.get<Sensors>(["sensors", "latest"]) 73 177 if (!data.value) {
+57
src/util.ts
··· 1 + /** 2 + * Wraps a promise in a try/catch block and returns a Result object representing 3 + * either a successful value or an error. 4 + * 5 + * This utility function provides a more structured way to handle asynchronous operations 6 + * without using try/catch blocks throughout your codebase. It follows a pattern similar 7 + * to Rust's Result type, allowing for more predictable error handling. 8 + * 9 + * @template T - The type of the value returned by the promise on success 10 + * @template E - The type of the error object (defaults to Error) 11 + * 12 + * @param promise - The promise to wrap and execute 13 + * 14 + * @returns A Promise resolving to a discriminated union object with: 15 + * - On success: `{ success: true, value: T, error: null }` 16 + * - On failure: `{ success: false, value: null, error: E }` 17 + * 18 + * @example 19 + * // Success case 20 + * const successResult = await tryCatch(Promise.resolve('data')); 21 + * if (successResult.success) { 22 + * console.log(successResult.value); // 'data' 23 + * } 24 + * 25 + * @example 26 + * // Error case 27 + * const errorResult = await tryCatch(Promise.reject(new Error('Failed'))); 28 + * if (!errorResult.success) { 29 + * console.error(errorResult.error.message); // 'Failed' 30 + * } 31 + * 32 + * @example 33 + * // Using with custom error type 34 + * interface ApiError { code: number; message: string } 35 + * const apiCall = tryCatch<UserData, ApiError>(fetchUserData()); 36 + */ 37 + export async function tryCatch<T, E = Error>( 38 + promise: Promise<T>, 39 + ): Promise< 40 + | { 41 + success: true 42 + value: T 43 + error: null 44 + } 45 + | { 46 + success: false 47 + value: null 48 + error: E 49 + } 50 + > { 51 + try { 52 + const value = await promise 53 + return { success: true, value, error: null } 54 + } catch (error) { 55 + return { success: false, value: null, error: error as E } 56 + } 57 + }