- The Gieß den Kiez user
would like to connect with you
+
would like to connect with you
and has sent you the following message:
- To reply to the user by e-mail, please write to:
+ To reply to the user via e-mail, please write to:
If the contact request contains inappropriate content, we are very sorry. Please inform our team immediately via info@citylab-berlin.org.
+ text-align: center;">We apologize if the contact request contains inappropriate content. Please notify our team immediately via info@citylab-berlin.org.
Date: Thu, 20 Jun 2024 14:15:36 +0200
Subject: [PATCH 08/12] feat: add daily rain tables (#268)
* feat: add monthly rain tables
* fix: foreign key
* fix: daily weather data
* fix: add RLS
* feat: created_at column, just to be sure
* feat: function for fetching aggregated weather data
* chore: formatting
* feat: add parameter to limit number of returned months
* fix: typo
---
.../migrations/20240619130511_montly_rain.sql | 25 +++++++++++++++
.../20240620113031_rain_functions.sql | 32 +++++++++++++++++++
2 files changed, 57 insertions(+)
create mode 100644 supabase/migrations/20240619130511_montly_rain.sql
create mode 100644 supabase/migrations/20240620113031_rain_functions.sql
diff --git a/supabase/migrations/20240619130511_montly_rain.sql b/supabase/migrations/20240619130511_montly_rain.sql
new file mode 100644
index 00000000..2e05b6e1
--- /dev/null
+++ b/supabase/migrations/20240619130511_montly_rain.sql
@@ -0,0 +1,25 @@
+create table if not exists daily_weather_data (
+ id serial primary key,
+ created_at timestamp not null default now(),
+ measure_day timestamp not null,
+ day_finished boolean not null default false,
+ sum_precipitation_mm_per_sqm float,
+ avg_temperature_celsius float,
+ avg_pressure_msl float,
+ sum_sunshine_minutes float,
+ avg_wind_direction_deg float,
+ avg_wind_speed_kmh float,
+ avg_cloud_cover_percentage float,
+ avg_dew_point_celcius float,
+ avg_relative_humidity_percentage float,
+ avg_visibility_m float,
+ avg_wind_gust_direction_deg float,
+ avg_wind_gust_speed_kmh float,
+ source_dwd_station_ids text[]
+);
+
+alter table "public"."daily_weather_data" enable row level security;
+create policy "Allow anonymous select on daily_weather_data"
+ on "public"."daily_weather_data"
+ for select
+ using (true);
\ No newline at end of file
diff --git a/supabase/migrations/20240620113031_rain_functions.sql b/supabase/migrations/20240620113031_rain_functions.sql
new file mode 100644
index 00000000..66e68ca8
--- /dev/null
+++ b/supabase/migrations/20240620113031_rain_functions.sql
@@ -0,0 +1,32 @@
+CREATE OR REPLACE FUNCTION public.accumulated_weather_per_month (limit_monts int)
+ RETURNS TABLE (
+ measure_day text, sum_precipitation_mm_per_sqm float, avg_temperature_celsius float, avg_pressure_msl float, sum_sunshine_minutes float, avg_wind_direction_deg float, avg_wind_speed_kmh float, avg_cloud_cover_percentage float, avg_dew_point_celcius float, avg_relative_humidity_percentage float, avg_visibility_m float, avg_wind_gust_direction_deg float, avg_wind_gust_speed_kmh float)
+ LANGUAGE plpgsql
+ SECURITY INVOKER
+ AS $function$
+BEGIN
+ RETURN query
+ SELECT
+ to_char(daily_weather_data.measure_day, 'YYYY-MM'),
+ sum(daily_weather_data.sum_precipitation_mm_per_sqm) AS sum_precipitation_mm_per_sqm,
+ avg(daily_weather_data.avg_temperature_celsius) AS avg_temperature_celsius,
+ avg(daily_weather_data.avg_pressure_msl) AS avg_pressure_msl,
+ sum(daily_weather_data.sum_sunshine_minutes) AS sum_sunshine_minutes,
+ avg(daily_weather_data.avg_wind_direction_deg) AS avg_wind_direction_deg,
+ avg(daily_weather_data.avg_wind_speed_kmh) AS avg_wind_speed_kmh,
+ avg(daily_weather_data.avg_cloud_cover_percentage) AS avg_cloud_cover_percentage,
+ avg(daily_weather_data.avg_dew_point_celcius) AS avg_dew_point_celcius,
+ avg(daily_weather_data.avg_relative_humidity_percentage) AS avg_relative_humidity_percentage,
+ avg(daily_weather_data.avg_visibility_m) AS avg_visibility_m,
+ avg(daily_weather_data.avg_wind_gust_direction_deg) AS avg_wind_gust_direction_deg,
+ avg(daily_weather_data.avg_wind_gust_speed_kmh) AS avg_wind_gust_speed_kmh
+ FROM
+ daily_weather_data
+ GROUP BY
+ to_char(daily_weather_data.measure_day, 'YYYY-MM')
+ ORDER BY
+ to_char(daily_weather_data.measure_day, 'YYYY-MM')
+ DESC
+LIMIT limit_monts;
+END;
+$function$;
\ No newline at end of file
From b5e5281e0cfa809406a331b5f599311c605d1cf2 Mon Sep 17 00:00:00 2001
From: Jonas Jaszkowic
Date: Mon, 24 Jun 2024 11:28:16 +0200
Subject: [PATCH 09/12] fix: contact email subject
---
supabase/functions/submit_contact_request/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/supabase/functions/submit_contact_request/index.ts b/supabase/functions/submit_contact_request/index.ts
index b18bfb45..a46b0f4c 100644
--- a/supabase/functions/submit_contact_request/index.ts
+++ b/supabase/functions/submit_contact_request/index.ts
@@ -116,7 +116,7 @@ const handler = async (_request: Request): Promise => {
from: SMTP_FROM,
to: fullRecipientData.email,
replyTo: lookupData.senderEmail,
- subject: "[Gieß den Kiez] Kontaktanfrage / Contact request",
+ subject: "Kontaktanfrage / Contact request",
html: mailTemplate(
lookupData.senderUsername,
message,
From c34008cea9835b99a71582661c73ea7ebdb135a2 Mon Sep 17 00:00:00 2001
From: Jonas Jaszkowic
Date: Thu, 4 Jul 2024 17:19:39 +0200
Subject: [PATCH 10/12] feat: gdk stats (#269)
* feat: first gdk stats
* feat: monthly waterings
* feat: mostFrequentTreeSpecies
* fix: typo
* chore: refactoring
* fix: error handling and env
* chore: comments
* feat: more database functions
* feat: env var existence check
* feat: more distinguishable errors
* fix: console.error instead of console.log for errors
* chore: cleanup
* fix: env variables
* fix: more restrictive cors, chore: cleanup
---
README.md | 13 +-
package-lock.json | 52 ++--
package.json | 2 +-
src/database.ts | 95 +++++++
supabase/.env.sample | 3 -
supabase/functions/_shared/check-env.ts | 7 +
supabase/functions/_shared/common.ts | 43 ++++
.../{checks.ts => contact-request-checks.ts} | 9 -
supabase/functions/_shared/cors.ts | 2 +-
supabase/functions/_shared/errors.ts | 14 ++
.../functions/check_contact_request/index.ts | 12 +-
supabase/functions/gdk_stats/index.ts | 232 ++++++++++++++++++
.../functions/submit_contact_request/index.ts | 56 +++--
.../tests/submit-contact-request-tests.ts | 3 +-
.../20240620143046_db_stats_functions.sql | 83 +++++++
15 files changed, 559 insertions(+), 67 deletions(-)
create mode 100644 supabase/functions/_shared/check-env.ts
create mode 100644 supabase/functions/_shared/common.ts
rename supabase/functions/_shared/{checks.ts => contact-request-checks.ts} (92%)
create mode 100644 supabase/functions/_shared/errors.ts
create mode 100644 supabase/functions/gdk_stats/index.ts
create mode 100644 supabase/migrations/20240620143046_db_stats_functions.sql
diff --git a/README.md b/README.md
index 39c04694..34be28d6 100644
--- a/README.md
+++ b/README.md
@@ -116,7 +116,7 @@ deno test --allow-all supabase/functions/tests/submit-contact-request-tests.ts -
- **(Not recommended but possible)** Link your local project directly to the remote `supabase link --project-ref ` (will ask you for your database password from the creation process)
- **(Not recommended but possible)** Push your local state directly to your remote project `supabase db push` (will ask you for your database password from the creation process)
-#### Supabase
+#### Supabase Auth
Some of the requests need a authorized user. You can create a new user using email password via the Supabase API.
@@ -142,6 +142,17 @@ curl --request POST \
See the [docs/api.http](./docs/api.http) file for more examples or take a look into the API documentation in your local supabase instance under http://localhost:54323/project/default/api?page=users
+#### Supabase Edge Functions
+To run the Supabase Edge Functions locally:
+
+- Setup the .env file in [supabase/.env](supabase/.env) according to [supabase/.env.sample](supabase/.env.sample)
+- Note: The env variables `SUPABASE_SERVICE_ROLE_KEY` and `SUPABASE_URL` are injected automatically and can't be set the in the [supabase/.env](supabase/.env) file. If you want to overwrite them, you have to rename the environment variables to not start with `SUPABASE_`. For reference, see: https://supabase.com/docs/guides/functions/secrets
+- With the environment variables setup correctly, execute `supabase functions serve --no-verify-jwt --env-file supabase/.env`
+
+To deploy the Edge Functions in your linked remote Supabase project, execute:
+- `supabase functions deploy`
+- Make sure that you set the proper environment variables in the remote Supabase project too
+
## Tests
Locally you will need supabase running and a `.env` file with the right values in it.
diff --git a/package-lock.json b/package-lock.json
index bfb4df8e..51c75316 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@supabase/supabase-js": "2.43.2"
+ "@supabase/supabase-js": "2.43.5"
},
"devDependencies": {
"@saithodev/semantic-release-backmerge": "4.0.1",
@@ -2469,9 +2469,9 @@
}
},
"node_modules/@supabase/functions-js": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.3.1.tgz",
- "integrity": "sha512-QyzNle/rVzlOi4BbVqxLSH828VdGY1RElqGFAj+XeVypj6+PVtMlD21G8SDnsPQDtlqqTtoGRgdMlQZih5hTuw==",
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.1.tgz",
+ "integrity": "sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
@@ -2488,9 +2488,9 @@
}
},
"node_modules/@supabase/postgrest-js": {
- "version": "1.15.2",
- "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.2.tgz",
- "integrity": "sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ==",
+ "version": "1.15.5",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.5.tgz",
+ "integrity": "sha512-YR4TiitTE2hizT7mB99Cl3V9i00RAY5sUxS2/NuWWzkreM7OeYlP2OqnqVwwb4z6ILn+j8x9e/igJDepFhjswQ==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
@@ -2507,24 +2507,24 @@
}
},
"node_modules/@supabase/storage-js": {
- "version": "2.5.5",
- "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.5.tgz",
- "integrity": "sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w==",
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.6.0.tgz",
+ "integrity": "sha512-REAxr7myf+3utMkI2oOmZ6sdplMZZ71/2NEIEMBZHL9Fkmm3/JnaOZVSRqvG4LStYj2v5WhCruCzuMn6oD/Drw==",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/supabase-js": {
- "version": "2.43.2",
- "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.43.2.tgz",
- "integrity": "sha512-F9CljeJBo5aPucNhrLoMnpEHi5yqNZ0vH0/CL4mGy+/Ggr7FUrYErVJisa1NptViqyhs1HGNzzwjOYG6626h8g==",
+ "version": "2.43.5",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.43.5.tgz",
+ "integrity": "sha512-Y4GukjZWW6ouohMaPlYz8tSz9ykf9jY7w9/RhqKuScmla3Xiklce8eLr8TYAtA+oQYCWxo3RgS3B6O4rd/72FA==",
"dependencies": {
"@supabase/auth-js": "2.64.2",
- "@supabase/functions-js": "2.3.1",
+ "@supabase/functions-js": "2.4.1",
"@supabase/node-fetch": "2.6.15",
- "@supabase/postgrest-js": "1.15.2",
+ "@supabase/postgrest-js": "1.15.5",
"@supabase/realtime-js": "2.9.5",
- "@supabase/storage-js": "2.5.5"
+ "@supabase/storage-js": "2.6.0"
}
},
"node_modules/@technologiestiftung/semantic-release-config": {
@@ -3266,12 +3266,12 @@
}
},
"node_modules/braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
- "fill-range": "^7.0.1"
+ "fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -4695,9 +4695,9 @@
}
},
"node_modules/fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -12707,9 +12707,9 @@
}
},
"node_modules/ws": {
- "version": "8.17.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
- "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
diff --git a/package.json b/package.json
index a22b9c42..b67b4f90 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"node": ">=18"
},
"dependencies": {
- "@supabase/supabase-js": "2.43.2"
+ "@supabase/supabase-js": "2.43.5"
},
"devDependencies": {
"@saithodev/semantic-release-backmerge": "4.0.1",
diff --git a/src/database.ts b/src/database.ts
index 994acd24..34bcec46 100644
--- a/src/database.ts
+++ b/src/database.ts
@@ -51,6 +51,66 @@ export type Database = {
},
]
}
+ daily_weather_data: {
+ Row: {
+ avg_cloud_cover_percentage: number | null
+ avg_dew_point_celcius: number | null
+ avg_pressure_msl: number | null
+ avg_relative_humidity_percentage: number | null
+ avg_temperature_celsius: number | null
+ avg_visibility_m: number | null
+ avg_wind_direction_deg: number | null
+ avg_wind_gust_direction_deg: number | null
+ avg_wind_gust_speed_kmh: number | null
+ avg_wind_speed_kmh: number | null
+ created_at: string
+ day_finished: boolean
+ id: number
+ measure_day: string
+ source_dwd_station_ids: string[] | null
+ sum_precipitation_mm_per_sqm: number | null
+ sum_sunshine_minutes: number | null
+ }
+ Insert: {
+ avg_cloud_cover_percentage?: number | null
+ avg_dew_point_celcius?: number | null
+ avg_pressure_msl?: number | null
+ avg_relative_humidity_percentage?: number | null
+ avg_temperature_celsius?: number | null
+ avg_visibility_m?: number | null
+ avg_wind_direction_deg?: number | null
+ avg_wind_gust_direction_deg?: number | null
+ avg_wind_gust_speed_kmh?: number | null
+ avg_wind_speed_kmh?: number | null
+ created_at?: string
+ day_finished?: boolean
+ id?: number
+ measure_day: string
+ source_dwd_station_ids?: string[] | null
+ sum_precipitation_mm_per_sqm?: number | null
+ sum_sunshine_minutes?: number | null
+ }
+ Update: {
+ avg_cloud_cover_percentage?: number | null
+ avg_dew_point_celcius?: number | null
+ avg_pressure_msl?: number | null
+ avg_relative_humidity_percentage?: number | null
+ avg_temperature_celsius?: number | null
+ avg_visibility_m?: number | null
+ avg_wind_direction_deg?: number | null
+ avg_wind_gust_direction_deg?: number | null
+ avg_wind_gust_speed_kmh?: number | null
+ avg_wind_speed_kmh?: number | null
+ created_at?: string
+ day_finished?: boolean
+ id?: number
+ measure_day?: string
+ source_dwd_station_ids?: string[] | null
+ sum_precipitation_mm_per_sqm?: number | null
+ sum_sunshine_minutes?: number | null
+ }
+ Relationships: []
+ }
profiles: {
Row: {
id: string
@@ -311,6 +371,41 @@ export type Database = {
[_ in never]: never
}
Functions: {
+ accumulated_weather_per_month: {
+ Args: {
+ limit_monts: number
+ }
+ Returns: {
+ measure_day: string
+ sum_precipitation_mm_per_sqm: number
+ avg_temperature_celsius: number
+ avg_pressure_msl: number
+ sum_sunshine_minutes: number
+ avg_wind_direction_deg: number
+ avg_wind_speed_kmh: number
+ avg_cloud_cover_percentage: number
+ avg_dew_point_celcius: number
+ avg_relative_humidity_percentage: number
+ avg_visibility_m: number
+ avg_wind_gust_direction_deg: number
+ avg_wind_gust_speed_kmh: number
+ }[]
+ }
+ calculate_avg_waterings_per_month: {
+ Args: Record
+ Returns: {
+ month: string
+ watering_count: number
+ avg_amount_per_watering: number
+ }[]
+ }
+ calculate_top_tree_species: {
+ Args: Record
+ Returns: {
+ gattung_deutsch: string
+ percentage: number
+ }[]
+ }
count_by_age: {
Args: {
start_year: number
diff --git a/supabase/.env.sample b/supabase/.env.sample
index 3b66df06..bc6870a3 100644
--- a/supabase/.env.sample
+++ b/supabase/.env.sample
@@ -1,6 +1,3 @@
-URL=http://host.docker.internal:54321
-ANON_KEY=ey..
-SERVICE_ROLE_KEY=ey...
ALLOWED_ORIGIN=http://localhost:5173
SMTP_HOST=...
SMTP_USER=...
diff --git a/supabase/functions/_shared/check-env.ts b/supabase/functions/_shared/check-env.ts
new file mode 100644
index 00000000..fab3e20b
--- /dev/null
+++ b/supabase/functions/_shared/check-env.ts
@@ -0,0 +1,7 @@
+export const loadEnvVars = (vars: string[]) => {
+ const missingVars = vars.filter((v) => !Deno.env.get(v));
+ if (missingVars.length > 0) {
+ throw new Error(`Missing environment variables: ${missingVars.join(", ")}`);
+ }
+ return vars.map((v) => Deno.env.get(v));
+};
diff --git a/supabase/functions/_shared/common.ts b/supabase/functions/_shared/common.ts
new file mode 100644
index 00000000..ccc435ed
--- /dev/null
+++ b/supabase/functions/_shared/common.ts
@@ -0,0 +1,43 @@
+export interface TreeSpecies {
+ speciesName?: string;
+ percentage: number;
+}
+
+export interface Monthly {
+ month: string;
+ wateringCount: number;
+ averageAmountPerWatering: number;
+ totalSum: number;
+}
+
+export interface Watering {
+ id: string;
+ lat: number;
+ lng: number;
+ amount: number;
+ timestamp: string;
+}
+
+export interface TreeAdoptions {
+ count: number;
+ veryThirstyCount: number;
+}
+
+export interface GdkStats {
+ numTrees: number;
+ numPumps: number;
+ numActiveUsers: number;
+ numWateringsThisYear: number;
+ monthlyWaterings: Monthly[];
+ treeAdoptions: TreeAdoptions;
+ mostFrequentTreeSpecies: TreeSpecies[];
+ totalTreeSpeciesCount: number;
+ waterings: Watering[];
+ monthlyWeather: MonthlyWeather[];
+}
+
+export interface MonthlyWeather {
+ month: string;
+ averageTemperatureCelsius: number;
+ totalRainfallLiters: number;
+}
diff --git a/supabase/functions/_shared/checks.ts b/supabase/functions/_shared/contact-request-checks.ts
similarity index 92%
rename from supabase/functions/_shared/checks.ts
rename to supabase/functions/_shared/contact-request-checks.ts
index f1628fd7..fb0bc788 100644
--- a/supabase/functions/_shared/checks.ts
+++ b/supabase/functions/_shared/contact-request-checks.ts
@@ -24,10 +24,7 @@ export async function checkIfContactRequestIsAllowed(
const { data: senderData, error: senderDataError } =
await supabaseClient.auth.getUser(token);
- console.log(senderData);
-
if (senderDataError) {
- console.log(senderDataError);
return { isAllowed: false, reason: "unauthorized", lookupData: undefined };
}
@@ -39,10 +36,7 @@ export async function checkIfContactRequestIsAllowed(
.eq("id", senderData.user.id)
.single();
- console.log(senderLookupData);
-
if (senderLookupDataError) {
- console.log(senderLookupDataError);
return { isAllowed: false, reason: "not_found", lookupData: undefined };
}
@@ -55,7 +49,6 @@ export async function checkIfContactRequestIsAllowed(
.single();
if (recipientDataError) {
- console.log(recipientDataError);
return { isAllowed: false, reason: "not_found", lookupData: undefined };
}
@@ -69,7 +62,6 @@ export async function checkIfContactRequestIsAllowed(
.not("contact_mail_id", "is", null); // only count sent emails
if (requestsToRecipientError) {
- console.log(requestsToRecipientError);
return {
isAllowed: false,
reason: "internal_server_error",
@@ -95,7 +87,6 @@ export async function checkIfContactRequestIsAllowed(
.gt("created_at", sub(new Date(), { days: 1 }).toISOString());
if (requestsOfLast24hError) {
- console.log(requestsOfLast24hError);
return {
isAllowed: false,
reason: "internal_server_error",
diff --git a/supabase/functions/_shared/cors.ts b/supabase/functions/_shared/cors.ts
index ce27bf50..e9d9d09d 100644
--- a/supabase/functions/_shared/cors.ts
+++ b/supabase/functions/_shared/cors.ts
@@ -2,7 +2,7 @@ const ALLOWED_ORIGIN = Deno.env.get("ALLOWED_ORIGIN");
export const corsHeaders = {
"Access-Control-Allow-Origin": ALLOWED_ORIGIN,
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers":
"Content-Type,Authorization,x-client-info,apikey",
};
diff --git a/supabase/functions/_shared/errors.ts b/supabase/functions/_shared/errors.ts
new file mode 100644
index 00000000..732aa005
--- /dev/null
+++ b/supabase/functions/_shared/errors.ts
@@ -0,0 +1,14 @@
+export enum ErrorTypes {
+ GdkStatsPump = "gdk_stats_pumps",
+ GdkStatsUser = "gdk_stats_users",
+ GdkStatsWatering = "gdk_stats_waterings",
+ GdkStatsAdoption = "gdk_stats_adoptions",
+ GdkStatsTreeSpecie = "gdk_stats_tree_species",
+ GdkStatsWeather = "gdk_stats_weather",
+}
+
+export class GdkError extends Error {
+ constructor(message: string, public errorType: ErrorTypes) {
+ super(message);
+ }
+}
diff --git a/supabase/functions/check_contact_request/index.ts b/supabase/functions/check_contact_request/index.ts
index 727604ab..85a8d3b4 100644
--- a/supabase/functions/check_contact_request/index.ts
+++ b/supabase/functions/check_contact_request/index.ts
@@ -1,10 +1,16 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { checkIfContactRequestIsAllowed } from "../_shared/checks.ts";
import { corsHeaders } from "../_shared/cors.ts";
+import { loadEnvVars } from "../_shared/check-env.ts";
-const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
-const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
-const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
+const ENV_VARS = [
+ "SUPABASE_URL",
+ "SUPABASE_ANON_KEY",
+ "SUPABASE_SERVICE_ROLE_KEY",
+];
+
+const [SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY] =
+ loadEnvVars(ENV_VARS);
const handler = async (_request: Request): Promise => {
if (_request.method === "OPTIONS") {
diff --git a/supabase/functions/gdk_stats/index.ts b/supabase/functions/gdk_stats/index.ts
new file mode 100644
index 00000000..64eaee2f
--- /dev/null
+++ b/supabase/functions/gdk_stats/index.ts
@@ -0,0 +1,232 @@
+import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
+import { corsHeaders } from "../_shared/cors.ts";
+import { loadEnvVars } from "../_shared/check-env.ts";
+import {
+ GdkStats,
+ Monthly,
+ MonthlyWeather,
+ TreeAdoptions,
+ TreeSpecies,
+ Watering,
+} from "../_shared/common.ts";
+import { GdkError, ErrorTypes } from "../_shared/errors.ts";
+
+const ENV_VARS = ["SUPABASE_URL", "SUPABASE_SERVICE_ROLE_KEY", "PUMPS_URL"];
+const [SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, PUMPS_URL] =
+ loadEnvVars(ENV_VARS);
+
+// As trees table barely changes, we can hardcode the values
+// It would be too expensive to calculate on each request
+
+// SELECT COUNT(1) FROM trees;
+const TREE_COUNT = 885825;
+
+// SELECT trees.gattung_deutsch, (COUNT(1) * 100.0) / (SELECT COUNT(1) FROM trees) AS percentage
+// FROM trees
+// GROUP BY trees.gattung_deutsch
+// ORDER BY COUNT(1) DESC
+// LIMIT 20;
+const MOST_FREQUENT_TREE_SPECIES: TreeSpecies[] = [
+ { speciesName: "AHORN", percentage: 22.8128580701605848 },
+ { speciesName: "LINDE", percentage: 21.5930911861823724 },
+ { speciesName: "EICHE", percentage: 10.5370699630288149 },
+ { speciesName: undefined, percentage: 4.1923630513927695 },
+ { speciesName: "ROBINIE", percentage: 3.9515705698078063 },
+ { speciesName: "ROSSKASTANIE", percentage: 3.6574944260999633 },
+ { speciesName: "BIRKE", percentage: 3.610419665283775 },
+ { speciesName: "HAINBUCHE", percentage: 3.4514717918324726 },
+ { speciesName: "PLATANE", percentage: 3.3499844777467333 },
+ { speciesName: "PAPPEL", percentage: 2.8882679987582197 },
+ { speciesName: "ESCHE", percentage: 2.7732339909124263 },
+ { speciesName: "KIEFER", percentage: 2.4801738492365874 },
+ { speciesName: "ULME", percentage: 1.946998560663788 },
+ { speciesName: "BUCHE", percentage: 1.7521519487483419 },
+ { speciesName: "HASEL", percentage: 1.1728050122766912 },
+ { speciesName: "WEIßDORN", percentage: 1.1243755820844975 },
+ { speciesName: "WEIDE", percentage: 1.0893799565376909 },
+ { speciesName: "MEHLBEERE", percentage: 0.90469336494228544013 },
+ { speciesName: "ERLE", percentage: 0.80907628481923630514 },
+ { speciesName: "APFEL", percentage: 0.70092851296813704739 },
+];
+
+// SELECT COUNT(gattung_deutsch) FROM trees GROUP BY gattung_deutsch;
+const TOTAL_TREE_SPECIES_COUNT = 97;
+
+const supabaseServiceRoleClient = createClient(
+ SUPABASE_URL,
+ SUPABASE_SERVICE_ROLE_KEY
+);
+
+const getUserProfilesCount = async (): Promise => {
+ const { count } = await supabaseServiceRoleClient
+ .from("profiles")
+ .select("*", { count: "exact", head: true });
+
+ if (count === null) {
+ throw new GdkError(
+ "Could not fetch count of profiles table",
+ ErrorTypes.GdkStatsUser
+ );
+ }
+
+ return count || 0;
+};
+
+const getWateringsCount = async (): Promise => {
+ const beginningOfYear = new Date(`${new Date().getFullYear()}-01-01`);
+ const { count } = await supabaseServiceRoleClient
+ .from("trees_watered")
+ .select("*", { count: "exact", head: true })
+ .gt("timestamp", beginningOfYear.toISOString());
+
+ if (count === null) {
+ throw new GdkError(
+ "Could not fetch count of trees_watered table",
+ ErrorTypes.GdkStatsWatering
+ );
+ }
+
+ return count || 0;
+};
+
+const getPumpsCount = async (): Promise => {
+ const response = await fetch(PUMPS_URL);
+ if (response.status !== 200) {
+ throw new GdkError(response.statusText, ErrorTypes.GdkStatsPump);
+ }
+ const geojson = await response.json();
+ return geojson.features.length;
+};
+
+const getAdoptedTreesCount = async (): Promise => {
+ const { data, error } = await supabaseServiceRoleClient
+ .rpc("calculate_adoptions")
+ .select("*");
+
+ if (error) {
+ throw new GdkError(error.message, ErrorTypes.GdkStatsAdoption);
+ }
+
+ return {
+ count: data[0].total_adoptions,
+ veryThirstyCount: data[0].very_thirsty_adoptions,
+ } as TreeAdoptions;
+};
+
+const getMonthlyWaterings = async (): Promise => {
+ const { data, error } = await supabaseServiceRoleClient
+ .rpc("calculate_avg_waterings_per_month")
+ .select("*");
+
+ if (error) {
+ throw new GdkError(error.message, ErrorTypes.GdkStatsWatering);
+ }
+
+ return data.map((month: any) => ({
+ month: month.month,
+ wateringCount: month.watering_count,
+ totalSum: month.total_sum,
+ averageAmountPerWatering: month.avg_amount_per_watering,
+ }));
+};
+
+const getMonthlyWeather = async (): Promise => {
+ const { data, error } = await supabaseServiceRoleClient
+ .rpc("get_monthly_weather")
+ .select("*");
+
+ if (error) {
+ throw new GdkError(error.message, ErrorTypes.GdkStatsWeather);
+ }
+
+ return data.map((month: any) => ({
+ month: month.month,
+ averageTemperatureCelsius: month.avg_temperature_celsius,
+ totalRainfallLiters: month.total_rainfall_liters,
+ }));
+};
+
+const getWaterings = async (): Promise => {
+ const { data, error } = await supabaseServiceRoleClient
+ .rpc("get_waterings_with_location")
+ .select("*");
+
+ if (error) {
+ throw new GdkError(error.message, ErrorTypes.GdkStatsWatering);
+ }
+
+ return data.map((watering: any) => {
+ return {
+ id: watering.id,
+ lat: watering.lat,
+ lng: watering.lng,
+ amount: watering.amount,
+ timestamp: watering.timestamp,
+ };
+ });
+};
+
+const handler = async (request: Request): Promise => {
+ if (request.method === "OPTIONS") {
+ return new Response(null, { headers: corsHeaders, status: 204 });
+ }
+
+ try {
+ const [
+ usersCount,
+ wateringsCount,
+ treeAdoptions,
+ numPumps,
+ monthlyWaterings,
+ waterings,
+ monthlyWeather,
+ ] = await Promise.all([
+ getUserProfilesCount(),
+ getWateringsCount(),
+ getAdoptedTreesCount(),
+ getPumpsCount(),
+ getMonthlyWaterings(),
+ getWaterings(),
+ getMonthlyWeather(),
+ ]);
+
+ const stats: GdkStats = {
+ numTrees: TREE_COUNT,
+ numPumps: numPumps,
+ numActiveUsers: usersCount,
+ numWateringsThisYear: wateringsCount,
+ monthlyWaterings: monthlyWaterings,
+ treeAdoptions: treeAdoptions,
+ mostFrequentTreeSpecies: MOST_FREQUENT_TREE_SPECIES,
+ totalTreeSpeciesCount: TOTAL_TREE_SPECIES_COUNT,
+ waterings: waterings,
+ monthlyWeather: monthlyWeather,
+ };
+
+ return new Response(JSON.stringify(stats), {
+ status: 200,
+ headers: {
+ ...corsHeaders,
+ "Content-Type": "application/json",
+ },
+ });
+ } catch (error) {
+ if (error instanceof GdkError) {
+ console.error(
+ `Error of type ${error.errorType} in gdk_stats function invocation: ${error.message}`
+ );
+ } else {
+ console.error(JSON.stringify(error));
+ }
+
+ return new Response(JSON.stringify(error), {
+ status: 500,
+ headers: {
+ ...corsHeaders,
+ "Content-Type": "application/json",
+ },
+ });
+ }
+};
+
+Deno.serve(handler);
diff --git a/supabase/functions/submit_contact_request/index.ts b/supabase/functions/submit_contact_request/index.ts
index a46b0f4c..64bd201b 100644
--- a/supabase/functions/submit_contact_request/index.ts
+++ b/supabase/functions/submit_contact_request/index.ts
@@ -1,28 +1,42 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import nodemailer from "npm:nodemailer";
-import { checkIfContactRequestIsAllowed } from "../_shared/checks.ts";
+import { checkIfContactRequestIsAllowed } from "../_shared/contact-request-checks.ts";
import { corsHeaders } from "../_shared/cors.ts";
import { mailTemplate } from "./mail-template.ts";
-
-const SMTP_HOST = Deno.env.get("SMTP_HOST");
-const SMTP_USER = Deno.env.get("SMTP_USER");
-const SMTP_PASSWORD = Deno.env.get("SMTP_PASSWORD");
-const SMTP_FROM = Deno.env.get("SMTP_FROM");
-const SMTP_PORT = parseInt(Deno.env.get("SMTP_PORT"));
-const SMTP_SECURE = Deno.env.get("SMTP_SECURE") === "true";
-
-const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
-const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
-const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
-
-const handler = async (_request: Request): Promise => {
- if (_request.method === "OPTIONS") {
+import { loadEnvVars } from "../_shared/check-env.ts";
+
+const ENV_VARS = [
+ "SMTP_HOST",
+ "SMTP_USER",
+ "SMTP_PASSWORD",
+ "SMTP_FROM",
+ "SMTP_PORT",
+ "SMTP_SECURE",
+ "SUPABASE_URL",
+ "SUPABASE_ANON_KEY",
+ "SUPABASE_SERVICE_ROLE_KEY",
+];
+
+const [
+ SMTP_HOST,
+ SMTP_USER,
+ SMTP_PASSWORD,
+ SMTP_FROM,
+ SMTP_PORT,
+ SMTP_SECURE,
+ SUPABASE_URL,
+ SUPABASE_ANON_KEY,
+ SUPABASE_SERVICE_ROLE_KEY,
+] = loadEnvVars(ENV_VARS);
+
+const handler = async (request: Request): Promise => {
+ if (request.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders, status: 204 });
}
- const { recipientContactName, message } = await _request.json();
+ const { recipientContactName, message } = await request.json();
- const authHeader = _request.headers.get("Authorization")!;
+ const authHeader = request.headers.get("Authorization")!;
const supabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
global: { headers: { Authorization: authHeader } },
@@ -68,7 +82,7 @@ const handler = async (_request: Request): Promise => {
.single();
if (fullRecipientDataError) {
- console.log(fullRecipientDataError);
+ console.error(fullRecipientDataError);
return new Response(JSON.stringify(fullRecipientDataError), {
status: 404,
headers: corsHeaders,
@@ -88,7 +102,7 @@ const handler = async (_request: Request): Promise => {
.single();
if (insertedRequestError) {
- console.log(insertedRequestError);
+ console.error(insertedRequestError);
return new Response(JSON.stringify(insertedRequestError), {
status: 500,
headers: corsHeaders,
@@ -136,14 +150,14 @@ const handler = async (_request: Request): Promise => {
.eq("id", insertedRequest.id);
if (updateRequestError) {
- console.log(updateRequestError);
+ console.error(updateRequestError);
return new Response(JSON.stringify(updateRequestError), {
status: 500,
headers: corsHeaders,
});
}
} catch (e) {
- console.log(e);
+ console.error(e);
return new Response(JSON.stringify(e), {
status: 500,
headers: corsHeaders,
diff --git a/supabase/functions/tests/submit-contact-request-tests.ts b/supabase/functions/tests/submit-contact-request-tests.ts
index 717e67b8..3af31e15 100644
--- a/supabase/functions/tests/submit-contact-request-tests.ts
+++ b/supabase/functions/tests/submit-contact-request-tests.ts
@@ -97,14 +97,13 @@ const testContactRequestBlockReasons = async () => {
assertEquals(userLoginDataError, null);
// First contact request to user2 should be possible -> used 1/3 requests
- const { data: firstContactRequestData, error: firstContactRequestDataError } =
+ const { data: firstContactRequestData } =
await supabaseAnonClient.functions.invoke("submit_contact_request", {
body: {
recipientContactName: "user2",
message: "Hello, world!",
},
});
- console.log(firstContactRequestData, firstContactRequestDataError);
assertEquals(firstContactRequestData.code, "contact_request_sent");
diff --git a/supabase/migrations/20240620143046_db_stats_functions.sql b/supabase/migrations/20240620143046_db_stats_functions.sql
new file mode 100644
index 00000000..51a2ae98
--- /dev/null
+++ b/supabase/migrations/20240620143046_db_stats_functions.sql
@@ -0,0 +1,83 @@
+CREATE OR REPLACE FUNCTION public.calculate_avg_waterings_per_month()
+ RETURNS TABLE(month text, watering_count bigint, avg_amount_per_watering numeric, total_sum numeric)
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ RETURN QUERY
+ SELECT to_char(trees_watered.timestamp, 'yyyy-mm') AS month, COUNT(1) AS watering_count, SUM(trees_watered.amount) / COUNT(1) as avg_amount_per_watering, SUM(trees_watered.amount) as total_sum
+ FROM trees_watered
+ GROUP BY to_char(trees_watered.timestamp, 'yyyy-mm')
+ ORDER BY to_char(trees_watered.timestamp, 'yyyy-mm') DESC;
+END;
+$function$;
+
+CREATE OR REPLACE FUNCTION public.calculate_top_tree_species()
+ RETURNS TABLE(gattung_deutsch text, percentage numeric)
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ RETURN QUERY
+ SELECT trees.gattung_deutsch, (COUNT(1) * 100.0) / (SELECT COUNT(1) FROM trees) AS percentage
+ FROM trees
+ GROUP BY trees.gattung_deutsch
+ ORDER BY COUNT(1) DESC
+ LIMIT 20;
+END;
+$function$;
+
+CREATE OR REPLACE FUNCTION public.get_waterings_with_location()
+ RETURNS TABLE(id text, lat double precision, lng double precision, amount numeric, "timestamp" timestamp with time zone)
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ RETURN QUERY
+ SELECT t.id, ST_Y(t.geom) AS lat, ST_X(t.geom) AS lng, tw.amount, tw."timestamp"
+ from trees_watered tw, trees t
+ where tw.tree_id = t.id
+ and tw."timestamp" > DATE_TRUNC('year', CURRENT_DATE)::date;
+END;
+$function$;
+
+CREATE OR REPLACE FUNCTION public.calculate_adoptions()
+ RETURNS TABLE(total_adoptions bigint, very_thirsty_adoptions bigint)
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+RETURN QUERY
+ WITH adoptions AS (
+ SELECT
+ ta.id AS adoption_id,
+ t.id AS tree_id,
+ t.pflanzjahr,
+ date_part('year',
+ now()) - t.pflanzjahr AS age,
+ (date_part('year',
+ now()) - t.pflanzjahr >= 5
+ AND date_part('year',
+ now()) - t.pflanzjahr <= 10) AS very_thirsty
+ FROM
+ trees_adopted ta,
+ trees t
+ WHERE
+ ta.tree_id = t.id
+)
+SELECT
+ count(1) total_adoptions,
+ count(1) FILTER (WHERE adoptions.very_thirsty) AS very_thirsty_adoptions
+FROM
+ adoptions;
+END;
+$function$;
+
+CREATE OR REPLACE FUNCTION public.get_monthly_weather()
+ RETURNS TABLE(month text, avg_temperature_celsius double precision, total_rainfall_liters double precision)
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ RETURN QUERY
+ SELECT to_char(daily_weather_data.measure_day, 'yyyy-mm') AS month, AVG(daily_weather_data.avg_temperature_celsius) as avg_temperature_celsius, SUM(daily_weather_data.sum_precipitation_mm_per_sqm) as total_rainfall_liters
+ FROM daily_weather_data
+ GROUP BY to_char(daily_weather_data.measure_day, 'yyyy-mm')
+ ORDER BY to_char(daily_weather_data.measure_day, 'yyyy-mm') DESC;
+END;
+$function$;
From 4a6dc2eeaa36b42e3d5eba61471a5715df80cd0d Mon Sep 17 00:00:00 2001
From: Jonas Jaszkowic
Date: Thu, 4 Jul 2024 17:25:55 +0200
Subject: [PATCH 11/12] fix: wrong import
---
supabase/functions/check_contact_request/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/supabase/functions/check_contact_request/index.ts b/supabase/functions/check_contact_request/index.ts
index 85a8d3b4..aaae7eb7 100644
--- a/supabase/functions/check_contact_request/index.ts
+++ b/supabase/functions/check_contact_request/index.ts
@@ -1,7 +1,7 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
-import { checkIfContactRequestIsAllowed } from "../_shared/checks.ts";
import { corsHeaders } from "../_shared/cors.ts";
import { loadEnvVars } from "../_shared/check-env.ts";
+import { checkIfContactRequestIsAllowed } from "../_shared/contact-request-checks.ts";
const ENV_VARS = [
"SUPABASE_URL",
From ceb5942ca31264e52e551826503676a58f239cb8 Mon Sep 17 00:00:00 2001
From: Jonas Jaszkowic
Date: Mon, 8 Jul 2024 16:20:56 +0200
Subject: [PATCH 12/12] feat: add max temperature
---
supabase/functions/gdk_stats/index.ts | 1 +
.../20240708094214_fix_db_stats_function.sql | 14 ++++++++++++++
2 files changed, 15 insertions(+)
create mode 100644 supabase/migrations/20240708094214_fix_db_stats_function.sql
diff --git a/supabase/functions/gdk_stats/index.ts b/supabase/functions/gdk_stats/index.ts
index 64eaee2f..cc156ed7 100644
--- a/supabase/functions/gdk_stats/index.ts
+++ b/supabase/functions/gdk_stats/index.ts
@@ -142,6 +142,7 @@ const getMonthlyWeather = async (): Promise => {
return data.map((month: any) => ({
month: month.month,
averageTemperatureCelsius: month.avg_temperature_celsius,
+ maximumTemperatureCelsius: month.max_temperature_celsius,
totalRainfallLiters: month.total_rainfall_liters,
}));
};
diff --git a/supabase/migrations/20240708094214_fix_db_stats_function.sql b/supabase/migrations/20240708094214_fix_db_stats_function.sql
new file mode 100644
index 00000000..820f38bf
--- /dev/null
+++ b/supabase/migrations/20240708094214_fix_db_stats_function.sql
@@ -0,0 +1,14 @@
+drop function if exists get_monthly_weather();
+
+CREATE OR REPLACE FUNCTION public.get_monthly_weather()
+ RETURNS TABLE(month text, avg_temperature_celsius double precision, max_temperature_celsius double precision, total_rainfall_liters double precision)
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ RETURN QUERY
+ SELECT to_char(daily_weather_data.measure_day, 'yyyy-mm') AS month, AVG(daily_weather_data.avg_temperature_celsius) as avg_temperature_celsius, MAX(daily_weather_data.avg_temperature_celsius) as max_temperature_celsius, SUM(daily_weather_data.sum_precipitation_mm_per_sqm) as total_rainfall_liters
+ FROM daily_weather_data
+ GROUP BY to_char(daily_weather_data.measure_day, 'yyyy-mm')
+ ORDER BY to_char(daily_weather_data.measure_day, 'yyyy-mm') DESC;
+END;
+$function$