feat: activate public brain subscription (#1241)

* feat: add public brain details modal

* feat(brain): add subscription route

* feat: activate subscription button

* feat: add last_update column to brain table

* feat: display last update on public brain details page

* feat: change RBAC rule for public brains

* feat: maintain brain last_update time
This commit is contained in:
Mamadou DICKO 2023-09-22 11:44:09 +02:00 committed by GitHub
parent 6166b17123
commit 2c9a0c1ed2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 329 additions and 68 deletions

View File

@ -10,6 +10,9 @@ from models.files import File
from models.notifications import NotificationsStatusEnum
from models.settings import get_supabase_client
from parsers.github import process_github
from repository.brain.update_brain_last_update_time import (
update_brain_last_update_time,
)
from repository.notification.update_notification import update_notification_by_id
from utils.processors import filter_file
@ -98,6 +101,8 @@ def process_file_and_notify(
message=str(notification_message),
),
)
update_brain_last_update_time(brain_id)
return True
@ -158,4 +163,5 @@ def process_crawl_and_notify(
message=str(notification_message),
),
)
update_brain_last_update_time(brain_id)
return True

View File

@ -15,6 +15,7 @@ class BrainEntity(BaseModel):
openai_api_key: Optional[str]
status: Optional[str]
prompt_id: Optional[UUID]
last_update: str
@property
def id(self) -> UUID:
@ -40,3 +41,4 @@ class PublicBrain(BaseModel):
name: str
description: Optional[str]
number_of_subscribers: int = 0
last_update: str

View File

