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 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 +); diff --git a/__tests__/schema.test.ts b/__tests__/schema.test.ts new file mode 100644 index 00000000..932f7d71 --- /dev/null +++ b/__tests__/schema.test.ts @@ -0,0 +1,189 @@ +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 numberOfTrees = 10; + 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(numberOfTrees); + expect(treesError).toBeNull(); + expect(trees).toHaveLength(numberOfTrees); + + 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(numberOfTrees); + 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(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`, { + 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"; + const numberOfTrees = 10; + 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(numberOfTrees); + expect(treesError).toBeNull(); + expect(trees).toHaveLength(numberOfTrees); + + 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(numberOfTrees); + + // 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(numberOfTrees); + await deleteSupabaseUser(email); + await truncateTreesWaterd(); + }); +}); 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 + } + } } + 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; +} 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"; + 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$; + 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$ +; + + 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;