feat: walking skeleton - study space uploads
This commit is contained in:
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -3,6 +3,7 @@
|
|||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"svelte.svelte-vscode",
|
"svelte.svelte-vscode",
|
||||||
"redhat.vscode-yaml"
|
"redhat.vscode-yaml",
|
||||||
|
"naumovs.color-highlight"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/lib/assets/study_space.png
Normal file
BIN
src/lib/assets/study_space.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
35
src/lib/components/SpaceCard.svelte
Normal file
35
src/lib/components/SpaceCard.svelte
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
alt: string;
|
||||||
|
imgSrc: string;
|
||||||
|
description?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { alt, imgSrc, description }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<img src={imgSrc} {alt} />
|
||||||
|
<div class="description">
|
||||||
|
{@render description?.()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #38353f;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: #edebe9;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 16rem;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
215
src/lib/database.d.ts
vendored
Normal file
215
src/lib/database.d.ts
vendored
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
export type Json =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| { [key: string]: Json | undefined }
|
||||||
|
| Json[]
|
||||||
|
|
||||||
|
export type 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
|
||||||
|
}
|
||||||
|
CompositeTypes: {
|
||||||
|
[_ in never]: never
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public: {
|
||||||
|
Tables: {
|
||||||
|
study_space_images: {
|
||||||
|
Row: {
|
||||||
|
created_at: string | null
|
||||||
|
image_path: string
|
||||||
|
study_space_id: string
|
||||||
|
updated_at: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
created_at?: string | null
|
||||||
|
image_path: string
|
||||||
|
study_space_id: string
|
||||||
|
updated_at?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
created_at?: string | null
|
||||||
|
image_path?: string
|
||||||
|
study_space_id?: string
|
||||||
|
updated_at?: string | null
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "study_space_images_study_space_id_fkey"
|
||||||
|
columns: ["study_space_id"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "study_spaces"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
study_spaces: {
|
||||||
|
Row: {
|
||||||
|
created_at: string | null
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
updated_at: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
created_at?: string | null
|
||||||
|
id?: string
|
||||||
|
title: string
|
||||||
|
updated_at?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
created_at?: string | null
|
||||||
|
id?: string
|
||||||
|
title?: string
|
||||||
|
updated_at?: string | null
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Views: {
|
||||||
|
[_ in never]: never
|
||||||
|
}
|
||||||
|
Functions: {
|
||||||
|
[_ in never]: never
|
||||||
|
}
|
||||||
|
Enums: {
|
||||||
|
[_ in never]: never
|
||||||
|
}
|
||||||
|
CompositeTypes: {
|
||||||
|
[_ in never]: never
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultSchema = Database[Extract<keyof Database, "public">]
|
||||||
|
|
||||||
|
export type Tables<
|
||||||
|
DefaultSchemaTableNameOrOptions extends
|
||||||
|
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
||||||
|
| { schema: keyof Database },
|
||||||
|
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||||
|
schema: keyof Database
|
||||||
|
}
|
||||||
|
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||||
|
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||||
|
: never = never,
|
||||||
|
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||||
|
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||||
|
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||||
|
Row: infer R
|
||||||
|
}
|
||||||
|
? R
|
||||||
|
: never
|
||||||
|
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
|
||||||
|
DefaultSchema["Views"])
|
||||||
|
? (DefaultSchema["Tables"] &
|
||||||
|
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
||||||
|
Row: infer R
|
||||||
|
}
|
||||||
|
? R
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type TablesInsert<
|
||||||
|
DefaultSchemaTableNameOrOptions extends
|
||||||
|
| keyof DefaultSchema["Tables"]
|
||||||
|
| { schema: keyof Database },
|
||||||
|
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||||
|
schema: keyof Database
|
||||||
|
}
|
||||||
|
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||||
|
: never = never,
|
||||||
|
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||||
|
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||||
|
Insert: infer I
|
||||||
|
}
|
||||||
|
? I
|
||||||
|
: never
|
||||||
|
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||||
|
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||||
|
Insert: infer I
|
||||||
|
}
|
||||||
|
? I
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type TablesUpdate<
|
||||||
|
DefaultSchemaTableNameOrOptions extends
|
||||||
|
| keyof DefaultSchema["Tables"]
|
||||||
|
| { schema: keyof Database },
|
||||||
|
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||||
|
schema: keyof Database
|
||||||
|
}
|
||||||
|
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||||
|
: never = never,
|
||||||
|
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||||
|
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||||
|
Update: infer U
|
||||||
|
}
|
||||||
|
? U
|
||||||
|
: never
|
||||||
|
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||||
|
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||||
|
Update: infer U
|
||||||
|
}
|
||||||
|
? U
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type Enums<
|
||||||
|
DefaultSchemaEnumNameOrOptions extends
|
||||||
|
| keyof DefaultSchema["Enums"]
|
||||||
|
| { schema: keyof Database },
|
||||||
|
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||||
|
schema: keyof Database
|
||||||
|
}
|
||||||
|
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||||
|
: never = never,
|
||||||
|
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
||||||
|
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||||
|
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
||||||
|
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type CompositeTypes<
|
||||||
|
PublicCompositeTypeNameOrOptions extends
|
||||||
|
| keyof DefaultSchema["CompositeTypes"]
|
||||||
|
| { schema: keyof Database },
|
||||||
|
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||||
|
schema: keyof Database
|
||||||
|
}
|
||||||
|
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||||
|
: never = never,
|
||||||
|
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||||
|
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||||
|
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
||||||
|
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||||
|
: never
|
||||||
|
|
||||||
|
export const Constants = {
|
||||||
|
graphql_public: {
|
||||||
|
Enums: {},
|
||||||
|
},
|
||||||
|
public: {
|
||||||
|
Enums: {},
|
||||||
|
},
|
||||||
|
} as const
|
||||||
12
src/routes/+layout.svelte
Normal file
12
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
const { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render children?.()}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
18
src/routes/+page.server.ts
Normal file
18
src/routes/+page.server.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from "$env/static/public";
|
||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
import type { Database } from "$lib/database";
|
||||||
|
import { error } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ depends }) => {
|
||||||
|
depends("db:study_spaces");
|
||||||
|
const supabase = createClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
|
||||||
|
const { data: studySpaces, error: err } = await supabase
|
||||||
|
.from("study_spaces")
|
||||||
|
.select("*, study_space_images(*)");
|
||||||
|
if (err) error(500, "Failed to load study spaces");
|
||||||
|
|
||||||
|
return {
|
||||||
|
studySpaces
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,2 +1,87 @@
|
|||||||
<h1>Welcome to SvelteKit</h1>
|
<script lang="ts">
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
import SpaceCard from "$lib/components/SpaceCard.svelte";
|
||||||
|
import defaultImg from "$lib/assets/study_space.png";
|
||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
import type { Database } from "$lib/database";
|
||||||
|
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from "$env/static/public";
|
||||||
|
import { invalidate } from "$app/navigation";
|
||||||
|
|
||||||
|
const supabase = createClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
|
||||||
|
const { data } = $props();
|
||||||
|
const { studySpaces } = $derived(data);
|
||||||
|
|
||||||
|
let title = $state("");
|
||||||
|
let fileInput = $state<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
async function uploadStudySpace() {
|
||||||
|
const imageFile = fileInput?.files?.[0];
|
||||||
|
const imageB64 = imageFile
|
||||||
|
? await new Promise<string>((resolve) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(reader.result as string);
|
||||||
|
reader.readAsDataURL(imageFile);
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
if (!imageB64 || !imageFile) {
|
||||||
|
alert("Please select an image file.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await fetch("/api/study_spaces", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
title,
|
||||||
|
img: imageB64,
|
||||||
|
imgTitle: imageFile.name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
alert("Study space uploaded successfully!");
|
||||||
|
await invalidate("db:study_spaces");
|
||||||
|
} else {
|
||||||
|
alert("Failed to upload study space.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{#each studySpaces as studySpace (studySpace.id)}
|
||||||
|
{@const imgUrl =
|
||||||
|
studySpace.study_space_images.length > 0
|
||||||
|
? supabase.storage
|
||||||
|
.from("files_bucket")
|
||||||
|
.getPublicUrl(studySpace.study_space_images[0].image_path).data.publicUrl
|
||||||
|
: defaultImg}
|
||||||
|
<SpaceCard alt="Photo of {studySpace.title}" imgSrc={imgUrl}>
|
||||||
|
{#snippet description()}
|
||||||
|
<p>{studySpace.title}</p>
|
||||||
|
{/snippet}
|
||||||
|
</SpaceCard>
|
||||||
|
{/each}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onsubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadStudySpace();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input type="text" name="title" bind:value={title} placeholder="Study Space Title" required />
|
||||||
|
<input type="file" name="image" accept=".png, image/png" required bind:this={fileInput} />
|
||||||
|
<button type="submit">Upload Study Space</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
width: min(600px, 100vw);
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
36
src/routes/api/study_spaces/+server.ts
Normal file
36
src/routes/api/study_spaces/+server.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from "$env/static/public";
|
||||||
|
import type { Database } from "$lib/database";
|
||||||
|
import { createClient } from "@supabase/supabase-js";
|
||||||
|
import { error, type RequestHandler } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
const supabase = createClient<Database>(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY);
|
||||||
|
const body = await request.json();
|
||||||
|
const title = body.title;
|
||||||
|
const imgB64 = body.img;
|
||||||
|
const imgTitle = body.imgTitle;
|
||||||
|
if (!title || !imgB64 || !imgTitle) error(400, "Missing required fields: title, img, imgTitle");
|
||||||
|
|
||||||
|
const img = await fetch(imgB64).then((res) => res.blob());
|
||||||
|
const { data: imageData, error: imageError } = await supabase.storage
|
||||||
|
.from("files_bucket")
|
||||||
|
.upload(`public/${imgTitle}`, img, {
|
||||||
|
contentType: img.type,
|
||||||
|
upsert: false
|
||||||
|
});
|
||||||
|
if (imageError) error(500, `Failed to upload image: ${imageError.message}`);
|
||||||
|
|
||||||
|
const { data: studySpaceData, error: studySpaceError } = await supabase
|
||||||
|
.from("study_spaces")
|
||||||
|
.insert({ title })
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
if (studySpaceError) error(500, "Failed to create study space");
|
||||||
|
|
||||||
|
const { error: imageLinkError } = await supabase
|
||||||
|
.from("study_space_images")
|
||||||
|
.insert({ study_space_id: studySpaceData.id, image_path: imageData.path });
|
||||||
|
|
||||||
|
if (imageLinkError) error(500, "Failed to link image to study space");
|
||||||
|
return new Response(JSON.stringify({ id: studySpaceData.id }), { status: 200 });
|
||||||
|
};
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { describe, test, expect } from "vitest";
|
|
||||||
import "@testing-library/jest-dom/vitest";
|
|
||||||
import { render, screen } from "@testing-library/svelte";
|
|
||||||
import Page from "./+page.svelte";
|
|
||||||
|
|
||||||
describe("/+page.svelte", () => {
|
|
||||||
test("should render h1", () => {
|
|
||||||
render(Page);
|
|
||||||
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
56
supabase/migrations/20250530085401_study_spaces_skeleton.sql
Normal file
56
supabase/migrations/20250530085401_study_spaces_skeleton.sql
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
CREATE FUNCTION handle_updated_at()
|
||||||
|
RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SET search_path = ''
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
INSERT INTO storage.buckets (id, name, public)
|
||||||
|
VALUES ('files_bucket', 'files_bucket', true);
|
||||||
|
|
||||||
|
CREATE POLICY "Whack"
|
||||||
|
ON storage.objects
|
||||||
|
FOR ALL
|
||||||
|
USING (bucket_id = 'files_bucket');
|
||||||
|
|
||||||
|
CREATE TABLE study_spaces (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE study_space_images (
|
||||||
|
study_space_id uuid REFERENCES study_spaces(id) ON DELETE CASCADE,
|
||||||
|
image_path text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now(),
|
||||||
|
PRIMARY KEY (study_space_id, image_path)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Triggers
|
||||||
|
CREATE TRIGGER study_spaces_updated_at
|
||||||
|
AFTER UPDATE ON study_spaces
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION handle_updated_at();
|
||||||
|
|
||||||
|
CREATE TRIGGER study_space_images_updated_at
|
||||||
|
AFTER UPDATE ON study_space_images
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION handle_updated_at();
|
||||||
|
|
||||||
|
-- Security
|
||||||
|
-- ALTER TABLE study_spaces ENABLE ROW LEVEL SECURITY;
|
||||||
|
-- ALTER TABLE study_space_images ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- CREATE POLICY "Allow all users to view study spaces"
|
||||||
|
-- ON study_spaces
|
||||||
|
-- FOR SELECT
|
||||||
|
-- USING (true);
|
||||||
|
|
||||||
|
-- CREATE POLICY "Allow all users to view study space images"
|
||||||
|
-- ON study_space_images
|
||||||
|
-- FOR SELECT
|
||||||
|
-- USING (true);
|
||||||
10
supabase/schemas/0000_common.sql
Normal file
10
supabase/schemas/0000_common.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE FUNCTION handle_updated_at()
|
||||||
|
RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SET search_path = ''
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
45
supabase/schemas/0001_study_spaces.sql
Normal file
45
supabase/schemas/0001_study_spaces.sql
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
INSERT INTO storage.buckets (id, name, public)
|
||||||
|
VALUES ('files_bucket', 'files_bucket', true);
|
||||||
|
|
||||||
|
CREATE POLICY "Whack"
|
||||||
|
ON storage.objects
|
||||||
|
FOR ALL
|
||||||
|
USING (bucket_id = 'files_bucket');
|
||||||
|
|
||||||
|
CREATE TABLE study_spaces (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE study_space_images (
|
||||||
|
study_space_id uuid REFERENCES study_spaces(id) ON DELETE CASCADE,
|
||||||
|
image_path text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now(),
|
||||||
|
updated_at timestamp with time zone DEFAULT now(),
|
||||||
|
PRIMARY KEY (study_space_id, image_path)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Triggers
|
||||||
|
CREATE TRIGGER study_spaces_updated_at
|
||||||
|
AFTER UPDATE ON study_spaces
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION handle_updated_at();
|
||||||
|
|
||||||
|
CREATE TRIGGER study_space_images_updated_at
|
||||||
|
AFTER UPDATE ON study_space_images
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION handle_updated_at();
|
||||||
|
|
||||||
|
-- Security
|
||||||
|
-- ALTER TABLE study_spaces ENABLE ROW LEVEL SECURITY;
|
||||||
|
-- ALTER TABLE study_space_images ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- CREATE POLICY "Allow all users to view study spaces"
|
||||||
|
-- ON study_spaces
|
||||||
|
-- FOR SELECT
|
||||||
|
-- USING (true);
|
||||||
|
|
||||||
|
-- CREATE POLICY "Allow all users to view study space images"
|
||||||
|
-- ON study_space_images
|
||||||
|
-- FOR SELECT
|
||||||
|
-- USING (true);
|
||||||
Reference in New Issue
Block a user