Skip to content

Commit 11a23a7

Browse files
authored
Merge pull request #299 from technologiestiftung/staging
staging to master (baumkataster 2025)
2 parents f70cad4 + 86aab39 commit 11a23a7

13 files changed

+279
-73
lines changed

.github/workflows/deploy-to-supabase-production.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222

2323
- uses: supabase/setup-cli@v1
2424
with:
25-
version: 1.142.2
25+
version: 2.20.3
2626

2727
- run: |
2828
supabase link --project-ref $PRODUCTION_PROJECT_ID

.github/workflows/deploy-to-supabase-staging.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222

2323
- uses: supabase/setup-cli@v1
2424
with:
25-
version: 1.142.2
25+
version: 2.20.3
2626

2727
- run: |
2828
supabase link --project-ref $STAGING_PROJECT_ID

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,6 @@ cython_debug/
281281
sql/trees_trees_2022-11-09T12-28-18.sql
282282
sql/trees_bezirk_mitte_table_2022-11-08T16-32-58.sql
283283
sql/*.dump
284+
# user specific cursor rules
285+
.cursor/rules/*
286+
.cursor/mcp.json

.tool-versions

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
supabase-cli 2.20.3
2+
direnv 2.35.0

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,40 @@ npm test
168168

169169
On CI the Supabase is started automagically. See [.github/workflows/tests.yml](.github/workflows/tests.yml)
170170

171+
172+
## Database Caveats
173+
174+
To not hardcode values in the edge functions we are using materialized views.
175+
176+
- `most_frequent_tree_species`
177+
- `total_tree_species_count`
178+
- `waterings_with_location`
179+
180+
In case you need to restore the data from a backup you need to disable the triggers first.
181+
182+
```sql
183+
ALTER TABLE trees DISABLE TRIGGER tg_refresh_trees_count_mv;
184+
ALTER TABLE trees DISABLE TRIGGER tg_refresh_most_frequent_tree_species_mv;
185+
ALTER TABLE trees DISABLE TRIGGER tg_refresh_total_tree_species_count_mv;
186+
```
187+
188+
After restore is done, refresh the materialized views and re-enable our specific triggers.
189+
190+
```sql
191+
-- Manually refresh all materialized views
192+
REFRESH MATERIALIZED VIEW CONCURRENTLY total_tree_species_count;
193+
REFRESH MATERIALIZED VIEW CONCURRENTLY most_frequent_tree_species;
194+
REFRESH MATERIALIZED VIEW CONCURRENTLY trees_count;
195+
196+
-- Re-enable our specific triggers
197+
ALTER TABLE trees ENABLE TRIGGER tg_refresh_trees_count_mv;
198+
ALTER TABLE trees ENABLE TRIGGER tg_refresh_most_frequent_tree_species_mv;
199+
ALTER TABLE trees ENABLE TRIGGER tg_refresh_total_tree_species_count_mv;
200+
```
201+
202+
203+
204+
171205
## Contributors ✨
172206

173207
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
"scripts": {
77
"test": "jest --runInBand",
88
"lint": "eslint ./**/*.ts ",
9-
"format": "prettier ./**/*.ts --write"
9+
"format": "prettier ./**/*.ts --write",
10+
"dev:supabase:functions": "supabase functions serve --no-verify-jwt --env-file supabase/.env",
11+
"supabase:types:local": "supabase gen types --local > src/database.ts"
1012
},
1113
"engines": {
12-
"node": ">=18"
14+
"node": ">=20"
1315
},
1416
"dependencies": {
1517
"@supabase/supabase-js": "2.43.5"

src/database.ts

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,7 @@ export type Database = {
3434
id?: string
3535
user_id?: string
3636
}
37-
Relationships: [
38-
{
39-
foreignKeyName: "contact_requests_contact_id_fkey"
40-
columns: ["contact_id"]
41-
isOneToOne: false
42-
referencedRelation: "users"
43-
referencedColumns: ["id"]
44-
},
45-
{
46-
foreignKeyName: "contact_requests_user_id_fkey"
47-
columns: ["user_id"]
48-
isOneToOne: false
49-
referencedRelation: "users"
50-
referencedColumns: ["id"]
51-
},
52-
]
37+
Relationships: []
5338
}
5439
daily_weather_data: {
5540
Row: {
@@ -124,15 +109,7 @@ export type Database = {
124109
id?: string
125110
username?: string | null
126111
}
127-
Relationships: [
128-
{
129-
foreignKeyName: "fk_users_profiles"
130-
columns: ["id"]
131-
isOneToOne: false
132-
referencedRelation: "users"
133-
referencedColumns: ["id"]
134-
},
135-
]
112+
Relationships: []
136113
}
137114
radolan_data: {
138115
Row: {
@@ -368,7 +345,25 @@ export type Database = {
368345
}
369346
}
370347
Views: {
371-
[_ in never]: never
348+
most_frequent_tree_species: {
349+
Row: {
350+
gattung_deutsch: string | null
351+
percentage: number | null
352+
}
353+
Relationships: []
354+
}
355+
total_tree_species_count: {
356+
Row: {
357+
count: number | null
358+
}
359+
Relationships: []
360+
}
361+
trees_count: {
362+
Row: {
363+
count: number | null
364+
}
365+
Relationships: []
366+
}
372367
}
373368
Functions: {
374369
accumulated_weather_per_month: {
@@ -391,12 +386,20 @@ export type Database = {
391386
avg_wind_gust_speed_kmh: number
392387
}[]
393388
}
389+
calculate_adoptions: {
390+
Args: Record<PropertyKey, never>
391+
Returns: {
392+
total_adoptions: number
393+
very_thirsty_adoptions: number
394+
}[]
395+
}
394396
calculate_avg_waterings_per_month: {
395397
Args: Record<PropertyKey, never>
396398
Returns: {
397399
month: string
398400
watering_count: number
399401
avg_amount_per_watering: number
402+
total_sum: number
400403
}[]
401404
}
402405
calculate_top_tree_species: {
@@ -413,6 +416,15 @@ export type Database = {
413416
}
414417
Returns: number
415418
}
419+
get_monthly_weather: {
420+
Args: Record<PropertyKey, never>
421+
Returns: {
422+
month: string
423+
avg_temperature_celsius: number
424+
max_temperature_celsius: number
425+
total_rainfall_liters: number
426+
}[]
427+
}
416428
get_user_data_for_id: {
417429
Args: {
418430
u_id: string
@@ -430,6 +442,22 @@ export type Database = {
430442
watered: number
431443
}[]
432444
}
445+
get_waterings_with_location: {
446+
Args: Record<PropertyKey, never>
447+
Returns: {
448+
id: string
449+
lat: number
450+
lng: number
451+
amount: number
452+
timestamp: string
453+
}[]
454+
}
455+
is_username_taken: {
456+
Args: {
457+
given_username: string
458+
}
459+
Returns: boolean
460+
}
433461
remove_account: {
434462
Args: Record<PropertyKey, never>
435463
Returns: undefined
@@ -557,3 +585,18 @@ export type Enums<
557585
? PublicSchema["Enums"][PublicEnumNameOrOptions]
558586
: never
559587

588+
export type CompositeTypes<
589+
PublicCompositeTypeNameOrOptions extends
590+
| keyof PublicSchema["CompositeTypes"]
591+
| { schema: keyof Database },
592+
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
593+
schema: keyof Database
594+
}
595+
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
596+
: never = never,
597+
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
598+
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
599+
: PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"]
600+
? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
601+
: never
602+

supabase/config.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ project_id = "giessdenkiez-de-supabase"
77
port = 54321
88
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
99
# endpoints. public and storage are always included.
10-
schemas = []
11-
# Extra schemas to add to the search_path of every request.
12-
extra_search_path = ["extensions"]
10+
schemas = ["public", "storage", "graphql_public"]
11+
extra_search_path = ["public", "extensions"]
1312
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
1413
# for accidental or malicious requests.
1514
max_rows = 10000

supabase/functions/_shared/errors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ export enum ErrorTypes {
55
GdkStatsAdoption = "gdk_stats_adoptions",
66
GdkStatsTreeSpecie = "gdk_stats_tree_species",
77
GdkStatsWeather = "gdk_stats_weather",
8+
GdkStatsTreeCount = "gdk_stats_tree_count",
9+
GdkStatsTreeSpeciesCount = "gdk_stats_tree_species_count",
10+
GdkStatsMostFrequentTreeSpecies = "gdk_stats_most_frequent_tree_species",
811
}
912

1013
export class GdkError extends Error {

supabase/functions/gdk_stats/index.ts

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,48 +15,63 @@ const ENV_VARS = ["SUPABASE_URL", "SUPABASE_SERVICE_ROLE_KEY", "PUMPS_URL"];
1515
const [SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, PUMPS_URL] =
1616
loadEnvVars(ENV_VARS);
1717

18-
// As trees table barely changes, we can hardcode the values
19-
// It would be too expensive to calculate on each request
20-
21-
// SELECT COUNT(1) FROM trees;
22-
const TREE_COUNT = 885825;
23-
24-
// SELECT trees.gattung_deutsch, (COUNT(1) * 100.0) / (SELECT COUNT(1) FROM trees) AS percentage
25-
// FROM trees
26-
// GROUP BY trees.gattung_deutsch
27-
// ORDER BY COUNT(1) DESC
28-
// LIMIT 20;
29-
const MOST_FREQUENT_TREE_SPECIES: TreeSpecies[] = [
30-
{ speciesName: "AHORN", percentage: 22.8128580701605848 },
31-
{ speciesName: "LINDE", percentage: 21.5930911861823724 },
32-
{ speciesName: "EICHE", percentage: 10.5370699630288149 },
33-
{ speciesName: undefined, percentage: 4.1923630513927695 },
34-
{ speciesName: "ROBINIE", percentage: 3.9515705698078063 },
35-
{ speciesName: "ROSSKASTANIE", percentage: 3.6574944260999633 },
36-
{ speciesName: "BIRKE", percentage: 3.610419665283775 },
37-
{ speciesName: "HAINBUCHE", percentage: 3.4514717918324726 },
38-
{ speciesName: "PLATANE", percentage: 3.3499844777467333 },
39-
{ speciesName: "PAPPEL", percentage: 2.8882679987582197 },
40-
{ speciesName: "ESCHE", percentage: 2.7732339909124263 },
41-
{ speciesName: "KIEFER", percentage: 2.4801738492365874 },
42-
{ speciesName: "ULME", percentage: 1.946998560663788 },
43-
{ speciesName: "BUCHE", percentage: 1.7521519487483419 },
44-
{ speciesName: "HASEL", percentage: 1.1728050122766912 },
45-
{ speciesName: "WEIßDORN", percentage: 1.1243755820844975 },
46-
{ speciesName: "WEIDE", percentage: 1.0893799565376909 },
47-
{ speciesName: "MEHLBEERE", percentage: 0.90469336494228544013 },
48-
{ speciesName: "ERLE", percentage: 0.80907628481923630514 },
49-
{ speciesName: "APFEL", percentage: 0.70092851296813704739 },
50-
];
51-
52-
// SELECT COUNT(gattung_deutsch) FROM trees GROUP BY gattung_deutsch;
53-
const TOTAL_TREE_SPECIES_COUNT = 97;
54-
5518
const supabaseServiceRoleClient = createClient(
5619
SUPABASE_URL,
5720
SUPABASE_SERVICE_ROLE_KEY
5821
);
5922

23+
const getMostFrequentTreeSpecies = async (): Promise<TreeSpecies[]> => {
24+
const { data, error } = await supabaseServiceRoleClient
25+
.from("most_frequent_tree_species")
26+
.select("*");
27+
28+
// rename all fields from gattung_deutsch to speciesName
29+
const renamedData = data.map((species) => ({
30+
speciesName: species.gattung_deutsch,
31+
percentage: species.percentage,
32+
}));
33+
34+
if (error) {
35+
throw new GdkError(
36+
error.message,
37+
ErrorTypes.GdkStatsMostFrequentTreeSpecies
38+
);
39+
}
40+
41+
return renamedData;
42+
};
43+
44+
const getTotalTreeSpeciesCount = async (): Promise<number> => {
45+
const { data, error } = await supabaseServiceRoleClient
46+
.from("total_tree_species_count")
47+
.select("*");
48+
49+
if (error) {
50+
throw new GdkError(error.message, ErrorTypes.GdkStatsTreeSpeciesCount);
51+
}
52+
53+
return data[0].count ?? 0;
54+
};
55+
56+
const getTreeCount = async (): Promise<number> => {
57+
const { data, error } = await supabaseServiceRoleClient
58+
.from("trees_count")
59+
.select("count");
60+
61+
if (error) {
62+
throw new GdkError(error.message, ErrorTypes.GdkStatsTreeCount);
63+
}
64+
65+
if (data === null) {
66+
throw new GdkError(
67+
"Could not fetch count of trees_count table",
68+
ErrorTypes.GdkStatsTreeCount
69+
);
70+
}
71+
72+
return data[0].count ?? 0;
73+
};
74+
6075
const getUserProfilesCount = async (): Promise<number> => {
6176
const { count } = await supabaseServiceRoleClient
6277
.from("profiles")
@@ -181,6 +196,9 @@ const handler = async (request: Request): Promise<Response> => {
181196
monthlyWaterings,
182197
waterings,
183198
monthlyWeather,
199+
treeCount,
200+
totalTreeSpeciesCount,
201+
mostFrequentTreeSpecies,
184202
] = await Promise.all([
185203
getUserProfilesCount(),
186204
getWateringsCount(),
@@ -189,17 +207,20 @@ const handler = async (request: Request): Promise<Response> => {
189207
getMonthlyWaterings(),
190208
getWaterings(),
191209
getMonthlyWeather(),
210+
getTreeCount(),
211+
getTotalTreeSpeciesCount(),
212+
getMostFrequentTreeSpecies(),
192213
]);
193214

194215
const stats: GdkStats = {
195-
numTrees: TREE_COUNT,
216+
numTrees: treeCount,
196217
numPumps: numPumps,
197218
numActiveUsers: usersCount,
198219
numWateringsThisYear: wateringsCount,
199220
monthlyWaterings: monthlyWaterings,
200221
treeAdoptions: treeAdoptions,
201-
mostFrequentTreeSpecies: MOST_FREQUENT_TREE_SPECIES,
202-
totalTreeSpeciesCount: TOTAL_TREE_SPECIES_COUNT,
222+
mostFrequentTreeSpecies: mostFrequentTreeSpecies,
223+
totalTreeSpeciesCount: totalTreeSpeciesCount,
203224
waterings: waterings,
204225
monthlyWeather: monthlyWeather,
205226
};

0 commit comments

Comments
 (0)