From 74034e57177a7a84ed58c42dfacddabd9f2f6e96 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 17:24:11 +0200 Subject: [PATCH 01/10] fix(profile): Make usernames case insensitive This is done by using the citext extensions. Once we have imported all the users from auth0 we will remove the uuid trigger function and only have the DB reject duplicate names --- ...30419150648_unique_username_case_insensitive.sql | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 supabase/migrations/20230419150648_unique_username_case_insensitive.sql diff --git a/supabase/migrations/20230419150648_unique_username_case_insensitive.sql b/supabase/migrations/20230419150648_unique_username_case_insensitive.sql new file mode 100644 index 00000000..4c96effc --- /dev/null +++ b/supabase/migrations/20230419150648_unique_username_case_insensitive.sql @@ -0,0 +1,13 @@ +CREATE EXTENSION IF NOT EXISTS "citext" WITH SCHEMA "extensions"; + +ALTER TABLE "public"."profiles" + DROP CONSTRAINT "username_length_constraint"; + +ALTER TABLE "public"."profiles" + ALTER COLUMN "username" SET data TYPE citext USING "username"::citext; + +ALTER TABLE "public"."profiles" + ADD CONSTRAINT "username_length_constraint" CHECK (((length((username)::text) >= 3) AND (length((username)::text) <= 50))) NOT valid; + +ALTER TABLE "public"."profiles" validate CONSTRAINT "username_length_constraint"; + From e9a3c8013dd1239f092233c5356f4ce235d95480 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:08:27 +0200 Subject: [PATCH 02/10] test(utils): Update test utils for schema tests --- __test-utils/postgres.ts | 7 ++++--- __test-utils/req-test-token.ts | 20 +++++++++++++------- __test-utils/supabase.ts | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 __test-utils/supabase.ts diff --git a/__test-utils/postgres.ts b/__test-utils/postgres.ts index fedd3b3b..2a5e19a6 100644 --- a/__test-utils/postgres.ts +++ b/__test-utils/postgres.ts @@ -16,15 +16,16 @@ export async function truncateTreesAdopted() { sql.end(); } -export async function createWateredTrees() { +export async function createWateredTrees(userId?: string, userName?: string) { const sql = postgres(url); + const randomText = sql`md5(random()::text)`; await sql` INSERT INTO trees_watered (uuid, amount, timestamp, username, tree_id) SELECT - md5(random()::text), + ${userId ? userId : sql`extensions.uuid_generate_v4()::text`}, random() * 10, NOW() - (random() * INTERVAL '7 days'), - md5(random()::text), + ${userName ? userName : randomText}, id FROM trees diff --git a/__test-utils/req-test-token.ts b/__test-utils/req-test-token.ts index dc918b32..7dfcc5e5 100644 --- a/__test-utils/req-test-token.ts +++ b/__test-utils/req-test-token.ts @@ -1,9 +1,10 @@ +import { SignupResponse } from "../_types/user"; +import { SUPABASE_ANON_KEY, SUPABASE_URL } from "./supabase"; const issuer = process.env.issuer || ""; const client_id = process.env.client_id || ""; const client_secret = process.env.client_secret || ""; const audience = process.env.audience || ""; -const SUPABASE_URL = process.env.SUPABASE_URL || "http://localhost:54321"; -const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || ""; + export async function requestAuth0TestToken() { const response = await fetch(`${issuer}oauth/token`, { method: "POST", @@ -47,14 +48,15 @@ export async function requestSupabaseTestToken( const json = await response.text(); throw new Error(`Could not get test token, ${json}`); } - const json = (await response.json()) as { - access_token: string; - user: { id: string }; - }; + const json = (await response.json()) as SignupResponse; return json.access_token; } -export async function createSupabaseUser(email: string, password: string) { +export async function createSupabaseUser( + email: string, + password: string, + opts?: { returnFullUser: boolean } +) { const response = await fetch(`${SUPABASE_URL}/auth/v1/signup`, { method: "POST", headers: { @@ -67,6 +69,7 @@ export async function createSupabaseUser(email: string, password: string) { }), }); if (!response.ok) { + console.log(response.status); const json = await response.text(); throw new Error(`Could not create test user, ${json}`); } @@ -74,5 +77,8 @@ export async function createSupabaseUser(email: string, password: string) { access_token: string; user: { id: string }; }; + if (opts?.returnFullUser) { + return json; + } return json.access_token; } diff --git a/__test-utils/supabase.ts b/__test-utils/supabase.ts new file mode 100644 index 00000000..ffea419c --- /dev/null +++ b/__test-utils/supabase.ts @@ -0,0 +1,16 @@ +import { createClient } from "@supabase/supabase-js"; +import { Database } from "../_types/database"; +export const SUPABASE_URL = + process.env.SUPABASE_URL || "http://localhost:54321"; +export const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || ""; +export const SUPABASE_SERVICE_ROLE_KEY = + process.env.SUPABASE_SERVICE_ROLE_KEY || ""; + +export const supabaseAnonClient = createClient( + SUPABASE_URL, + SUPABASE_ANON_KEY +); +export const supabaseServiceRoleClient = createClient( + SUPABASE_URL, + SUPABASE_SERVICE_ROLE_KEY +); From f2c3f3e9491784bc4ef89526eadff6fa6f27cb5a Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:09:12 +0200 Subject: [PATCH 03/10] fix(schema): Update outdated types --- _types/database.ts | 857 ++++++++++++++++++++++++--------------------- 1 file changed, 454 insertions(+), 403 deletions(-) diff --git a/_types/database.ts b/_types/database.ts index a4e7c8d3..5c6cbd13 100644 --- a/_types/database.ts +++ b/_types/database.ts @@ -1,407 +1,458 @@ export type Json = - | string - | number - | boolean - | null - | { [key: string]: Json } - | Json[]; + | string + | number + | boolean + | null + | { [key: string]: Json } + | Json[] export interface Database { - graphql_public: { - Tables: { - [_ in never]: never; - }; - Views: { - [_ in never]: never; - }; - Functions: { - graphql: { - Args: { - operationName: string; - query: string; - variables: Json; - extensions: Json; - }; - Returns: Json; - }; - }; - Enums: { - [_ in never]: never; - }; - }; - public: { - Tables: { - profiles: { - Row: { - id: string; - username: string | null; - }; - Insert: { - id: string; - username?: string | null; - }; - Update: { - id?: string; - username?: string | null; - }; - }; - radolan_data: { - Row: { - geom_id: number | null; - id: number; - measured_at: string | null; - value: number | null; - }; - Insert: { - geom_id?: number | null; - id?: number; - measured_at?: string | null; - value?: number | null; - }; - Update: { - geom_id?: number | null; - id?: number; - measured_at?: string | null; - value?: number | null; - }; - }; - radolan_geometry: { - Row: { - centroid: unknown | null; - geometry: unknown | null; - id: number; - }; - Insert: { - centroid?: unknown | null; - geometry?: unknown | null; - id?: number; - }; - Update: { - centroid?: unknown | null; - geometry?: unknown | null; - id?: number; - }; - }; - radolan_harvester: { - Row: { - collection_date: string | null; - end_date: string | null; - id: number; - start_date: string | null; - }; - Insert: { - collection_date?: string | null; - end_date?: string | null; - id?: number; - start_date?: string | null; - }; - Update: { - collection_date?: string | null; - end_date?: string | null; - id?: number; - start_date?: string | null; - }; - }; - radolan_temp: { - Row: { - geometry: unknown | null; - id: number; - measured_at: string | null; - value: number | null; - }; - Insert: { - geometry?: unknown | null; - id?: number; - measured_at?: string | null; - value?: number | null; - }; - Update: { - geometry?: unknown | null; - id?: number; - measured_at?: string | null; - value?: number | null; - }; - }; - trees: { - Row: { - adopted: string | null; - artbot: string | null; - artdtsch: string | null; - baumhoehe: string | null; - bezirk: string | null; - caretaker: string | null; - eigentuemer: string | null; - gattung: string | null; - gattungdeutsch: string | null; - geom: unknown | null; - gmlid: string | null; - hausnr: string | null; - id: string; - kennzeich: string | null; - kronedurch: string | null; - lat: string | null; - lng: string | null; - pflanzjahr: number | null; - radolan_days: number[] | null; - radolan_sum: number | null; - stammumfg: string | null; - standalter: string | null; - standortnr: string | null; - strname: string | null; - type: string | null; - watered: string | null; - zusatz: string | null; - }; - Insert: { - adopted?: string | null; - artbot?: string | null; - artdtsch?: string | null; - baumhoehe?: string | null; - bezirk?: string | null; - caretaker?: string | null; - eigentuemer?: string | null; - gattung?: string | null; - gattungdeutsch?: string | null; - geom?: unknown | null; - gmlid?: string | null; - hausnr?: string | null; - id: string; - kennzeich?: string | null; - kronedurch?: string | null; - lat?: string | null; - lng?: string | null; - pflanzjahr?: number | null; - radolan_days?: number[] | null; - radolan_sum?: number | null; - stammumfg?: string | null; - standalter?: string | null; - standortnr?: string | null; - strname?: string | null; - type?: string | null; - watered?: string | null; - zusatz?: string | null; - }; - Update: { - adopted?: string | null; - artbot?: string | null; - artdtsch?: string | null; - baumhoehe?: string | null; - bezirk?: string | null; - caretaker?: string | null; - eigentuemer?: string | null; - gattung?: string | null; - gattungdeutsch?: string | null; - geom?: unknown | null; - gmlid?: string | null; - hausnr?: string | null; - id?: string; - kennzeich?: string | null; - kronedurch?: string | null; - lat?: string | null; - lng?: string | null; - pflanzjahr?: number | null; - radolan_days?: number[] | null; - radolan_sum?: number | null; - stammumfg?: string | null; - standalter?: string | null; - standortnr?: string | null; - strname?: string | null; - type?: string | null; - watered?: string | null; - zusatz?: string | null; - }; - }; - trees_adopted: { - Row: { - id: number; - tree_id: string; - uuid: string | null; - }; - Insert: { - id?: number; - tree_id: string; - uuid?: string | null; - }; - Update: { - id?: number; - tree_id?: string; - uuid?: string | null; - }; - }; - trees_watered: { - Row: { - amount: number; - id: number; - time: string | null; - timestamp: string; - tree_id: string; - username: string | null; - uuid: string | null; - }; - Insert: { - amount: number; - id?: number; - time?: string | null; - timestamp: string; - tree_id: string; - username?: string | null; - uuid?: string | null; - }; - Update: { - amount?: number; - id?: number; - time?: string | null; - timestamp?: string; - tree_id?: string; - username?: string | null; - uuid?: string | null; - }; - }; - }; - Views: { - [_ in never]: never; - }; - Functions: { - count_by_age: { - Args: { start_year: number; end_year: number }; - Returns: number; - }; - get_watered_and_adopted: { - Args: Record; - Returns: { tree_id: string; adopted: number; watered: number }[]; - }; - }; - Enums: { - [_ in never]: never; - }; - }; - storage: { - Tables: { - buckets: { - Row: { - created_at: string | null; - id: string; - name: string; - owner: string | null; - public: boolean | null; - updated_at: string | null; - }; - Insert: { - created_at?: string | null; - id: string; - name: string; - owner?: string | null; - public?: boolean | null; - updated_at?: string | null; - }; - Update: { - created_at?: string | null; - id?: string; - name?: string; - owner?: string | null; - public?: boolean | null; - updated_at?: string | null; - }; - }; - migrations: { - Row: { - executed_at: string | null; - hash: string; - id: number; - name: string; - }; - Insert: { - executed_at?: string | null; - hash: string; - id: number; - name: string; - }; - Update: { - executed_at?: string | null; - hash?: string; - id?: number; - name?: string; - }; - }; - objects: { - Row: { - bucket_id: string | null; - created_at: string | null; - id: string; - last_accessed_at: string | null; - metadata: Json | null; - name: string | null; - owner: string | null; - path_tokens: string[] | null; - updated_at: string | null; - }; - Insert: { - bucket_id?: string | null; - created_at?: string | null; - id?: string; - last_accessed_at?: string | null; - metadata?: Json | null; - name?: string | null; - owner?: string | null; - path_tokens?: string[] | null; - updated_at?: string | null; - }; - Update: { - bucket_id?: string | null; - created_at?: string | null; - id?: string; - last_accessed_at?: string | null; - metadata?: Json | null; - name?: string | null; - owner?: string | null; - path_tokens?: string[] | null; - updated_at?: string | null; - }; - }; - }; - Views: { - [_ in never]: never; - }; - Functions: { - extension: { - Args: { name: string }; - Returns: string; - }; - filename: { - Args: { name: string }; - Returns: string; - }; - foldername: { - Args: { name: string }; - Returns: string[]; - }; - get_size_by_bucket: { - Args: Record; - Returns: { size: number; bucket_id: string }[]; - }; - search: { - Args: { - prefix: string; - bucketname: string; - limits: number; - levels: number; - offsets: number; - search: string; - sortcolumn: string; - sortorder: string; - }; - Returns: { - name: string; - id: string; - updated_at: string; - created_at: string; - last_accessed_at: string; - metadata: Json; - }[]; - }; - }; - Enums: { - [_ in never]: never; - }; - }; + graphql_public: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + graphql: { + Args: { + operationName?: string + query?: string + variables?: Json + extensions?: Json + } + Returns: Json + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + public: { + Tables: { + profiles: { + Row: { + id: string + username: string | null + } + Insert: { + id: string + username?: string | null + } + Update: { + id?: string + username?: string | null + } + } + radolan_data: { + Row: { + geom_id: number | null + id: number + measured_at: string | null + value: number | null + } + Insert: { + geom_id?: number | null + id?: number + measured_at?: string | null + value?: number | null + } + Update: { + geom_id?: number | null + id?: number + measured_at?: string | null + value?: number | null + } + } + radolan_geometry: { + Row: { + centroid: unknown | null + geometry: unknown | null + id: number + } + Insert: { + centroid?: unknown | null + geometry?: unknown | null + id?: number + } + Update: { + centroid?: unknown | null + geometry?: unknown | null + id?: number + } + } + radolan_harvester: { + Row: { + collection_date: string | null + end_date: string | null + id: number + start_date: string | null + } + Insert: { + collection_date?: string | null + end_date?: string | null + id?: number + start_date?: string | null + } + Update: { + collection_date?: string | null + end_date?: string | null + id?: number + start_date?: string | null + } + } + radolan_temp: { + Row: { + geometry: unknown | null + id: number + measured_at: string | null + value: number | null + } + Insert: { + geometry?: unknown | null + id?: number + measured_at?: string | null + value?: number | null + } + Update: { + geometry?: unknown | null + id?: number + measured_at?: string | null + value?: number | null + } + } + trees: { + Row: { + adopted: string | null + artbot: string | null + artdtsch: string | null + baumhoehe: string | null + bezirk: string | null + caretaker: string | null + eigentuemer: string | null + gattung: string | null + gattungdeutsch: string | null + geom: unknown | null + gmlid: string | null + hausnr: string | null + id: string + kennzeich: string | null + kronedurch: string | null + lat: string | null + lng: string | null + pflanzjahr: number | null + radolan_days: number[] | null + radolan_sum: number | null + stammumfg: string | null + standalter: string | null + standortnr: string | null + strname: string | null + type: string | null + watered: string | null + zusatz: string | null + } + Insert: { + adopted?: string | null + artbot?: string | null + artdtsch?: string | null + baumhoehe?: string | null + bezirk?: string | null + caretaker?: string | null + eigentuemer?: string | null + gattung?: string | null + gattungdeutsch?: string | null + geom?: unknown | null + gmlid?: string | null + hausnr?: string | null + id: string + kennzeich?: string | null + kronedurch?: string | null + lat?: string | null + lng?: string | null + pflanzjahr?: number | null + radolan_days?: number[] | null + radolan_sum?: number | null + stammumfg?: string | null + standalter?: string | null + standortnr?: string | null + strname?: string | null + type?: string | null + watered?: string | null + zusatz?: string | null + } + Update: { + adopted?: string | null + artbot?: string | null + artdtsch?: string | null + baumhoehe?: string | null + bezirk?: string | null + caretaker?: string | null + eigentuemer?: string | null + gattung?: string | null + gattungdeutsch?: string | null + geom?: unknown | null + gmlid?: string | null + hausnr?: string | null + id?: string + kennzeich?: string | null + kronedurch?: string | null + lat?: string | null + lng?: string | null + pflanzjahr?: number | null + radolan_days?: number[] | null + radolan_sum?: number | null + stammumfg?: string | null + standalter?: string | null + standortnr?: string | null + strname?: string | null + type?: string | null + watered?: string | null + zusatz?: string | null + } + } + trees_adopted: { + Row: { + id: number + tree_id: string + uuid: string | null + } + Insert: { + id?: number + tree_id: string + uuid?: string | null + } + Update: { + id?: number + tree_id?: string + uuid?: string | null + } + } + trees_watered: { + Row: { + amount: number + id: number + time: string | null + timestamp: string + tree_id: string + username: string | null + uuid: string | null + } + Insert: { + amount: number + id?: number + time?: string | null + timestamp: string + tree_id: string + username?: string | null + uuid?: string | null + } + Update: { + amount?: number + id?: number + time?: string | null + timestamp?: string + tree_id?: string + username?: string | null + uuid?: string | null + } + } + } + Views: { + [_ in never]: never + } + Functions: { + count_by_age: { + Args: { + start_year: number + end_year: number + } + Returns: number + } + get_watered_and_adopted: { + Args: Record + Returns: { + tree_id: string + adopted: number + watered: number + }[] + } + remove_account: { + Args: Record + Returns: undefined + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + storage: { + Tables: { + buckets: { + Row: { + allowed_mime_types: string[] | null + avif_autodetection: boolean | null + created_at: string | null + file_size_limit: number | null + id: string + name: string + owner: string | null + public: boolean | null + updated_at: string | null + } + Insert: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id: string + name: string + owner?: string | null + public?: boolean | null + updated_at?: string | null + } + Update: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id?: string + name?: string + owner?: string | null + public?: boolean | null + updated_at?: string | null + } + } + migrations: { + Row: { + executed_at: string | null + hash: string + id: number + name: string + } + Insert: { + executed_at?: string | null + hash: string + id: number + name: string + } + Update: { + executed_at?: string | null + hash?: string + id?: number + name?: string + } + } + objects: { + Row: { + bucket_id: string | null + created_at: string | null + id: string + last_accessed_at: string | null + metadata: Json | null + name: string | null + owner: string | null + path_tokens: string[] | null + updated_at: string | null + version: string | null + } + Insert: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + Update: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + } + } + Views: { + [_ in never]: never + } + Functions: { + can_insert_object: { + Args: { + bucketid: string + name: string + owner: string + metadata: Json + } + Returns: undefined + } + extension: { + Args: { + name: string + } + Returns: string + } + filename: { + Args: { + name: string + } + Returns: string + } + foldername: { + Args: { + name: string + } + Returns: string[] + } + get_size_by_bucket: { + Args: Record + Returns: { + size: number + bucket_id: string + }[] + } + search: { + Args: { + prefix: string + bucketname: string + limits?: number + levels?: number + offsets?: number + search?: string + sortcolumn?: string + sortorder?: string + } + Returns: { + name: string + id: string + updated_at: string + created_at: string + last_accessed_at: string + metadata: Json + }[] + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } } + From be85f2bd7dcac3e4c7beed8ec846828dd09d20a2 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:09:19 +0200 Subject: [PATCH 04/10] test(utils): Update test utils for schema tests --- _types/user.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 _types/user.ts diff --git a/_types/user.ts b/_types/user.ts new file mode 100644 index 00000000..c3ad19e9 --- /dev/null +++ b/_types/user.ts @@ -0,0 +1,38 @@ +export interface SignupResponse { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + user: User; +} +export interface User { + id: string; + aud: string; + role: string; + email: string; + email_confirmed_at: string; + phone: string; + last_sign_in_at: string; + app_metadata: AppMetadata; + user_metadata: Record; + identities?: IdentitiesEntity[] | null; + created_at: string; + updated_at: string; +} +export interface AppMetadata { + provider: string; + providers?: string[] | null; +} +export interface IdentitiesEntity { + id: string; + user_id: string; + identity_data: IdentityData; + provider: string; + last_sign_in_at: string; + created_at: string; + updated_at: string; +} +export interface IdentityData { + email: string; + sub: string; +} From 6f69042eaa63146e04fd4d6cb71e8f982e396270 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:24:29 +0200 Subject: [PATCH 05/10] fix(schema): Update call to uuid generate The function was not able to find the uuid_generate_v4 function --- ...230419165714_fix_username_uuid_trigger.sql | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 supabase/migrations/20230419165714_fix_username_uuid_trigger.sql diff --git a/supabase/migrations/20230419165714_fix_username_uuid_trigger.sql b/supabase/migrations/20230419165714_fix_username_uuid_trigger.sql new file mode 100644 index 00000000..ba642211 --- /dev/null +++ b/supabase/migrations/20230419165714_fix_username_uuid_trigger.sql @@ -0,0 +1,21 @@ +SET check_function_bodies = OFF; + +CREATE OR REPLACE FUNCTION public.username_append_uuid() + RETURNS TRIGGER + LANGUAGE plpgsql + AS $function$ +BEGIN + IF EXISTS( + SELECT + 1 + FROM + public.profiles + WHERE + username = NEW.username) THEN + NEW.username := NEW.username || '-' || TRIM(BOTH FROM SUBSTRING( + LEFT(extensions.uuid_generate_v4()::text, 8), 1, 6)); +END IF; + RETURN NEW; +END; +$function$; + From 28110dd850e9000deadf435e78688443ca23d2cb Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:24:52 +0200 Subject: [PATCH 06/10] chore(schema): Remove logging from delete function --- ...0230419180719_chore_make_delete_silent.sql | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 supabase/migrations/20230419180719_chore_make_delete_silent.sql diff --git a/supabase/migrations/20230419180719_chore_make_delete_silent.sql b/supabase/migrations/20230419180719_chore_make_delete_silent.sql new file mode 100644 index 00000000..1aac87c3 --- /dev/null +++ b/supabase/migrations/20230419180719_chore_make_delete_silent.sql @@ -0,0 +1,31 @@ +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.delete_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +DECLARE + row_count int; +BEGIN + DELETE FROM public.profiles p + WHERE p.id = OLD.id; +-- IF found THEN +-- GET DIAGNOSTICS row_count = ROW_COUNT; +-- RAISE NOTICE 'DELETEd % row(s) FROM profiles', row_count; +-- END IF; + UPDATE + trees_watered + SET + uuid = NULL, + username = NULL + WHERE + uuid = OLD.id::text; + DELETE FROM trees_adopted ta + WHERE ta.uuid = OLD.id::text; + RETURN OLD; +END; +$function$ +; + + From c238268adcc9a5841f43433f6cfc3b8118994a72 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:27:52 +0200 Subject: [PATCH 07/10] test(schema): Verify functions in schema These test check some beahaviour in the schema - adding uuid to username on duplicates (needed 4 migration from auth0) - cascading removal of users data from the database - change of username on trees_watered on change of username in profile --- __tests__/schema.test.ts | 187 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 __tests__/schema.test.ts diff --git a/__tests__/schema.test.ts b/__tests__/schema.test.ts new file mode 100644 index 00000000..91463d48 --- /dev/null +++ b/__tests__/schema.test.ts @@ -0,0 +1,187 @@ +import { + deleteSupabaseUser, + truncateTreesWaterd, +} from "../__test-utils/postgres"; +import { + SUPABASE_ANON_KEY, + SUPABASE_URL, + supabaseAnonClient, + supabaseServiceRoleClient, +} from "../__test-utils/supabase"; +describe("misc test testing the schema function of the database", () => { + test("inserting an existing username should alter the new name and add a uuid at end", async () => { + const email1 = "someone@email.com"; + const email2 = "someone@foo.com"; + await deleteSupabaseUser(email1); + await deleteSupabaseUser(email2); + const password = "12345678"; + const { data: user1, error } = await supabaseAnonClient.auth.signUp({ + email: email1, + password: password, + }); + const { data: user2, error: error2 } = await supabaseAnonClient.auth.signUp( + { + email: email2, + password: password, + } + ); + expect(error).toBeNull(); + expect(user1).toBeDefined(); + expect(error2).toBeNull(); + expect(user2).toBeDefined(); + + const { data: users, error: usersError } = await supabaseAnonClient + .from("profiles") + .select("*") + .in("id", [user1?.user?.id, user2?.user?.id]); + + expect(usersError).toBeNull(); + expect(users).toHaveLength(2); + expect(users?.[0].username).toBe("someone"); + expect(users?.[1].username).not.toBe("someone"); + expect(users?.[1].username).toContain("someone-"); + expect(users?.[1].username).toMatch(/^someone-[a-zA-Z0-9]{6}$/); + await deleteSupabaseUser(email1); + await deleteSupabaseUser(email2); + }); + + test("a user should be able to remove its account and his associated data", async () => { + const email = "user@email.com"; + await deleteSupabaseUser(email); // clean up before running + const { data, error } = await supabaseAnonClient.auth.signUp({ + email: email, + password: "12345678", + }); + expect(error).toBeNull(); + expect(data).toBeDefined(); + const { data: trees, error: treesError } = await supabaseAnonClient + .from("trees") + .select("*") + .limit(10); + expect(treesError).toBeNull(); + expect(trees).toHaveLength(10); + + const { data: adoptedTrees, error: adoptedTreesError } = + await supabaseServiceRoleClient + .from("trees_adopted") + .insert( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + trees!.map((tree) => ({ + uuid: data.user?.id, + tree_id: tree.id, + })) + ) + .select("*"); + expect(adoptedTreesError).toBeNull(); + expect(adoptedTrees).toHaveLength(10); + const { data: userTrees, error: userTreesError } = + await supabaseServiceRoleClient + .from("trees_watered") + .insert( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + trees!.map((tree) => ({ + uuid: data.user?.id, + amount: 1, + timestamp: new Date().toISOString(), + username: "user", + tree_id: tree.id, + })) + ) + .select("*"); + expect(userTreesError).toBeNull(); + expect(userTrees).toHaveLength(10); + + // since wa can not pass the token to our supabase client, we need to use fetch directly + const response = await fetch(`${SUPABASE_URL}/rest/v1/rpc/remove_account`, { + method: "POST", + headers: { + apikey: SUPABASE_ANON_KEY, + "Content-Type": "application/json", + Authorization: `Bearer ${data.session?.access_token}`, + }, + }); + expect(response.ok).toBeTruthy(); + expect(response.status).toBe(204); + const { data: treesAfter, error: treesAfterError } = + await supabaseAnonClient + .from("trees_watered") + .select("*") + .eq("uuid", data.user?.id); + expect(treesAfterError).toBeNull(); + expect(treesAfter).toHaveLength(0); + + const { data: adoptedTreesAfter, error: adoptedTreesAfterError } = + await supabaseAnonClient + .from("trees_adopted") + .select("*") + .eq("uuid", data.user?.id); + expect(adoptedTreesAfterError).toBeNull(); + expect(adoptedTreesAfter).toHaveLength(0); + await truncateTreesWaterd(); + }); + + test("if a user changes his username all the usernames on the trees_watered table should change too", async () => { + const email = "foo@bar.com"; + await deleteSupabaseUser(email); + await truncateTreesWaterd(); + const { data, error } = await supabaseAnonClient.auth.signUp({ + email: email, + password: "12345678", + }); + expect(error).toBeNull(); + expect(data).toBeDefined(); + const { data: trees, error: treesError } = await supabaseAnonClient + .from("trees") + .select("*") + .limit(10); + expect(treesError).toBeNull(); + expect(trees).toHaveLength(10); + + const { data: adoptedTrees, error: adoptedTreesError } = + await supabaseServiceRoleClient + .from("trees_watered") + .insert( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + trees!.map((tree) => ({ + uuid: data.user?.id, + tree_id: tree.id, + amount: 1, + timestamp: new Date().toISOString(), + username: "foo", + })) + ) + .select("*"); + expect(adoptedTreesError).toBeNull(); + expect(adoptedTrees).toHaveLength(10); + + // since we cant pass our access token to change our username to our anon client we use fetch directly + const changeResponse = await fetch( + `${SUPABASE_URL}/rest/v1/profiles?id=eq.${data?.user?.id}`, + { + method: "PATCH", + headers: { + apikey: SUPABASE_ANON_KEY, + "Content-Type": "application/json", + Authorization: `Bearer ${data.session?.access_token}`, + }, + body: JSON.stringify({ + username: "bar", + }), + } + ); + + expect(changeResponse.ok).toBeTruthy(); + expect(changeResponse.status).toBe(204); + + const { data: treesAfter, error: treesAfterError } = + await supabaseServiceRoleClient + .from("trees_watered") + .select("*") + .eq("username", "bar"); + + expect(treesAfterError).toBeNull(); + expect(treesAfter).toHaveLength(10); + await deleteSupabaseUser(email); + await truncateTreesWaterd(); + }); +}); From 0c2096f1a3afef01339cebc3887b58f05030537e Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:28:16 +0200 Subject: [PATCH 08/10] test: Boilerplate of schema tests. WIP --- supabase/tests/database/unique_names.test.sql | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 supabase/tests/database/unique_names.test.sql diff --git a/supabase/tests/database/unique_names.test.sql b/supabase/tests/database/unique_names.test.sql new file mode 100644 index 00000000..2f97e1ba --- /dev/null +++ b/supabase/tests/database/unique_names.test.sql @@ -0,0 +1,6 @@ +begin; +select plan(1); -- only one statement to run + + +SELECT * FROM finish(); +rollback; From f0f3e78a14e17844b78445c756c43b32451b9f99 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Wed, 19 Apr 2023 20:28:44 +0200 Subject: [PATCH 09/10] chore: Used extension on this project --- .vscode/extensions.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1600801f..07132bed 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,7 @@ { - "recommendations": ["humao.rest-client", "mikestead.dotenv"] -} + "recommendations": [ + "humao.rest-client", + "mikestead.dotenv", + "mkhl.direnv" + ] +} \ No newline at end of file From df78e61cc1e7bca4ddcb4ba751e5aef6d8379414 Mon Sep 17 00:00:00 2001 From: ff6347 Date: Thu, 20 Apr 2023 14:37:25 +0200 Subject: [PATCH 10/10] test(schema): Make number of trees dynamic --- __tests__/schema.test.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/__tests__/schema.test.ts b/__tests__/schema.test.ts index 91463d48..932f7d71 100644 --- a/__tests__/schema.test.ts +++ b/__tests__/schema.test.ts @@ -46,6 +46,7 @@ describe("misc test testing the schema function of the database", () => { }); test("a user should be able to remove its account and his associated data", async () => { + const numberOfTrees = 10; const email = "user@email.com"; await deleteSupabaseUser(email); // clean up before running const { data, error } = await supabaseAnonClient.auth.signUp({ @@ -57,9 +58,9 @@ describe("misc test testing the schema function of the database", () => { const { data: trees, error: treesError } = await supabaseAnonClient .from("trees") .select("*") - .limit(10); + .limit(numberOfTrees); expect(treesError).toBeNull(); - expect(trees).toHaveLength(10); + expect(trees).toHaveLength(numberOfTrees); const { data: adoptedTrees, error: adoptedTreesError } = await supabaseServiceRoleClient @@ -73,7 +74,7 @@ describe("misc test testing the schema function of the database", () => { ) .select("*"); expect(adoptedTreesError).toBeNull(); - expect(adoptedTrees).toHaveLength(10); + expect(adoptedTrees).toHaveLength(numberOfTrees); const { data: userTrees, error: userTreesError } = await supabaseServiceRoleClient .from("trees_watered") @@ -89,7 +90,7 @@ describe("misc test testing the schema function of the database", () => { ) .select("*"); expect(userTreesError).toBeNull(); - expect(userTrees).toHaveLength(10); + expect(userTrees).toHaveLength(numberOfTrees); // since wa can not pass the token to our supabase client, we need to use fetch directly const response = await fetch(`${SUPABASE_URL}/rest/v1/rpc/remove_account`, { @@ -122,6 +123,7 @@ describe("misc test testing the schema function of the database", () => { test("if a user changes his username all the usernames on the trees_watered table should change too", async () => { const email = "foo@bar.com"; + const numberOfTrees = 10; await deleteSupabaseUser(email); await truncateTreesWaterd(); const { data, error } = await supabaseAnonClient.auth.signUp({ @@ -133,9 +135,9 @@ describe("misc test testing the schema function of the database", () => { const { data: trees, error: treesError } = await supabaseAnonClient .from("trees") .select("*") - .limit(10); + .limit(numberOfTrees); expect(treesError).toBeNull(); - expect(trees).toHaveLength(10); + expect(trees).toHaveLength(numberOfTrees); const { data: adoptedTrees, error: adoptedTreesError } = await supabaseServiceRoleClient @@ -152,7 +154,7 @@ describe("misc test testing the schema function of the database", () => { ) .select("*"); expect(adoptedTreesError).toBeNull(); - expect(adoptedTrees).toHaveLength(10); + expect(adoptedTrees).toHaveLength(numberOfTrees); // since we cant pass our access token to change our username to our anon client we use fetch directly const changeResponse = await fetch( @@ -180,7 +182,7 @@ describe("misc test testing the schema function of the database", () => { .eq("username", "bar"); expect(treesAfterError).toBeNull(); - expect(treesAfter).toHaveLength(10); + expect(treesAfter).toHaveLength(numberOfTrees); await deleteSupabaseUser(email); await truncateTreesWaterd(); });