@ -78,7 +78,7 @@ class Brain(Repository):
def get_public_brains(self) -> list[PublicBrain]:
response = (
self.db.from_("brains")
.select("id:brain_id, name, description")
.select("id:brain_id, name, description, last_update")
.filter("status", "eq", "public")
.execute()
)
@ -88,11 +88,17 @@ class Brain(Repository):
id=item["id"],
name=item["name"],
description=item["description"],
last_update=item["last_update"],
)
brain.number_of_subscribers = self.get_brain_subscribers_count(brain.id)
public_brains.append(brain)
return public_brains
def update_brain_last_update_time(self, brain_id: UUID) -> None:
self.db.table("brains").update({"last_update": "now()"}).match(
{"brain_id": brain_id}
).execute()
def get_brain_for_user(self, user_id, brain_id) -> MinimalBrainEntity | None:
response = (
self.db.from_("brains_users")

View File

@ -3,9 +3,16 @@ from uuid import UUID
from models import BrainEntity, get_supabase_db
from models.databases.supabase.brains import BrainUpdatableProperties
from repository.brain.update_brain_last_update_time import update_brain_last_update_time
def update_brain_by_id(brain_id: UUID, brain: BrainUpdatableProperties) -> BrainEntity:
"""Update a prompt by id"""
supabase_db = get_supabase_db()
return supabase_db.update_brain_by_id(brain_id, brain) # type: ignore
brain_update_answer = supabase_db.update_brain_by_id(brain_id, brain)
if brain_update_answer is None:
raise Exception("Brain not found")
update_brain_last_update_time(brain_id)
return brain_update_answer

View File

@ -0,0 +1,8 @@
from uuid import UUID
from models.settings import get_supabase_db
def update_brain_last_update_time(brain_id: UUID):
supabase_db = get_supabase_db()
supabase_db.update_brain_last_update_time(brain_id)

View File

@ -5,6 +5,8 @@ from auth.auth_bearer import get_current_user
from fastapi import Depends, HTTPException, status
from models import UserIdentity
from repository.brain import get_brain_for_user
from repository.brain.get_brain_details import get_brain_details
from routes.authorizations.types import RoleEnum
@ -43,6 +45,11 @@ def validate_brain_authorization(
return: None
"""
brain = get_brain_details(brain_id)
if brain and brain.status == "public":
return
if required_roles is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,

View File

@ -361,3 +361,45 @@ def update_brain_subscription(
update_brain_user_rights(brain_id, user_id, subscription.rights)
return {"message": "Brain subscription updated successfully"}
@subscription_router.post(
"/brains/{brain_id}/subscribe",
tags=["Subscription"],
)
async def subscribe_to_brain_handler(
brain_id: UUID, current_user: UserIdentity = Depends(get_current_user)
):
"""
Subscribe to a public brain
"""
if not current_user.email:
raise HTTPException(status_code=400, detail="UserIdentity email is not defined")
brain = get_brain_by_id(brain_id)
if brain is None:
raise HTTPException(status_code=404, detail="Brain not found")
if brain.status != "public":
raise HTTPException(
status_code=403,
detail="You cannot subscribe to this brain without invitation",
)
# check if user is already subscribed to brain
user_brain = get_brain_for_user(current_user.id, brain_id)
if user_brain is not None:
raise HTTPException(
status_code=403,
detail="You are already subscribed to this brain",
)
try:
create_brain_user(
user_id=current_user.id,
brain_id=brain_id,
rights=RoleEnum.Viewer,
is_default_brain=False,
)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error adding user to brain: {e}")
return {"message": "You have successfully subscribed to the brain"}

View File

@ -1,12 +1,15 @@
/* eslint-disable max-lines */
import { Content, List, Root } from "@radix-ui/react-tabs";
import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { BrainTabTrigger, KnowledgeTab, PeopleTab } from "./components";
import ConfirmationDeleteModal from "./components/Modals/ConfirmationDeleteModal";
import { SettingsTab } from "./components/SettingsTab/SettingsTab";
import { useBrainManagementTabs } from "./hooks/useBrainManagementTabs";
import { isUserBrainOwner } from "./utils/isUserBrainOwner";
export const BrainManagementTabs = (): JSX.Element => {
const { t } = useTranslation(["translation", "config", "delete_brain"]);
@ -19,13 +22,21 @@ export const BrainManagementTabs = (): JSX.Element => {
setIsDeleteModalOpen,
brain,
} = useBrainManagementTabs();
const isPubliclyAccessible = brain?.status === "public";
const { allBrains } = useBrainContext();
if (brainId === undefined) {
return <div />;
}
const isCurrentUserBrainOwner = isUserBrainOwner({
brainId,
userAccessibleBrains: allBrains,
});
const isPublicBrain = brain?.status === "public";
const hasEditRights = !isPublicBrain || isCurrentUserBrainOwner;
return (
<Root
className="flex flex-col w-full h-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl overflow-hidden bg-white dark:bg-black border border-black/10 dark:border-white/25 p-4 md:p-10"
@ -41,7 +52,7 @@ export const BrainManagementTabs = (): JSX.Element => {
value="settings"
onChange={setSelectedTab}
/>
{!isPubliclyAccessible && (
{hasEditRights && (
<>
<BrainTabTrigger
selected={selectedTab === "people"}
@ -73,7 +84,7 @@ export const BrainManagementTabs = (): JSX.Element => {
<div className="flex justify-center mt-4">
<Button
disabled={isPubliclyAccessible}
disabled={!isCurrentUserBrainOwner}
className="px-8 md:px-20 py-2 bg-red-500 text-white rounded-md"
onClick={() => setIsDeleteModalOpen(true)}
>
@ -89,5 +100,3 @@ export const BrainManagementTabs = (): JSX.Element => {
</Root>
);
};
export default BrainManagementTabs;

View File

@ -9,16 +9,19 @@ import { Chip } from "@/lib/components/ui/Chip";
import { Divider } from "@/lib/components/ui/Divider";
import Field from "@/lib/components/ui/Field";
import { TextArea } from "@/lib/components/ui/TextArea";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
import { SaveButton } from "@/shared/SaveButton";
import { PublicPrompts } from "./components/PublicPrompts/PublicPrompts";
import { useSettingsTab } from "./hooks/useSettingsTab";
import { isUserBrainOwner } from "../../utils/isUserBrainOwner";
type SettingsTabProps = {
brainId: UUID;
};
// eslint-disable-next-line complexity
export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
const { t } = useTranslation(["translation", "brain", "config"]);
const {
@ -38,8 +41,16 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
accessibleModels,
brain,
} = useSettingsTab({ brainId });
const { allBrains } = useBrainContext();
const isPubliclyAccessible = brain?.status === "public";
const isCurrentUserBrainOwner = isUserBrainOwner({
brainId,
userAccessibleBrains: allBrains,
});
const isPublicBrain = brain?.status === "public";
const hasEditRights = !isPublicBrain || isCurrentUserBrainOwner;
return (
<form
@ -58,14 +69,14 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
autoComplete="off"
className="flex-1"
required
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("name")}
/>
</div>
<div className="mt-4">
<div className="flex flex-1 items-center flex-col">
{isPubliclyAccessible && (
{isPublicBrain && (
<Chip className="mb-3 bg-purple-600 text-white w-full">
{t("brain:public_brain_label")}
</Chip>
@ -80,7 +91,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
isLoading={isSettingAsDefault}
onClick={() => void setAsDefaultBrainHandler()}
type="button"
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
>
{t("setDefaultBrain", { ns: "brain" })}
</Button>
@ -93,7 +104,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
placeholder={t("brainDescriptionPlaceholder", { ns: "brain" })}
autoComplete="off"
className="flex-1 m-3"
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("description")}
/>
<Divider text={t("modelSection", { ns: "config" })} />
@ -102,7 +113,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
placeholder={t("openAiKeyPlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("openAiKey")}
/>
<fieldset className="w-full flex flex-col mt-2">
@ -111,7 +122,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
</label>
<select
id="model"
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("model")}
className="px-5 py-2 dark:bg-gray-700 bg-gray-200 rounded-md"
onChange={() => {
@ -136,7 +147,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
max="1"
step="0.01"
value={temperature}
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("temperature")}
/>
</fieldset>
@ -149,24 +160,21 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
min="10"
max={defineMaxTokens(model)}
value={maxTokens}
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("maxTokens")}
/>
</fieldset>
<div className="flex w-full justify-end py-4">
<SaveButton
disabled={isPubliclyAccessible}
handleSubmit={handleSubmit}
/>
<SaveButton disabled={!hasEditRights} handleSubmit={handleSubmit} />
</div>
<Divider text={t("customPromptSection", { ns: "config" })} />
{!isPubliclyAccessible && <PublicPrompts onSelect={pickPublicPrompt} />}
{hasEditRights && <PublicPrompts onSelect={pickPublicPrompt} />}
<Field
label={t("promptName", { ns: "config" })}
placeholder={t("promptNamePlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("prompt.title")}
/>
<TextArea
@ -174,18 +182,15 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
placeholder={t("promptContentPlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("prompt.content")}
/>
<div className="flex w-full justify-end py-4">
<SaveButton
disabled={isPubliclyAccessible}
handleSubmit={handleSubmit}
/>
<SaveButton disabled={!hasEditRights} handleSubmit={handleSubmit} />
</div>
{promptId !== "" && (
<Button
disabled={isUpdating || isPubliclyAccessible}
disabled={isUpdating || !hasEditRights}
onClick={() => void removeBrainPrompt()}
>
{t("removePrompt", { ns: "config" })}

View File

@ -0,0 +1,19 @@
import { UUID } from "crypto";
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
type IsUserBrainOwnerProps = {
userAccessibleBrains: MinimalBrainForUser[];
brainId?: UUID;
};
export const isUserBrainOwner = ({
brainId,
userAccessibleBrains,
}: IsUserBrainOwnerProps): boolean => {
const brain = userAccessibleBrains.find(({ id }) => id === brainId);
if (brain === undefined) {
return false;
}
return brain.role === "Owner";
};

View File

@ -1,26 +0,0 @@
import { useTranslation } from "react-i18next";
import { MdAdd } from "react-icons/md";
import Button from "@/lib/components/ui/Button";
import { Brain } from "@/lib/context/BrainProvider/types";
type PublicBrainItemProps = {
brain: Brain;
};
export const PublicBrainItem = ({
brain,
}: PublicBrainItemProps): JSX.Element => {
const { t } = useTranslation("brain");
return (
<div className="flex justify-center items-center flex-col w-full h-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl overflow-hidden bg-white dark:bg-black border border-black/10 dark:border-white/25 md:p-5">
<p className="font-bold mb-5 text-xl">{brain.name}</p>
<p className="line-clamp-2 text-center px-5">{brain.description ?? ""}</p>
<Button className="bg-purple-600 text-white p-0 px-3 rounded-xl border-0 w-content mt-3">
{t("public_brain_subscribe_button_label")}
<MdAdd className="text-md" />
</Button>
</div>
);
};

View File

@ -0,0 +1,68 @@
import { useTranslation } from "react-i18next";
import { MdAdd } from "react-icons/md";
import Button from "@/lib/components/ui/Button";
import { Modal } from "@/lib/components/ui/Modal";
import { PublicBrain } from "@/lib/context/BrainProvider/types";
import { usePublicBrainItem } from "./hooks/usePublicBrainItem";
import { formatDate } from "./utils/formatDate";
type PublicBrainItemProps = {
brain: PublicBrain;
};
export const PublicBrainItem = ({
brain,
}: PublicBrainItemProps): JSX.Element => {
const { handleSubscribeToBrain, subscriptionRequestPending } =
usePublicBrainItem({
brainId: brain.id,
});
const { t } = useTranslation("brain");
const subscribeButton = (
<Button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
void handleSubscribeToBrain();
}}
disabled={subscriptionRequestPending}
isLoading={subscriptionRequestPending}
className="bg-purple-600 text-white p-0 px-3 rounded-xl border-0 w-content mt-3"
>
{t("public_brain_subscribe_button_label")}
<MdAdd className="text-md" />
</Button>
);
return (
<Modal
CloseTrigger={<div />}
Trigger={
<div className="flex justify-center items-center flex-col w-full h-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl overflow-hidden bg-white dark:bg-black border border-black/10 dark:border-white/25 md:p-5 cursor-pointer">
<p className="font-bold mb-5 text-xl">{brain.name}</p>
<p className="line-clamp-2 text-center px-5">
{brain.description ?? ""}
</p>
{subscribeButton}
</div>
}
>
<div>
<p className="text-2xl font-bold text-center mb-10">{brain.name}</p>
<p className="mb-10">{brain.description ?? ""}</p>
<p className="font-bold mb-5">
<span>
<span className="mr-2">{t("public_brain_last_update_label")}:</span>
{formatDate(brain.last_update)}
</span>
</p>
<div className="flex flex-1 justify-end">{subscribeButton}</div>
</div>
</Modal>
);
};

View File

@ -0,0 +1,52 @@
import { UUID } from "crypto";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useSubscriptionApi } from "@/lib/api/subscription/useSubscriptionApi";
import { getAxiosErrorParams } from "@/lib/helpers/getAxiosErrorParams";
import { useToast } from "@/lib/hooks";
type UseSubscribeToBrainProps = {
brainId: UUID;
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const usePublicBrainItem = ({ brainId }: UseSubscribeToBrainProps) => {
const { subscribeToBrain } = useSubscriptionApi();
const [subscriptionRequestPending, setSubscriptionRequestPending] =
useState(false);
const { publish } = useToast();
const { t } = useTranslation("brain");
const handleSubscribeToBrain = async () => {
try {
setSubscriptionRequestPending(true);
await subscribeToBrain(brainId);
publish({
text: t("public_brain_subscription_success_message"),
variant: "success",
});
} catch (e) {
const error = getAxiosErrorParams(e);
if (error !== undefined) {
publish({
text: error.message,
variant: "danger",
});
return;
}
publish({
text: JSON.stringify(error),
variant: "danger",
});
} finally {
setSubscriptionRequestPending(false);
}
};
return {
handleSubscribeToBrain,
subscriptionRequestPending,
};
};

View File

@ -0,0 +1 @@
export * from "./PublicBrainItem";

View File

@ -0,0 +1,2 @@
export const formatDate = (date: string): string =>
new Date(date).toLocaleDateString();

View File

@ -0,0 +1 @@
export * from "./PublicBrainItem";

View File

@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
import { PUBLIC_BRAINS_KEY } from "@/lib/api/brain/config";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { Brain } from "@/lib/context/BrainProvider/types";
import { PublicBrain } from "@/lib/context/BrainProvider/types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainsLibrary = () => {
@ -14,9 +14,9 @@ export const useBrainsLibrary = () => {
queryFn: getPublicBrains,
});
const [displayingPublicBrains, setDisplayingPublicBrains] = useState<Brain[]>(
[]
);
const [displayingPublicBrains, setDisplayingPublicBrains] = useState<
PublicBrain[]
>([]);
useEffect(() => {
setDisplayingPublicBrains(publicBrains);

View File

@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
import Field from "@/lib/components/ui/Field";
import { PublicBrainItem } from "./components/PublicBrainItem";
import { PublicBrainItem } from "./components/PublicBrainItem/PublicBrainItem";
import { useBrainsLibrary } from "./hooks/useBrainsLibrary";
const BrainsLibrary = (): JSX.Element => {

View File

@ -54,3 +54,16 @@ export const getInvitation = async (
role: invitation.rights,
};
};
export const subscribeToBrain = async (
brainId: UUID,
axiosInstance: AxiosInstance
): Promise<{ message: string }> => {
const subscribedToBrain = (
await axiosInstance.post<{ message: string }>(
`/brains/${brainId}/subscribe`
)
).data;
return subscribedToBrain;
};

View File

@ -6,6 +6,7 @@ import {
acceptInvitation,
declineInvitation,
getInvitation,
subscribeToBrain,
} from "./subscription";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@ -19,5 +20,7 @@ export const useSubscriptionApi = () => {
declineInvitation(brainId, axiosInstance),
getInvitation: async (brainId: UUID) =>
getInvitation(brainId, axiosInstance),
subscribeToBrain: async (brainId: UUID) =>
subscribeToBrain(brainId, axiosInstance),
};
};

View File

@ -38,6 +38,7 @@ export type PublicBrain = {
name: string;
description?: string;
number_of_subscribers: number;
last_update: string;
};
export type BrainContextType = ReturnType<typeof useBrainProvider>;

View File

@ -36,5 +36,7 @@
"cancel_set_brain_status_to_public": "No, keep it private",
"brain_library_button_label":"Brains library",
"public_brains_search_bar_placeholder":"Search public brains",
"public_brain_subscribe_button_label":"Subscribe"
"public_brain_subscribe_button_label":"Subscribe",
"public_brain_subscription_success_message":"You have successfully subscribed to the brain",
"public_brain_last_update_label":"Last update"
}

View File

@ -36,5 +36,7 @@
"cancel_set_brain_status_to_public": "No, mantenerlo privado",
"brain_library_button_label": "Biblioteca de cerebros",
"public_brains_search_bar_placeholder": "Buscar cerebros públicos",
"public_brain_subscribe_button_label": "Suscribirse"
"public_brain_subscribe_button_label": "Suscribirse",
"public_brain_subscription_success_message": "Te has suscrito con éxito al cerebro",
"public_brain_last_update_label": "Última actualización"
}

View File

@ -36,5 +36,7 @@
"cancel_set_brain_status_to_public": "Non, le garder privé",
"brain_library_button_label": "Bibliothèque des cerveaux",
"public_brains_search_bar_placeholder": "Rechercher des cerveaux publics",
"public_brain_subscribe_button_label": "S'abonner"
"public_brain_subscribe_button_label": "S'abonner",
"public_brain_subscription_success_message": "Vous vous êtes abonné avec succès au cerveau",
"public_brain_last_update_label": "Dernière mise à jour"
}

View File

@ -36,5 +36,7 @@
"cancel_set_brain_status_to_public": "Não, mantê-lo privado",
"brain_library_button_label": "Biblioteca de cérebros",
"public_brains_search_bar_placeholder": "Pesquisar cérebros públicos",
"public_brain_subscribe_button_label": "Inscrever-se"
"public_brain_subscribe_button_label": "Inscrever-se",
"public_brain_subscription_success_message": "Você se inscreveu com sucesso no cérebro",
"public_brain_last_update_label": "Última atualização"
}

View File

@ -36,5 +36,7 @@
"cancel_set_brain_status_to_public": "Нет, оставить приватным",
"brain_library_button_label": "Библиотека мозгов",
"public_brains_search_bar_placeholder": "Поиск общественных мозгов",
"public_brain_subscribe_button_label": "Подписаться"
"public_brain_subscribe_button_label": "Подписаться",
"public_brain_subscription_success_message": "Вы успешно подписались на мозг",
"public_brain_last_update_label": "Последнее обновление"
}

View File

@ -36,5 +36,7 @@
"cancel_set_brain_status_to_public": "不,保持私密",
"brain_library_button_label": "大脑库",
"public_brains_search_bar_placeholder": "搜索公共大脑",
"public_brain_subscribe_button_label": "订阅"
"public_brain_subscribe_button_label": "订阅",
"public_brain_subscription_success_message": "Вы успешно подписались на мозг",
"public_brain_last_update_label": "Последнее обновление"
}

View File

@ -0,0 +1,23 @@
-- Add last_update column to 'brains' table if it doesn't exist
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'brains'
AND column_name = 'last_update'
) THEN
ALTER TABLE brains ADD COLUMN last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
END IF;
END
$$;
-- Insert migration record if it doesn't exist
INSERT INTO migrations (name)
SELECT '20230921160000_add_last_update_field_to_brain'
WHERE NOT EXISTS (
SELECT 1 FROM migrations WHERE name = '20230921160000_add_last_update_field_to_brain'
);
-- Commit the changes
COMMIT;

View File

@ -269,9 +269,9 @@ CREATE POLICY "Access Quivr Storage 1jccrwz_2" ON storage.objects FOR UPDATE TO
CREATE POLICY "Access Quivr Storage 1jccrwz_3" ON storage.objects FOR DELETE TO anon USING (bucket_id = 'quivr');
INSERT INTO migrations (name)
SELECT '202309151054032_add_knowledge_tables'
SELECT '20230921160000_add_last_update_field_to_brain'
WHERE NOT EXISTS (
SELECT 1 FROM migrations WHERE name = '202309151054032_add_knowledge_tables'
SELECT 1 FROM migrations WHERE name = '20230921160000_add_last_update_field_to_brain'
);