From 906d788b43ba876b2aad696da605ccbf01845d1c Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Thu, 7 Nov 2024 15:53:14 +0800 Subject: [PATCH 01/86] feat: show device code on cli login (#30330) * feat: show device code on cli login * chore: use input otp to show verification code * chore: update header formatting and copy consistency * chore: destructure nonce in case api lies about types * chore: note about minimum cli version --- apps/studio/data/cli/login.ts | 2 +- apps/studio/pages/cli/login.tsx | 46 ++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/apps/studio/data/cli/login.ts b/apps/studio/data/cli/login.ts index 704e940a654..37d6741f1e2 100644 --- a/apps/studio/data/cli/login.ts +++ b/apps/studio/data/cli/login.ts @@ -4,7 +4,7 @@ export async function createCliLoginSession( sessionId: string, publicKey: string, tokenName?: string -) { +): Promise<{ nonce: string }> { if (!sessionId) { throw new Error('sessionId is required') } diff --git a/apps/studio/pages/cli/login.tsx b/apps/studio/pages/cli/login.tsx index 7c3ed52dd68..a41d77b70bd 100644 --- a/apps/studio/pages/cli/login.tsx +++ b/apps/studio/pages/cli/login.tsx @@ -1,27 +1,24 @@ import { useIsLoggedIn, useParams } from 'common' import APIAuthorizationLayout from 'components/layouts/APIAuthorizationLayout' +import CopyButton from 'components/ui/CopyButton' import { Loading } from 'components/ui/Loading' import { createCliLoginSession } from 'data/cli/login' import { withAuth } from 'hooks/misc/withAuth' import Link from 'next/link' import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import { toast } from 'sonner' import { NextPageWithLayout } from 'types' +import { InputOTP, InputOTPGroup, InputOTPSlot } from 'ui' +import { Admonition } from 'ui-patterns' const CliLoginPage: NextPageWithLayout = () => { const router = useRouter() - const { session_id, public_key, token_name, success } = useParams() - const [isSuccessfulLogin, setSuccessfulLogin] = useState(false) + const { session_id, public_key, token_name, device_code } = useParams() const isLoggedIn = useIsLoggedIn() useEffect(() => { - if (!isLoggedIn || !router.isReady) { - return - } - - if (success) { - setSuccessfulLogin(true) + if (!isLoggedIn || !router.isReady || device_code) { return } @@ -32,10 +29,10 @@ const CliLoginPage: NextPageWithLayout = () => { } try { - const session = await createCliLoginSession(session_id, public_key, token_name) + const { nonce } = await createCliLoginSession(session_id, public_key, token_name) - if (session) { - router.push(`/cli/login?success=true`) + if (nonce) { + router.push(`/cli/login?device_code=${nonce.substring(0, 8)}`) } else { router.push(`/404`) } @@ -46,14 +43,28 @@ const CliLoginPage: NextPageWithLayout = () => { } createSession() - }, [isLoggedIn, router, router.isReady, session_id, public_key, token_name, success]) + }, [isLoggedIn, router, router.isReady, session_id, public_key, token_name, device_code]) return (
- {isSuccessfulLogin ? ( + {device_code ? ( <> -

Well done! Now close this window, go back to your terminal and hack away!

+

+ Your Supabase Account is being used to login on Supabase CLI. +

+

Enter this verification code on Supabase CLI to authorize login.

+
+ + + {Array.from({ length: 8 }, (_, i) => ( + + ))} + + + +
+

After authorizing the login attempt, you can close this window.

If you ever want to remove your new token, go to{' '} @@ -61,6 +72,11 @@ const CliLoginPage: NextPageWithLayout = () => { {' '} page.

+ ) : ( From e5c87ea5602d66c9e49f903ec35be6faa68f090b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Thu, 7 Nov 2024 16:28:21 +0800 Subject: [PATCH 02/86] chore: remove kpsVersion property (#30350) KPS has long been removed and the property is never set for projects --- .../interfaces/Auth/Users/AddUserDropdown.tsx | 12 +------ .../interfaces/Auth/Users/UsersV2.tsx | 2 +- .../layouts/ProjectLayout/ConnectingState.tsx | 2 +- apps/studio/lib/pingPostgrest.ts | 35 +++---------------- apps/studio/pages/api/projects/[ref]/index.ts | 1 - apps/studio/types/base.ts | 1 - 6 files changed, 8 insertions(+), 45 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx b/apps/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx index 05e3eedb107..de1a2af369f 100644 --- a/apps/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx +++ b/apps/studio/components/interfaces/Auth/Users/AddUserDropdown.tsx @@ -18,18 +18,8 @@ import { import CreateUserModal from './CreateUserModal' import InviteUserModal from './InviteUserModal' -export type AddUserDropdownProps = { - projectKpsVersion?: string -} - -const AddUserDropdown = ({ projectKpsVersion }: AddUserDropdownProps) => { +const AddUserDropdown = () => { const inviteEnabled = IS_PLATFORM - ? semver.gte( - // @ts-ignore - semver.coerce(projectKpsVersion ?? 'kps-v2.5.4'), - semver.coerce('kps-v2.5.3') - ) - : true const canInviteUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'invite_user') const canCreateUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'create_user') diff --git a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx index 7ecab736fbf..032ab8774b2 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx @@ -363,7 +363,7 @@ export const UsersV2 = () => { > Refresh - +
diff --git a/apps/studio/components/layouts/ProjectLayout/ConnectingState.tsx b/apps/studio/components/layouts/ProjectLayout/ConnectingState.tsx index f1086183e4e..c229b33b787 100644 --- a/apps/studio/components/layouts/ProjectLayout/ConnectingState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ConnectingState.tsx @@ -31,7 +31,7 @@ const ConnectingState = ({ project }: ConnectingStateProps) => { }, [project]) const testProjectConnection = async () => { - const result = await pingPostgrest(project.ref, { kpsVersion: project.kpsVersion }) + const result = await pingPostgrest(project.ref) if (result) { clearInterval(checkProjectConnectionIntervalRef.current) setProjectPostgrestStatus(queryClient, project.ref, 'ONLINE') diff --git a/apps/studio/lib/pingPostgrest.ts b/apps/studio/lib/pingPostgrest.ts index 42919651b3f..2dbad44f109 100644 --- a/apps/studio/lib/pingPostgrest.ts +++ b/apps/studio/lib/pingPostgrest.ts @@ -1,5 +1,4 @@ -import semver from 'semver' -import { headWithTimeout, getWithTimeout } from './common/fetch' +import { headWithTimeout } from './common/fetch' import { API_URL } from './constants' const DEFAULT_TIMEOUT_MILLISECONDS = 2000 @@ -7,35 +6,23 @@ const DEFAULT_TIMEOUT_MILLISECONDS = 2000 /** * Ping Postgrest for health check. Default timeout in 2s. * - * Project with version gte 'kps-v3.8.6', we can ping the health-check api - * else ping the OpenApi url - * * @param restUrl project rest url * @param apikey project internal api key - * @param options optional, include project kpsVersion or custom timeout in milliseconds + * @param options optional, include custom timeout in milliseconds * * @return true if ping is successful else false */ async function pingPostgrest( projectRef: string, options?: { - kpsVersion?: string timeout?: number } ) { if (projectRef === undefined) return false - const { kpsVersion, timeout } = options ?? {} - const healthCheckApiEnable = semver.gte( - // @ts-ignore - semver.coerce(kpsVersion ?? 'kps-v0.0.1'), - semver.coerce('kps-v3.8.6') - ) - if (healthCheckApiEnable) { - return pingHealthCheckApi(projectRef, timeout) - } else { - return pingOpenApi(projectRef, timeout) - } + const { timeout } = options ?? {} + + return pingOpenApi(projectRef, timeout) } export default pingPostgrest @@ -51,15 +38,3 @@ async function pingOpenApi(ref: string, timeout?: number) { }) return error === undefined } - -/** - * Send a GET request to postgrest health check api. - * - * @return true if there's no error else false - */ -async function pingHealthCheckApi(ref: string, timeout?: number) { - const { error } = await getWithTimeout(`${API_URL}/projects/${ref}/live`, { - timeout: timeout ?? DEFAULT_TIMEOUT_MILLISECONDS, - }) - return error === undefined -} diff --git a/apps/studio/pages/api/projects/[ref]/index.ts b/apps/studio/pages/api/projects/[ref]/index.ts index 5d8423116b1..72412cd4c71 100644 --- a/apps/studio/pages/api/projects/[ref]/index.ts +++ b/apps/studio/pages/api/projects/[ref]/index.ts @@ -22,7 +22,6 @@ const handleGet = async (req: NextApiRequest, res: NextApiResponse) => { const response = { ...DEFAULT_PROJECT, connectionString: '', - kpsVersion: 'kps-v1.0.0', restUrl: PROJECT_REST_URL, } diff --git a/apps/studio/types/base.ts b/apps/studio/types/base.ts index e593dcf2d2c..0f3742062e7 100644 --- a/apps/studio/types/base.ts +++ b/apps/studio/types/base.ts @@ -38,7 +38,6 @@ export interface Project extends ProjectBase { // available after projects.fetchDetail connectionString?: string dbVersion?: string - kpsVersion?: string restUrl?: string lastDatabaseResizeAt?: string | null maxDatabasePreprovisionGb?: string | null From c59be649a3778b647fb1624a40439881fc374895 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Thu, 7 Nov 2024 16:29:07 +0800 Subject: [PATCH 03/86] chore: prefetch project homepage (#30317) --- .../Home/ProjectList/ProjectCard.tsx | 2 + .../NavigationBar/NavigationBar.tsx | 2 + .../NavigationBar/NavigationBar.utils.tsx | 2 +- .../NavigationBar/NavigationIconLink.tsx | 4 +- apps/studio/components/ui/CardButton.tsx | 20 ++++-- apps/studio/components/ui/ui.types.ts | 2 +- .../project-log-requests-count-query.ts | 11 ++- .../data/analytics/project-log-stats-query.ts | 11 ++- .../prefetchers/project.$ref.editor.$id.tsx | 50 +++++++------- .../data/prefetchers/project.$ref.editor.tsx | 6 +- apps/studio/data/prefetchers/project.$ref.tsx | 67 +++++++++++++++++++ .../data/projects/project-detail-query.ts | 15 ++--- 12 files changed, 145 insertions(+), 47 deletions(-) create mode 100644 apps/studio/data/prefetchers/project.$ref.tsx diff --git a/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx b/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx index 1c6cbd01585..bab4d0c394f 100644 --- a/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx +++ b/apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx @@ -3,6 +3,7 @@ import { GitBranch, Github } from 'lucide-react' import CardButton from 'components/ui/CardButton' import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper' import type { IntegrationProjectConnection } from 'data/integrations/integrations.types' +import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref' import type { ProjectInfo } from 'data/projects/projects-query' import type { ResourceWarning } from 'data/usage/resource-warnings-query' import { BASE_PATH } from 'lib/constants' @@ -73,6 +74,7 @@ const ProjectCard = ({ footer={ } + containerElement={} /> ) diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx index 8fd77400bda..0032c511051 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.tsx @@ -12,6 +12,7 @@ import { useIsDatabaseFunctionsAssistantEnabled, } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { useProjectLintsQuery } from 'data/lint/lint-query' +import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useFlag } from 'hooks/ui/useFlag' @@ -263,6 +264,7 @@ const NavigationBar = () => { label: 'Home', icon: , link: `/project/${projectRef}`, + linkElement: , }} onClick={onCloseNavigationIconLink} /> diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx index 3b92106f0d6..6d695d60116 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils.tsx @@ -26,7 +26,7 @@ export const generateToolRoutes = (ref?: string, project?: Project): Route[] => label: 'Table Editor', icon: , link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/editor`), - linkComponent: , + linkElement: , }, { key: 'sql', diff --git a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationIconLink.tsx b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationIconLink.tsx index 758deedc315..d8ff0949a26 100644 --- a/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationIconLink.tsx +++ b/apps/studio/components/layouts/ProjectLayout/NavigationBar/NavigationIconLink.tsx @@ -48,8 +48,8 @@ const NavigationIconLink = forwardRef>( function LinkComponent(props, ref) { - if (route.linkComponent && isValidElement(route.linkComponent)) { - return cloneElement(route.linkComponent, { ...props, ref }) + if (route.linkElement && isValidElement(route.linkElement)) { + return cloneElement(route.linkElement, { ...props, ref }) } return diff --git a/apps/studio/components/ui/CardButton.tsx b/apps/studio/components/ui/CardButton.tsx index ab15ba26513..ee56413b87d 100644 --- a/apps/studio/components/ui/CardButton.tsx +++ b/apps/studio/components/ui/CardButton.tsx @@ -1,6 +1,7 @@ -import { Loader, ChevronRight } from 'lucide-react' +import { ChevronRight, Loader } from 'lucide-react' import Link from 'next/link' -import React, { PropsWithChildren } from 'react' +import React, { cloneElement, PropsWithChildren } from 'react' + import { cn } from 'ui' interface CardButtonProps { @@ -18,6 +19,7 @@ interface CardButtonProps { fixedHeight?: boolean hideChevron?: boolean titleClass?: string + containerElement?: React.ReactNode } // Define separate interfaces for each type of container @@ -56,6 +58,7 @@ const CardButton = ({ fixedHeight = true, hideChevron = false, titleClass = '', + containerElement, ...props }: PropsWithChildren) => { const isLink = url || linkHref || props.onClick @@ -63,23 +66,28 @@ const CardButton = ({ let Container: React.ElementType let containerProps: ContainerProps = {} + const ContainerComponentOverride = + containerElement && React.isValidElement(containerElement) + ? (props: any) => cloneElement(containerElement, { ...props }) + : undefined + if (props.onClick) { - Container = 'button' + Container = ContainerComponentOverride ?? 'button' containerProps = props } else if (linkHref) { - Container = Link + Container = ContainerComponentOverride ?? Link containerProps = { href: linkHref, ...props, } } else if (url) { - Container = 'a' + Container = ContainerComponentOverride ?? 'a' containerProps = { href: url, ...props, } } else { - Container = 'div' + Container = ContainerComponentOverride ?? 'div' containerProps = props } diff --git a/apps/studio/components/ui/ui.types.ts b/apps/studio/components/ui/ui.types.ts index 2aee180817c..c1ef94670f2 100644 --- a/apps/studio/components/ui/ui.types.ts +++ b/apps/studio/components/ui/ui.types.ts @@ -6,5 +6,5 @@ export interface Route { icon: ReactNode link?: string disabled?: boolean - linkComponent?: ReactNode + linkElement?: ReactNode } diff --git a/apps/studio/data/analytics/project-log-requests-count-query.ts b/apps/studio/data/analytics/project-log-requests-count-query.ts index 310ea107b65..0d6e5b5863d 100644 --- a/apps/studio/data/analytics/project-log-requests-count-query.ts +++ b/apps/studio/data/analytics/project-log-requests-count-query.ts @@ -1,4 +1,4 @@ -import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' import { get, handleError } from 'data/fetchers' import type { ResponseError } from 'types' @@ -48,3 +48,12 @@ export const useProjectLogRequestsCountQuery = + getProjectLogRequestsCountStats({ projectRef }, signal) + ) +} diff --git a/apps/studio/data/analytics/project-log-stats-query.ts b/apps/studio/data/analytics/project-log-stats-query.ts index d29c1dce999..1374d3ba67e 100644 --- a/apps/studio/data/analytics/project-log-stats-query.ts +++ b/apps/studio/data/analytics/project-log-stats-query.ts @@ -1,4 +1,4 @@ -import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' import { get, isResponseOk } from 'lib/common/fetch' import { API_URL } from 'lib/constants' import { analyticsKeys } from './keys' @@ -61,3 +61,12 @@ export const useProjectLogStatsQuery = ( ...options, } ) + +export function prefetchProjectLogStats( + client: QueryClient, + { projectRef, interval }: ProjectLogStatsVariables +) { + return client.fetchQuery(analyticsKeys.usageApiCounts(projectRef, interval), ({ signal }) => + getProjectLogStats({ projectRef, interval }, signal) + ) +} diff --git a/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx b/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx index 74e2269059e..054f2c906f1 100644 --- a/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx +++ b/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx @@ -39,30 +39,34 @@ export function usePrefetchEditorTablePage() { projectRef: project.ref, connectionString: project.connectionString, id, - }).then((entity) => { - if (entity) { - const supaTable = parseSupaTable(entity) - - const { sorts: localSorts = [], filters: localFilters = [] } = - loadTableEditorSortsAndFiltersFromLocalStorage( - project.ref, - entity.name, - entity.schema - ) ?? {} - - prefetchTableRows(queryClient, { - queryKey: [supaTable.schema, supaTable.name], - projectRef: project?.ref, - connectionString: project?.connectionString, - table: supaTable, - sorts: sorts ?? formatSortURLParams(supaTable.name, localSorts), - filters: filters ?? formatFilterURLParams(localFilters), - page: 1, - limit: TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE, - impersonatedRole: roleImpersonationState.role, - }) - } }) + .then((entity) => { + if (entity) { + const supaTable = parseSupaTable(entity) + + const { sorts: localSorts = [], filters: localFilters = [] } = + loadTableEditorSortsAndFiltersFromLocalStorage( + project.ref, + entity.name, + entity.schema + ) ?? {} + + prefetchTableRows(queryClient, { + queryKey: [supaTable.schema, supaTable.name], + projectRef: project?.ref, + connectionString: project?.connectionString, + table: supaTable, + sorts: sorts ?? formatSortURLParams(supaTable.name, localSorts), + filters: filters ?? formatFilterURLParams(localFilters), + page: 1, + limit: TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE, + impersonatedRole: roleImpersonationState.role, + }) + } + }) + .catch(() => { + // eat prefetching errors as they are not critical + }) }, [project, queryClient, roleImpersonationState.role, router, tableEditorPrefetchingEnabled] ) diff --git a/apps/studio/data/prefetchers/project.$ref.editor.tsx b/apps/studio/data/prefetchers/project.$ref.editor.tsx index e21d6fed797..e460ca0d063 100644 --- a/apps/studio/data/prefetchers/project.$ref.editor.tsx +++ b/apps/studio/data/prefetchers/project.$ref.editor.tsx @@ -1,5 +1,6 @@ import { useQueryClient } from '@tanstack/react-query' import Link from 'next/link' +import { useRouter } from 'next/router' import { ComponentProps, PropsWithChildren, useCallback } from 'react' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -8,7 +9,6 @@ import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' import { prefetchEntityTypes } from 'data/entity-types/entity-types-infinite-query' import { useLocalStorage } from 'hooks/misc/useLocalStorage' import { useFlag } from 'hooks/ui/useFlag' -import { useRouter } from 'next/router' export function usePrefetchEditorIndexPage() { const router = useRouter() @@ -31,12 +31,16 @@ export function usePrefetchEditorIndexPage() { prefetchSchemas(queryClient, { projectRef: project.ref, connectionString: project.connectionString, + }).catch(() => { + // eat prefetching errors as they are not critical }) prefetchEntityTypes(queryClient, { projectRef: project?.ref, connectionString: project?.connectionString, sort: entityTypesSort, filterTypes: Object.values(ENTITY_TYPE), + }).catch(() => { + // eat prefetching errors as they are not critical }) }, [entityTypesSort, project, queryClient, router]) } diff --git a/apps/studio/data/prefetchers/project.$ref.tsx b/apps/studio/data/prefetchers/project.$ref.tsx new file mode 100644 index 00000000000..9b826cc45fd --- /dev/null +++ b/apps/studio/data/prefetchers/project.$ref.tsx @@ -0,0 +1,67 @@ +import { useQueryClient } from '@tanstack/react-query' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { ComponentProps, PropsWithChildren, useCallback } from 'react' + +import { prefetchProjectLogRequestsCount } from 'data/analytics/project-log-requests-count-query' +import { prefetchProjectLogStats } from 'data/analytics/project-log-stats-query' +import { prefetchProjectDetail } from 'data/projects/project-detail-query' + +export function usePrefetchProjectIndexPage() { + const router = useRouter() + const queryClient = useQueryClient() + + return useCallback( + ({ projectRef }: { projectRef?: string }) => { + // Prefetch code + router.prefetch(`/project/${projectRef}`) + + // Prefetch data + prefetchProjectDetail(queryClient, { + ref: projectRef, + }).catch(() => { + // eat prefetching errors as they are not critical + }) + + prefetchProjectLogRequestsCount(queryClient, { + projectRef, + }).catch(() => { + // eat prefetching errors as they are not critical + }) + + prefetchProjectLogStats(queryClient, { + projectRef, + interval: 'hourly', + }).catch(() => { + // eat prefetching errors as they are not critical + }) + }, + [queryClient, router] + ) +} + +type LinkProps = ComponentProps + +interface ProjectIndexPageLinkProps extends Omit { + projectRef?: string + href?: LinkProps['href'] +} + +export function ProjectIndexPageLink({ + href, + projectRef, + children, + ...props +}: PropsWithChildren) { + const prefetch = usePrefetchProjectIndexPage() + + return ( + prefetch({ projectRef })} + {...props} + > + {children} + + ) +} diff --git a/apps/studio/data/projects/project-detail-query.ts b/apps/studio/data/projects/project-detail-query.ts index 6f791b0c85b..dfee1c1dcfc 100644 --- a/apps/studio/data/projects/project-detail-query.ts +++ b/apps/studio/data/projects/project-detail-query.ts @@ -62,15 +62,8 @@ export function invalidateProjectDetailsQuery(client: QueryClient, ref: string) return client.invalidateQueries(projectKeys.detail(ref)) } -// get the cached value or fallback to fetching it -export async function getCachedProjectDetail( - client: QueryClient, - ref: string | undefined -): Promise { - if (!ref) return undefined - - const cached = client.getQueryData(projectKeys.detail(ref)) - if (cached) return cached - - return await client.fetchQuery(projectKeys.detail(ref)) +export function prefetchProjectDetail(client: QueryClient, { ref }: ProjectDetailVariables) { + return client.fetchQuery(projectKeys.detail(ref), ({ signal }) => + getProjectDetail({ ref }, signal) + ) } From 1f2f431b592e2dce4673f8b792dafea655163af1 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 7 Nov 2024 18:45:18 +0800 Subject: [PATCH 04/86] Remove clsx library from dashboard (#30311) * Remove clsx library from dashboard * PRETTY --- .../grid/components/header/Header.tsx | 3 +- .../Database/Hooks/HTTPRequestFields.tsx | 4 +- .../Migrations/MigrationsEmptyState.tsx | 7 +- .../EdgeFunctionDetails.tsx | 4 +- .../interfaces/GraphQL/GraphiQL.tsx | 25 ++++--- .../BillingBreakdown/BillingBreakdown.tsx | 17 ++--- .../BillingBreakdown/UpcomingInvoice.tsx | 66 ++++++++++--------- .../Subscription/EnterpriseCard.tsx | 8 +-- .../Organization/Usage/TotalUsage.tsx | 23 +++---- .../interfaces/Organization/Usage/Usage.tsx | 5 +- .../Organization/Usage/UsageBarChart.tsx | 7 +- .../Organization/Usage/UsageChartTooltips.tsx | 5 +- .../Usage/UsageSection/AttributeUsage.tsx | 9 ++- .../SpreadSheetImportConfiguration.tsx | 12 ++-- .../SpreadsheetImportPreview.tsx | 5 +- .../ForeignKeysManagement/ForeignKeyRow.tsx | 16 ++--- .../NotificationsPopover.tsx | 35 +++++----- .../SettingsLayout/SettingsMenuItem.tsx | 5 +- .../layouts/StorageLayout/BucketRow.tsx | 3 +- apps/studio/components/ui/SparkBar.tsx | 6 +- apps/studio/components/ui/TwoOptionToggle.tsx | 6 +- apps/studio/package.json | 1 - package-lock.json | 1 - 23 files changed, 131 insertions(+), 142 deletions(-) diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index 292745a27af..9024b480d53 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -1,5 +1,4 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import clsx from 'clsx' import saveAs from 'file-saver' import { ArrowUp, ChevronDown, FileText, Trash } from 'lucide-react' import Papa from 'papaparse' @@ -196,7 +195,7 @@ const DefaultHeader = ({ table, onAddColumn, onAddRow, onImportData }: DefaultHe className="-translate-x-[2px]" /> } - className={clsx(type === 'supabase_function' && 'rounded-r-none px-3')} + className={cn(type === 'supabase_function' && 'rounded-r-none px-3')} onClick={onAddHeader} > Add a new header diff --git a/apps/studio/components/interfaces/Database/Migrations/MigrationsEmptyState.tsx b/apps/studio/components/interfaces/Database/Migrations/MigrationsEmptyState.tsx index 9bb5ee66269..13283812b59 100644 --- a/apps/studio/components/interfaces/Database/Migrations/MigrationsEmptyState.tsx +++ b/apps/studio/components/interfaces/Database/Migrations/MigrationsEmptyState.tsx @@ -1,10 +1,9 @@ -import clsx from 'clsx' +import { Maximize2, Minimize2, Terminal } from 'lucide-react' import { useState } from 'react' -import { Collapsible } from 'ui' import { useParams } from 'common' import CommandRender from 'components/interfaces/Functions/CommandRender' -import { Terminal, Minimize2, Maximize2 } from 'lucide-react' +import { cn, Collapsible } from 'ui' const MigrationsEmptyState = () => { const { ref } = useParams() @@ -62,7 +61,7 @@ const MigrationsEmptyState = () => { + @@ -484,7 +499,7 @@ const JobItem: React.FC<{ job: JobItemProps }> = ({ job }) => { href={job.jobUrl} className=" first-of-type:rounded-t-md last-of-type:rounded-b-md - cursor-pointer md:cursor-default bg-surface-75 border border-muted drop-shadow-sm p-4 px-7 + cursor-pointer md:cursor-default bg-surface-75 border border-muted drop-shadow-sm p-4 flex flex-col md:flex-row md:items-center transition hover:bg-surface-100 @@ -492,8 +507,10 @@ const JobItem: React.FC<{ job: JobItemProps }> = ({ job }) => { " target="_blank" > -

{job.title}

-
+

+ {job.title} +

+
From 1adfb951b1dc0e1e915eb39554de67241bbd3aa6 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 7 Nov 2024 22:51:09 +0200 Subject: [PATCH 11/86] fix row level security docs url in expo tutorial (#30367) fix row level security docs url --- .../guides/getting-started/tutorials/with-expo-react-native.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/getting-started/tutorials/with-expo-react-native.mdx b/apps/docs/content/guides/getting-started/tutorials/with-expo-react-native.mdx index a86e882cd3c..63a270a0ecb 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-expo-react-native.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-expo-react-native.mdx @@ -40,7 +40,7 @@ npx expo install @supabase/supabase-js @react-native-async-storage/async-storage Now let's create a helper file to initialize the Supabase client. We need the API URL and the `anon` key that you copied [earlier](#get-the-api-keys). These variables are safe to expose in your Expo app since Supabase has -[Row Level Security](/docs/guides/auth#row-level-security) enabled on your Database. +[Row Level Security](/docs/guides/database/postgres/row-level-security) enabled on your Database. Date: Fri, 8 Nov 2024 11:17:16 +0800 Subject: [PATCH 12/86] chore: use service versions endpoint (#30352) * chore: use service versions endpoint * update types * fix types * fallback postgres version for unknown images --- .../Infrastructure/InfrastructureInfo.tsx | 113 ++++--- .../analytics/project-daily-stats-query.ts | 9 +- apps/studio/data/projects/keys.ts | 2 + .../data/projects/project-service-versions.ts | 42 +++ .../pages/api/projects/[ref]/settings.ts | 3 +- apps/studio/types/base.ts | 1 - packages/api-types/types/api.d.ts | 308 ++++++++++++++++-- 7 files changed, 401 insertions(+), 77 deletions(-) create mode 100644 apps/studio/data/projects/project-service-versions.ts diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx index 8453e3b577d..efaa3666ad5 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureInfo.tsx @@ -10,6 +10,7 @@ import { import AlertError from 'components/ui/AlertError' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useProjectUpgradeEligibilityQuery } from 'data/config/project-upgrade-eligibility-query' +import { useProjectServiceVersionsQuery } from 'data/projects/project-service-versions' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { @@ -41,6 +42,15 @@ const InfrastructureInfo = () => { } = useProjectUpgradeEligibilityQuery({ projectRef: ref, }) + + const { + data: serviceVersions, + error: serviceVersionsError, + isLoading: isLoadingServiceVersions, + isError: isErrorServiceVersions, + isSuccess: isSuccessServiceVersions, + } = useProjectServiceVersionsQuery({ projectRef: ref }) + const { data: databases } = useReadReplicasQuery({ projectRef: ref }) const { current_app_version, current_app_version_release_channel, latest_app_version } = data || {} @@ -91,53 +101,66 @@ const InfrastructureInfo = () => { )} {isSuccessUpgradeEligibility && ( <> - {authEnabled && ( - } + {isErrorServiceVersions && ( + )} - - - - - {isOnNonGenerallyAvailableReleaseChannel} - - - - This project uses a {isOnNonGenerallyAvailableReleaseChannel} database - version release - - - ), - isOnLatestVersion && ( - - - - Latest - - - - Project is on the latest version of Postgres that Supabase supports - - - ), - ]} - /> + {isSuccessServiceVersions && ( + <> + {authEnabled && ( + + )} + + + + + {isOnNonGenerallyAvailableReleaseChannel} + + + + This project uses a {isOnNonGenerallyAvailableReleaseChannel}{' '} + database version release + + + ), + isOnLatestVersion && ( + + + + Latest + + + + Project is on the latest version of Postgres that Supabase + supports + + + ), + ]} + /> + + )} + {data?.eligible && !hasReadReplicas && } {data.eligible && hasReadReplicas && ( diff --git a/apps/studio/data/analytics/project-daily-stats-query.ts b/apps/studio/data/analytics/project-daily-stats-query.ts index 3cceceb8741..4b97f5a1914 100644 --- a/apps/studio/data/analytics/project-daily-stats-query.ts +++ b/apps/studio/data/analytics/project-daily-stats-query.ts @@ -1,18 +1,13 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import dayjs from 'dayjs' +import { operations } from 'api-types' import { get, handleError } from 'data/fetchers' import type { AnalyticsData } from './constants' import { analyticsKeys } from './keys' export type ProjectDailyStatsAttribute = - | 'max_cpu_usage' - | 'avg_cpu_usage' - | 'disk_io_budget' - | 'ram_usage' - | 'disk_io_consumption' - | 'swap_usage' - + operations['DailyStatsController_getDailyStats']['parameters']['query']['attribute'] export type ProjectDailyStatsInterval = '1m' | '5m' | '10m' | '30m' | '1h' | '1d' export type ProjectDailyStatsVariables = { diff --git a/apps/studio/data/projects/keys.ts b/apps/studio/data/projects/keys.ts index b56dbaf49e1..87d8627132d 100644 --- a/apps/studio/data/projects/keys.ts +++ b/apps/studio/data/projects/keys.ts @@ -3,6 +3,8 @@ export const projectKeys = { status: (projectRef: string | undefined) => ['project', projectRef, 'status'] as const, types: (projectRef: string | undefined) => ['project', projectRef, 'types'] as const, detail: (projectRef: string | undefined) => ['project', projectRef, 'detail'] as const, + serviceVersions: (projectRef: string | undefined) => + ['project', projectRef, 'service-versions'] as const, readonlyStatusList: () => ['projects', 'readonly-statuses'] as const, readonlyStatus: (projectRef: string | undefined) => ['projects', projectRef, 'readonly-status'] as const, diff --git a/apps/studio/data/projects/project-service-versions.ts b/apps/studio/data/projects/project-service-versions.ts new file mode 100644 index 00000000000..0fc53482b68 --- /dev/null +++ b/apps/studio/data/projects/project-service-versions.ts @@ -0,0 +1,42 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' + +import { get, handleError } from 'data/fetchers' +import type { ResponseError } from 'types' +import { projectKeys } from './keys' + +export type ProjectServiceVersionsVariables = { + projectRef?: string +} + +export async function getProjectServiceVersions( + { projectRef }: ProjectServiceVersionsVariables, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('projectRef is required') + + const { data, error } = await get(`/platform/projects/{ref}/service-versions`, { + params: { path: { ref: projectRef } }, + signal, + }) + if (error) handleError(error) + return data +} + +export type ProjectServiceVersionsData = Awaited> +export type ProjectServiceVersionsError = ResponseError + +export const useProjectServiceVersionsQuery = ( + { projectRef }: ProjectServiceVersionsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + projectKeys.serviceVersions(projectRef), + ({ signal }) => getProjectServiceVersions({ projectRef }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined', + ...options, + } + ) diff --git a/apps/studio/pages/api/projects/[ref]/settings.ts b/apps/studio/pages/api/projects/[ref]/settings.ts index ef989bf4b1e..f488d59db4f 100644 --- a/apps/studio/pages/api/projects/[ref]/settings.ts +++ b/apps/studio/pages/api/projects/[ref]/settings.ts @@ -34,9 +34,8 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { cloud_provider: 'AWS', db_dns_name: '-', db_host: 'localhost', - dp_ip_addr_config: 'legacy' as const, + db_ip_addr_config: 'legacy' as const, db_name: 'postgres', - // @ts-expect-error API is typed wrongly db_port: 5432, db_user: 'postgres', inserted_at: '2021-08-02T06:40:40.646Z', diff --git a/apps/studio/types/base.ts b/apps/studio/types/base.ts index 0f3742062e7..f9fe06c14cd 100644 --- a/apps/studio/types/base.ts +++ b/apps/studio/types/base.ts @@ -43,7 +43,6 @@ export interface Project extends ProjectBase { maxDatabasePreprovisionGb?: string | null parent_project_ref?: string is_branch_enabled?: boolean - serviceVersions: { gotrue: string; postgrest: string; 'supabase-postgres': string } /** * postgrestStatus is available on client side only. diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 6050797151f..e4922dc9677 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -959,6 +959,10 @@ export interface paths { /** Run project lints */ get: operations['ProjectRunLintsController_runProjectLints'] } + '/platform/projects/{ref}/service-versions': { + /** Gets service versions for a specific project */ + get: operations['ProjectServiceVersionsController_getServiceVersions'] + } '/platform/projects/{ref}/settings': { /** Gets project's settings */ get: operations['SettingsController_getProjectApi'] @@ -1205,6 +1209,10 @@ export interface paths { /** Creates a partner organization */ post: operations['AwsPartnerOrganizationsSystemController_createPartnerOrganization'] } + '/system/partner-organizations/{slug}': { + /** Converts an organization into a partner organization */ + put: operations['AwsPartnerOrganizationsSystemController_convertToPartnerOrganization'] + } '/system/projects': { /** Create a project */ post: operations['SystemProjectsController_createProject'] @@ -1225,6 +1233,20 @@ export interface paths { /** Allows a project to obtain temporary credentials. */ post: operations['AwsCredentialsController_getTemporaryCredentials'] } + '/system/projects/{ref}/disk': { + /** Get database disk attributes */ + get: operations['SystemProjectDiskController_getDisk'] + /** Modify database disk */ + post: operations['SystemProjectDiskController_modifyDisk'] + } + '/system/projects/{ref}/disk/util': { + /** Get disk utilization */ + get: operations['SystemProjectDiskController_getDiskUtilization'] + } + '/system/projects/{ref}/disk/volume_info': { + /** Get AWS disk volume information */ + get: operations['SystemProjectDiskController_getAwsDiskInfo'] + } '/system/projects/{ref}/functions': { /** * List all functions @@ -2095,6 +2117,12 @@ export interface paths { /** Updates project's Postgres config */ put: operations['v1-update-postgres-config'] } + '/v1/projects/{ref}/config/storage': { + /** Gets project's storage config */ + get: operations['v1-get-storage-config'] + /** Updates project's storage config */ + patch: operations['v1-update-storage-config'] + } '/v1/projects/{ref}/custom-hostname': { /** [Beta] Gets project's custom hostname config */ get: operations['v1-get-hostname-config'] @@ -2564,9 +2592,8 @@ export interface components { uri_allow_list: string | null } AuthHealthResponse: { - description: string - name: string - version: string + /** @enum {string} */ + name: 'GoTrue' } AutoApiService: { app: { @@ -2681,16 +2708,19 @@ export interface components { } BranchResetResponse: { message: string + workflow_run_id: string } BranchResponse: { created_at: string git_branch?: string id: string is_default: boolean + /** Format: int64 */ latest_check_run_id?: number name: string parent_project_ref: string persistent: boolean + /** Format: int32 */ pr_number?: number project_ref: string reset_on_push: boolean @@ -2728,6 +2758,9 @@ export interface components { /** @enum {string} */ privilege_type: 'ALL' | 'SELECT' | 'INSERT' | 'UPDATE' | 'REFERENCES' } + ConvertIntoAwsPartnerOrganizationBody: { + partner_billing: components['schemas']['AwsPartnerBillingBody'] + } CopyObjectBody: { from: string to: string @@ -3256,7 +3289,7 @@ export interface components { | '8_attached_volume_to_upgraded_instance' | '9_completed_upgrade' | '10_completed_post_physical_backup' - /** @enum {number} */ + /** @enum {integer} */ status: 0 | 1 | 2 target_version: number } @@ -3459,6 +3492,7 @@ export interface components { query: string } FunctionResponse: { + /** Format: int64 */ created_at: number entrypoint_path?: string id: string @@ -3468,11 +3502,13 @@ export interface components { slug: string /** @enum {string} */ status: 'ACTIVE' | 'REMOVED' | 'THROTTLED' + /** Format: int64 */ updated_at: number verify_jwt?: boolean version: number } FunctionSlugResponse: { + /** Format: int64 */ created_at: number entrypoint_path?: string id: string @@ -3482,6 +3518,7 @@ export interface components { slug: string /** @enum {string} */ status: 'ACTIVE' | 'REMOVED' | 'THROTTLED' + /** Format: int64 */ updated_at: number verify_jwt?: boolean version: number @@ -4205,6 +4242,7 @@ export interface components { } OAuthTokenResponse: { access_token: string + /** Format: int64 */ expires_in: number refresh_token: string /** @enum {string} */ @@ -4483,6 +4521,7 @@ export interface components { session_replication_role?: 'origin' | 'replica' | 'local' shared_buffers?: string statement_timeout?: string + track_commit_timestamp?: boolean wal_keep_size?: string wal_sender_timeout?: string work_mem?: string @@ -4796,10 +4835,6 @@ export interface components { | 'PAUSE_FAILED' | 'RESIZING' subscription_id: string - v2MaintenanceWindow: { - end?: string - start?: string - } volumeSizeGb?: number } ProjectInfo: { @@ -4927,10 +4962,9 @@ export interface components { cloud_provider: string db_dns_name: string db_host: string - /** @enum {string|null} */ - db_ip_addr_config: 'legacy' | 'static-ipv4' | 'concurrent-ipv6' | 'ipv6' | null + db_ip_addr_config: string db_name: string - db_port: string + db_port: number db_user: string inserted_at: string jwt_secret?: string @@ -5022,8 +5056,6 @@ export interface components { } RealtimeHealthResponse: { connected_cluster: number - db_connected: boolean - healthy: boolean } Relationship: { constraint_name: string @@ -5358,8 +5390,8 @@ export interface components { service_api_keys: components['schemas']['ServiceApiKeyResponse'][] } ServiceVersions: { - gotrue: string - postgrest: string + gotrue?: string + postgrest?: string 'supabase-postgres': string } SettingsResponse: { @@ -5441,6 +5473,7 @@ export interface components { visibility: 'user' | 'project' | 'org' | 'public' } SnippetProject: { + /** Format: int64 */ id: number name: string } @@ -5460,6 +5493,7 @@ export interface components { visibility: 'user' | 'project' | 'org' | 'public' } SnippetUser: { + /** Format: int64 */ id: number username: string } @@ -5487,8 +5521,15 @@ export interface components { updated_at: string } StorageConfigResponse: { + features: components['schemas']['StorageFeatures'] + /** Format: int64 */ fileSizeLimit: number - isFreeTier: boolean + } + StorageFeatureImageTransformation: { + enabled: boolean + } + StorageFeatures: { + imageTransformation: components['schemas']['StorageFeatureImageTransformation'] } StorageObject: { bucket_id: string @@ -6272,6 +6313,7 @@ export interface components { session_replication_role?: 'origin' | 'replica' | 'local' shared_buffers?: string statement_timeout?: string + track_commit_timestamp?: boolean wal_keep_size?: string wal_sender_timeout?: string work_mem?: string @@ -6356,10 +6398,9 @@ export interface components { public: boolean } UpdateStorageConfigBody: { - fileSizeLimit: number - } - UpdateStorageConfigResponse: { - fileSizeLimit: number + features?: components['schemas']['StorageFeatures'] + /** Format: int64 */ + fileSizeLimit?: number } UpdateSubscriptionBody: { payment_method?: string @@ -6698,7 +6739,9 @@ export interface components { pool_mode?: 'transaction' | 'session' | 'statement' } V1PhysicalBackup: { + /** Format: int64 */ earliest_physical_backup_date_unix?: number + /** Format: int64 */ latest_physical_backup_date_unix?: number } V1PostgrestConfigResponse: { @@ -6709,6 +6752,7 @@ export interface components { max_rows: number } V1ProjectRefResponse: { + /** Format: int64 */ id: number name: string ref: string @@ -6750,6 +6794,7 @@ export interface components { | 'RESIZING' } V1RestorePitrBody: { + /** Format: int64 */ recovery_time_target_unix: number } V1RunQueryBody: { @@ -7548,6 +7593,9 @@ export interface operations { /** Retrieve CLI login session */ CliLoginController_getCliLoginSession: { parameters: { + query?: { + device_code?: string + } path: { session_id: string } @@ -12350,7 +12398,7 @@ export interface operations { responses: { 200: { content: { - 'application/json': components['schemas']['UpdateStorageConfigResponse'] + 'application/json': components['schemas']['StorageConfigResponse'] } } 403: { @@ -12692,7 +12740,46 @@ export interface operations { DailyStatsController_getDailyStats: { parameters: { query: { - attribute: string + attribute: + | 'total_realtime_requests' + | 'total_realtime_egress' + | 'avg_cpu_usage' + | 'total_ingress' + | 'total_egress' + | 'total_requests' + | 'total_get_requests' + | 'total_patch_requests' + | 'total_post_requests' + | 'total_delete_requests' + | 'total_options_requests' + | 'total_supavisor_egress_bytes' + | 'total_rest_ingress' + | 'total_rest_egress' + | 'total_rest_requests' + | 'total_rest_get_requests' + | 'total_rest_post_requests' + | 'total_rest_patch_requests' + | 'total_rest_delete_requests' + | 'total_rest_options_requests' + | 'total_auth_billing_period_mau' + | 'total_auth_billing_period_sso_mau' + | 'total_auth_ingress' + | 'total_auth_egress' + | 'total_auth_requests' + | 'total_auth_get_requests' + | 'total_auth_post_requests' + | 'total_auth_patch_requests' + | 'total_auth_options_requests' + | 'total_auth_delete_requests' + | 'total_storage_ingress' + | 'total_storage_egress' + | 'total_storage_image_render_count' + | 'total_storage_requests' + | 'total_storage_get_requests' + | 'total_storage_post_requests' + | 'total_storage_delete_requests' + | 'total_storage_options_requests' + | 'total_storage_patch_requests' interval: string endDate: string startDate: string @@ -13123,6 +13210,22 @@ export interface operations { } } } + /** Gets service versions for a specific project */ + ProjectServiceVersionsController_getServiceVersions: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['ServiceVersions'] + } + } + } + } /** Gets project's settings */ SettingsController_getProjectApi: { parameters: { @@ -14486,6 +14589,29 @@ export interface operations { } } } + /** Converts an organization into a partner organization */ + AwsPartnerOrganizationsSystemController_convertToPartnerOrganization: { + parameters: { + path: { + /** @description Organization slug */ + slug: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['ConvertIntoAwsPartnerOrganizationBody'] + } + } + responses: { + 200: { + content: never + } + /** @description Unexpected error while converting organization into a partner organization */ + 500: { + content: never + } + } + } /** Create a project */ SystemProjectsController_createProject: { requestBody: { @@ -14582,6 +14708,92 @@ export interface operations { } } } + /** Get database disk attributes */ + SystemProjectDiskController_getDisk: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['DiskResponse'] + } + } + 403: { + content: never + } + /** @description Failed to get database disk attributes */ + 500: { + content: never + } + } + } + /** Modify database disk */ + SystemProjectDiskController_modifyDisk: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['DiskRequestBody'] + } + } + responses: { + 201: { + content: never + } + 403: { + content: never + } + /** @description Failed to modify database disk */ + 500: { + content: never + } + } + } + /** Get disk utilization */ + SystemProjectDiskController_getDiskUtilization: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['DiskUtilMetricsResponse'] + } + } + 403: { + content: never + } + /** @description Failed to get disk utilization */ + 500: { + content: never + } + } + } + /** Get AWS disk volume information */ + SystemProjectDiskController_getAwsDiskInfo: { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: never + } + } + } /** * List all functions * @description Returns all functions you've previously added to the specified project. @@ -15661,6 +15873,55 @@ export interface operations { } } } + /** Gets project's storage config */ + 'v1-get-storage-config': { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['StorageConfigResponse'] + } + } + 403: { + content: never + } + /** @description Failed to retrieve project's storage config */ + 500: { + content: never + } + } + } + /** Updates project's storage config */ + 'v1-update-storage-config': { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateStorageConfigBody'] + } + } + responses: { + 200: { + content: never + } + 403: { + content: never + } + /** @description Failed to update project's storage config */ + 500: { + content: never + } + } + } /** [Beta] Gets project's custom hostname config */ 'v1-get-hostname-config': { parameters: { @@ -15939,6 +16200,9 @@ export interface operations { 'application/json': components['schemas']['V1ServiceHealthResponse'][] } } + 403: { + content: never + } /** @description Failed to retrieve project's service health status */ 500: { content: never From a6a389def98594f3c3370b2f1be65a93d0661f28 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 8 Nov 2024 17:04:45 +0800 Subject: [PATCH 13/86] Deprecate old org roles query (#30372) --- .../Organization/AuditLogs/AuditLogs.tsx | 85 +++++++++---------- .../TeamSettings/TeamSettings.tsx | 6 +- .../organizations/organization-roles-query.ts | 48 ----------- 3 files changed, 42 insertions(+), 97 deletions(-) delete mode 100644 apps/studio/data/organizations/organization-roles-query.ts diff --git a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx index 8a0865d6e8e..8f016a3ff55 100644 --- a/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx +++ b/apps/studio/components/interfaces/Organization/AuditLogs/AuditLogs.tsx @@ -1,4 +1,3 @@ -import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import dayjs from 'dayjs' @@ -11,27 +10,28 @@ import { LogDetailsPanel } from 'components/interfaces/AuditLogs' import { ScaffoldContainerLegacy } from 'components/layouts/Scaffold' import Table from 'components/to-be-cleaned/Table' import AlertError from 'components/ui/AlertError' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { DatePicker } from 'components/ui/DatePicker' import { FilterPopover } from 'components/ui/FilterPopover' import NoPermission from 'components/ui/NoPermission' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query' import { AuditLog, useOrganizationAuditLogsQuery, } from 'data/organizations/organization-audit-logs-query' import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query' -import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useProjectsQuery } from 'data/projects/projects-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { - Alert, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, WarningIcon, } from 'ui' +import { Admonition } from 'ui-patterns' // [Joshen considerations] // - Maybe fix the height of the table to the remaining height of the viewport, so that the search input is always visible @@ -58,7 +58,7 @@ const AuditLogs = () => { const { data: projects } = useProjectsQuery() const { data: organizations } = useOrganizationsQuery() const { data: members } = useOrganizationMembersQuery({ slug }) - const { data: rolesData } = useOrganizationRolesQuery({ slug }) + const { data: rolesData } = useOrganizationRolesV2Query({ slug }) const { data, error, isLoading, isSuccess, isError, isRefetching, refetch } = useOrganizationAuditLogsQuery( { @@ -96,7 +96,7 @@ const AuditLogs = () => { return () => clearInterval(interval) }, [dateRange.from, dateRange.to]) - const roles = rolesData?.roles ?? [] + const roles = [...(rolesData?.org_scoped_roles ?? []), ...(rolesData?.project_scoped_roles ?? [])] const retentionPeriod = data?.retention_period ?? 0 const logs = data?.result ?? [] @@ -229,16 +229,22 @@ const AuditLogs = () => { }} renderFooter={() => { return ( - - Your organization has a log retention period of{' '} - - {retentionPeriod} day - {retentionPeriod > 1 ? 's' : ''} - - . You may only view logs from{' '} - {dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the - earliest date. - + +
+ Your organization has a log retention period of{' '} + + {retentionPeriod} day + {retentionPeriod > 1 ? 's' : ''} + + . You may only view logs from{' '} + {dayjs().subtract(retentionPeriod, 'days').format('DD MMM YYYY')} as the + earliest date. +
+
) }} /> @@ -284,37 +290,24 @@ const AuditLogs = () => {

Date

- - -
, , diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx index c33d8ca7c17..149f5631e32 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/TeamSettings.tsx @@ -11,9 +11,9 @@ import { ScaffoldSectionContent, } from 'components/layouts/Scaffold' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query' import { useOrganizationMemberDeleteMutation } from 'data/organizations/organization-member-delete-mutation' import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query' -import { useOrganizationRolesQuery } from 'data/organizations/organization-roles-query' import { usePermissionsQuery } from 'data/permissions/permissions-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' @@ -37,10 +37,10 @@ const TeamSettings = () => { const isOwner = selectedOrganization?.is_owner const { data: permissions } = usePermissionsQuery() - const { data: rolesData } = useOrganizationRolesQuery({ slug }) + const { data: rolesData } = useOrganizationRolesV2Query({ slug }) const { data: members } = useOrganizationMembersQuery({ slug }) - const roles = rolesData?.roles ?? [] + const roles = rolesData?.org_scoped_roles ?? [] const { rolesAddable } = useGetRolesManagementPermissions( selectedOrganization?.slug, diff --git a/apps/studio/data/organizations/organization-roles-query.ts b/apps/studio/data/organizations/organization-roles-query.ts deleted file mode 100644 index 1331c9c1b35..00000000000 --- a/apps/studio/data/organizations/organization-roles-query.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { useQuery, UseQueryOptions } from '@tanstack/react-query' - -import { get, handleError } from 'data/fetchers' -import type { ResponseError, Role } from 'types' -import { organizationKeys } from './keys' - -export type OrganizationRolesVariables = { - slug?: string -} - -export type OrganizationRolesResponse = { - roles: Role[] -} - -export async function getOrganizationRoles( - { slug }: OrganizationRolesVariables, - signal?: AbortSignal -) { - if (!slug) throw new Error('slug is required') - - const { data, error } = await get('/platform/organizations/{slug}/roles', { - params: { path: { slug } }, - signal, - }) - - if (error) handleError(error) - - return { roles: data } as unknown as OrganizationRolesResponse -} - -export type OrganizationRolesData = Awaited> -export type OrganizationRolesError = ResponseError - -export const useOrganizationRolesQuery = ( - { slug }: OrganizationRolesVariables, - { - enabled = true, - ...options - }: UseQueryOptions = {} -) => - useQuery( - organizationKeys.roles(slug), - ({ signal }) => getOrganizationRoles({ slug }, signal), - { - enabled: enabled && typeof slug !== 'undefined', - ...options, - } - ) From 4342c4c3ab1fd634b1210e55e24424b6af8151f0 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 8 Nov 2024 17:32:38 +0800 Subject: [PATCH 14/86] Chore/disk mgt UI fixes (#30358) * Fix percentage width for disk space bar * Add warning in disk size section if 90% + within cooldown * Show read only warning in compute and disks * Fix copy * Add conditional --- .../DiskManagement/DiskManagementForm.tsx | 71 ++++++++++++++----- .../DiskManagement/fields/DiskSizeField.tsx | 4 +- .../DiskManagement/ui/DiskSpaceBar.tsx | 33 +++++---- .../ui/DiskTypeRecommendationSection.tsx | 2 +- ...sourceExhaustionWarningBanner.constants.ts | 2 +- .../ResourceExhaustionWarningBanner.tsx | 10 +-- 6 files changed, 84 insertions(+), 38 deletions(-) diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index 818a3d88e4f..b9f2d95490f 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -11,6 +11,7 @@ import { toast } from 'sonner' import { useParams } from 'common' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { MAX_WIDTH_CLASSES, PADDING_CLASSES, ScaffoldContainer } from 'components/layouts/Scaffold' +import { DocsButton } from 'components/ui/DocsButton' import { useDiskAttributesQuery, useRemainingDurationForDiskAttributeUpdate, @@ -25,7 +26,7 @@ import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { AddonVariantId } from 'data/subscriptions/types' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { PROJECT_STATUS } from 'lib/constants' +import { GB, PROJECT_STATUS } from 'lib/constants' import { Button, cn, @@ -35,9 +36,11 @@ import { Form_Shadcn_, Separator, } from 'ui' +import { Admonition } from 'ui-patterns' import { FormFooterChangeBadge } from '../DataWarehouse/FormFooterChangeBadge' import { CreateDiskStorageSchema, DiskStorageSchemaType } from './DiskManagement.schema' -import { mapComputeSizeNameToAddonVariantId, showMicroUpgrade } from './DiskManagement.utils' +import { DiskManagementMessage } from './DiskManagement.types' +import { mapComputeSizeNameToAddonVariantId } from './DiskManagement.utils' import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection' import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog' import { ComputeSizeField } from './fields/ComputeSizeField' @@ -53,7 +56,7 @@ import { } from './ui/DiskManagement.constants' import { NoticeBar } from './ui/NoticeBar' import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection' -import { DiskManagementMessage } from './DiskManagement.types' +import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query' export function DiskManagementForm() { const { @@ -65,6 +68,12 @@ export function DiskManagementForm() { const { ref: projectRef } = useParams() const queryClient = useQueryClient() + const { data: resourceWarnings } = useResourceWarningsQuery() + const projectResourceWarnings = (resourceWarnings ?? [])?.find( + (warning) => warning.project === project?.ref + ) + const isReadOnlyMode = projectResourceWarnings?.is_readonly_mode_enabled + /** * Permissions */ @@ -86,18 +95,8 @@ export function DiskManagementForm() { /** * Fetch form data */ - const { - data: databases, - // isLoading: isReadReplicasLoading, - // error: readReplicasError, - isSuccess: isReadReplicasSuccess, - } = useReadReplicasQuery({ projectRef }) - const { - data, - // isLoading: isDiskAttributesLoading, - // error: diskAttributesError, - isSuccess: isDiskAttributesSuccess, - } = useDiskAttributesQuery( + const { data: databases, isSuccess: isReadReplicasSuccess } = useReadReplicasQuery({ projectRef }) + const { data, isSuccess: isDiskAttributesSuccess } = useDiskAttributesQuery( { projectRef }, { refetchInterval, @@ -127,7 +126,7 @@ export function DiskManagementForm() { useRemainingDurationForDiskAttributeUpdate({ projectRef, }) - const { isSuccess: isDiskUtilizationSuccess } = useDiskUtilizationQuery({ + const { data: diskUtil, isSuccess: isDiskUtilizationSuccess } = useDiskUtilizationQuery({ projectRef, }) const { data: subscription, isSuccess: isSubscriptionSuccess } = useOrgSubscriptionQuery({ @@ -173,6 +172,9 @@ export function DiskManagementForm() { const isPlanUpgradeRequired = subscription?.plan.id === 'free' const { formState } = form + const usedSize = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100 + const totalSize = formState.defaultValues?.totalSize || 0 + const usedPercentage = (usedSize / totalSize) * 100 const isFlyArchitecture = project?.cloud_provider === 'FLY' @@ -300,7 +302,6 @@ export function DiskManagementForm() { - {!isFlyArchitecture && ( <> +
+ + {!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && ( + + + + )} + {isReadOnlyMode && ( + + + + )} +
- + - @@ -25,7 +27,9 @@ interface DiskSpaceBarProps { export default function DiskSpaceBar({ form }: DiskSpaceBarProps) { const { ref } = useParams() + const { resolvedTheme } = useTheme() const { formState, watch } = form + const isDarkMode = resolvedTheme?.includes('dark') const { data: diskUtil, @@ -50,6 +54,7 @@ export default function DiskSpaceBar({ form }: DiskSpaceBarProps) { projectRef: project?.ref, connectionString: project?.connectionString, }) + const { remainingDuration } = useRemainingDurationForDiskAttributeUpdate({ projectRef: ref }) const databaseSizeBytes = data?.result[0].db_size ?? 0 return ( @@ -91,16 +96,21 @@ export default function DiskSpaceBar({ form }: DiskSpaceBarProps) { >
= 90 && remainingDuration > 0 + ? 'bg-destructive' + : 'bg-foreground', + 'relative overflow-hidden transition-all duration-500 ease-in-out' + )} + style={{ width: `${usedPercentage >= 100 ? 100 : usedPercentage}%` }} >
@@ -126,15 +138,15 @@ export default function DiskSpaceBar({ form }: DiskSpaceBarProps) {
= 100 ? 100 : newUsedPercentage}%` }} >
New disk size diff --git a/apps/studio/components/interfaces/DiskManagement/ui/DiskTypeRecommendationSection.tsx b/apps/studio/components/interfaces/DiskManagement/ui/DiskTypeRecommendationSection.tsx index b637daf3489..4e09f85aa73 100644 --- a/apps/studio/components/interfaces/DiskManagement/ui/DiskTypeRecommendationSection.tsx +++ b/apps/studio/components/interfaces/DiskManagement/ui/DiskTypeRecommendationSection.tsx @@ -6,7 +6,7 @@ import { Admonition } from 'ui-patterns/admonition' import { DiskStorageSchemaType } from '../DiskManagement.schema' import { DISK_LIMITS, DISK_TYPE_OPTIONS, DiskType } from './DiskManagement.constants' -export function DiskTypeReccomendationSection({ +export function DiskTypeRecommendationSection({ actions, form, }: { diff --git a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts index c4a8540e494..fc10cb76512 100644 --- a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts +++ b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts @@ -43,7 +43,7 @@ export const RESOURCE_WARNING_MESSAGES: Record = }, }, docsUrl: 'https://supabase.com/docs/guides/platform/database-size#disabling-read-only-mode', - buttonText: 'View database settings', + buttonText: 'View Compute and Disk', metric: 'read_only', }, disk_io_exhaustion: { diff --git a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx index 01a303471c0..bf79befa704 100644 --- a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx +++ b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx @@ -78,8 +78,8 @@ const ResourceExhaustionWarningBanner = () => { const correctionUrlVariants = { undefined: undefined, null: '/project/[ref]/settings/[infra-path]', - disk_space: '/project/[ref]/settings/database', - read_only: '/project/[ref]/settings/database', + disk_space: '/project/[ref]/settings/compute-and-disk', + read_only: '/project/[ref]/settings/compute-and-disk', auth_email_rate_limit: '/project/[ref]/settings/auth', auth_restricted_email_sending: '/project/[ref]/settings/auth', default: (metric: string) => `/project/[ref]/settings/[infra-path]#${metric}`, @@ -114,7 +114,7 @@ const ResourceExhaustionWarningBanner = () => { const onUsageOrInfraAndNotInReadOnlyMode = isUsageOrInfraPage && !activeWarnings.includes('is_readonly_mode_enabled') const onDatabaseSettingsAndInReadOnlyMode = - router.pathname.endsWith('settings/database') && + router.pathname.endsWith('settings/compute-and-disk') && activeWarnings.includes('is_readonly_mode_enabled') // these take precedence over each other, so there's only one active warning to check @@ -149,7 +149,9 @@ const ResourceExhaustionWarningBanner = () => { onUsageOrInfraAndNotInReadOnlyMode || onDatabaseSettingsAndInReadOnlyMode || !isVisible || - bannerAcknowledged + (router.pathname.includes('/auth/') && + !!projectResourceWarnings?.auth_restricted_email_sending && + bannerAcknowledged) ) { return null } From 225d4613814dc828d6431ec4646525e1acbd8e55 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 8 Nov 2024 18:03:30 +0800 Subject: [PATCH 15/86] Hook up RLS to new assistant if feature flag is enabled (#30357) * Hook up RLS to new assistant if feature flag is enabled * Reset conversation if opening a different entity * Add prompt to use alter policy if policy definition is provided * Remove console log --- .../AIPolicyEditorPanel.utils.ts | 3 +- .../LockedQuerySection.tsx | 47 -------------- .../Policies/AIPolicyEditorPanel/index.tsx | 2 - .../Policies/PolicyTableRow/PolicyRow.tsx | 26 +++++++- .../PolicyTableRow/PolicyTableRowHeader.tsx | 61 +++++++++-------- .../Functions/FunctionsList/FunctionList.tsx | 28 +++++--- .../ui/AIAssistantPanel/AIAssistant.tsx | 65 ++++++++++++++----- .../ui/AIAssistantPanel/AIAssistant.utils.ts | 34 +++++++++- .../ui/AIAssistantPanel/AIAssistantPanel.tsx | 13 ++-- .../AIAssistantPanel/EntitiesDropdownMenu.tsx | 10 +-- .../data/database/entity-definition-query.ts | 59 +++++++++++++++-- apps/studio/pages/api/ai/sql/generate-v2.ts | 36 ++++++---- apps/studio/pages/api/ai/sql/suggest.ts | 2 +- apps/studio/state/app-state.ts | 5 +- packages/ai-commands/src/sql/chat.ts | 2 + packages/ai-commands/src/sql/rls.ts | 2 + 16 files changed, 254 insertions(+), 141 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyEditorPanel.utils.ts b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyEditorPanel.utils.ts index e68356d2d98..937eaaa4f1d 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyEditorPanel.utils.ts +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyEditorPanel.utils.ts @@ -1,7 +1,8 @@ import type { PostgresPolicy } from '@supabase/postgres-meta' +import { isEqual } from 'lodash' + import type { Message } from 'ai/react' import { uuidv4 } from 'lib/helpers' -import { isEqual } from 'lodash' export type MessageWithDebug = Message & { isDebug: boolean } diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/LockedQuerySection.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/LockedQuerySection.tsx index 429de771a3e..5f4e045b7d1 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/LockedQuerySection.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/LockedQuerySection.tsx @@ -1,29 +1,17 @@ import { PostgresPolicy } from '@supabase/postgres-meta' import { Lock } from 'lucide-react' -import { useRouter } from 'next/router' - -import { useParams } from 'common' -import { Button } from 'ui' -import { generateAlterPolicyQuery, generateCreatePolicyQuery } from './AIPolicyEditorPanel.utils' interface LockedCreateQuerySection { schema: string selectedPolicy?: PostgresPolicy formFields: { name: string; table: string; behavior: string; command: string; roles: string } - editorOneRef: any - editorTwoRef: any } export const LockedCreateQuerySection = ({ schema, selectedPolicy, formFields, - editorOneRef, - editorTwoRef, }: LockedCreateQuerySection) => { - const router = useRouter() - const { ref } = useParams() - const isEditing = selectedPolicy !== undefined const { name, table, behavior, command, roles } = formFields @@ -38,41 +26,6 @@ export const LockedCreateQuerySection = ({ Use options above to edit

- {/* */}

1

diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx index ba6aa9ab2ae..62dfc833efb 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx @@ -595,8 +595,6 @@ export const AIPolicyEditorPanel = memo(function ({ diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx index d6607237fea..a00bc79cba1 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx @@ -21,6 +21,8 @@ import { TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, } from 'ui' +import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { useAppStateSnapshot } from 'state/app-state' interface PolicyRowProps { policy: PostgresPolicy @@ -33,6 +35,8 @@ const PolicyRow = ({ onSelectEditPolicy = noop, onSelectDeletePolicy = noop, }: PolicyRowProps) => { + const { setAiAssistantPanel } = useAppStateSnapshot() + const enableAssistantV2 = useIsDatabaseFunctionsAssistantEnabled() const canUpdatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies') const { project } = useProjectContext() @@ -88,11 +92,31 @@ const PolicyRow = ({ - - - {!canToggleRLS - ? 'You need additional permissions to create RLS policies' - : 'Create with Supabase Assistant'} - - + { + if (enableAssistantV2) { + setAiAssistantPanel({ + open: true, + editor: 'rls-policies', + entity: undefined, + tables: [{ schema: table.schema, name: table.name }], + }) + } else { + onSelectCreatePolicy() + setEditView('conversation') + } + }} + tooltip={{ + content: { + side: 'bottom', + text: !canToggleRLS + ? 'You need additional permissions to create RLS policies' + : 'Create with Supabase Assistant', + }, + }} + > + +
)} diff --git a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx index b5f5bc8021d..263da4ab55b 100644 --- a/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx +++ b/apps/studio/components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx @@ -11,12 +11,14 @@ import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useAppStateSnapshot } from 'state/app-state' import { Button, + cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from 'ui' +import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' interface FunctionListProps { schema: string @@ -35,6 +37,7 @@ const FunctionList = ({ }: FunctionListProps) => { const router = useRouter() const { project: selectedProject } = useProjectContext() + const enableAssistantV2 = useIsDatabaseFunctionsAssistantEnabled() const { setAiAssistantPanel } = useAppStateSnapshot() const { data: functions } = useDatabaseFunctionsQuery({ @@ -108,7 +111,10 @@ const FunctionList = ({ + + +
+ setIsClosing(false)} + onConfirm={() => onClose()} + > +

+ There are unsaved changes. Are you sure you want to close the panel? Your changes will be + lost. +

+
+ {pgPartmanExtension && ( + setShowEnableExtensionModal(false)} + /> + )} + + ) +} diff --git a/apps/studio/components/interfaces/Integrations/Queues/Queues.constants.tsx b/apps/studio/components/interfaces/Integrations/Queues/Queues.constants.tsx new file mode 100644 index 00000000000..6ba001477a5 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/Queues.constants.tsx @@ -0,0 +1,23 @@ +import { Rows2, Rows3, Rows4 } from 'lucide-react' + +export const QUEUE_TYPES = [ + { + value: 'normal', + icon: , + label: 'Normal queue', + description: 'Create a normal queue.', + }, + { + value: 'partitioned', + icon: , + label: 'Partitioned queue', + description: 'Create a partitioned queue which is optimized for large amount of messages', + }, + + { + value: 'unlogged', + icon: , + label: 'Unlogged queue', + description: 'Creates an unlogged queue which loses all data on database restart.', + }, +] as const diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesDisabledState.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesDisabledState.tsx new file mode 100644 index 00000000000..ad4331f6706 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesDisabledState.tsx @@ -0,0 +1,88 @@ +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { ExternalLink } from 'lucide-react' +import Link from 'next/link' +import { useState } from 'react' + +import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' +import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' +import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { Button } from 'ui' + +export const QueuesDisabledState = () => { + const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) + const { project } = useProjectContext() + const canToggleExtensions = useCheckPermissions( + PermissionAction.TENANT_SQL_ADMIN_WRITE, + 'extensions' + ) + + const { data } = useDatabaseExtensionsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const pgmqExtension = (data ?? []).find((ext) => ext.name === 'pgmq') + + if (!pgmqExtension) return null + + return ( + <> +
+ +
+

+ Message queues in PostgreSQL allow you to handle asynchronous tasks, manage workloads, + and enable reliable communication between different parts of your application. +

+

Enable queues in your project.

+
+ +
+ + + + + {!canToggleExtensions && ( + + + +
+ + You need additional permissions to enable pgmq for this project + +
+
+
+ )} +
+ +
+
+
+ + setShowEnableExtensionModal(false)} + /> + + ) +} diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesListing.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesListing.tsx new file mode 100644 index 00000000000..f34ad985e9d --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesListing.tsx @@ -0,0 +1,134 @@ +import { useEffect, useState } from 'react' + +import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import Table from 'components/to-be-cleaned/Table' +import AlertError from 'components/ui/AlertError' +import { FormHeader } from 'components/ui/Forms/FormHeader' +import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' +import { useQueuesQuery } from 'data/database-queues/database-queues-query' +import { Search } from 'lucide-react' +import { useQueryState } from 'nuqs' +import { Button, Input, Sheet, SheetContent } from 'ui' +import { CreateQueueSheet } from './CreateQueueSheet' +import { QueuesDisabledState } from './QueuesDisabledState' +import { QueuesRows } from './QueuesRows' + +export const QueuesListing = () => { + const { project } = useProjectContext() + + const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) + // used for confirmation prompt in the Create Queue Sheet + const [isClosingCreateQueueSheet, setIsClosingCreateQueueSheet] = useState(false) + const [createQueueSheetShown, setCreateQueueSheetShown] = useState(false) + + const { + data: queues, + error, + isLoading, + isError, + refetch, + } = useQueuesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const { data: extensions, isLoading: isLoadingExtensions } = useDatabaseExtensionsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const pgmqExtension = (extensions ?? []).find((ext) => ext.name === 'pgmq') + // TODO: Change this to true for local development to work + const pgmqExtensionInstalled = pgmqExtension?.installed_version + + useEffect(() => { + // refetch the queues after the pgmq extension has been installed + if (pgmqExtensionInstalled && isError) { + refetch() + } + }, [isError, pgmqExtensionInstalled, refetch]) + + const [searchQuery, setSearchQuery] = useQueryState('search') + + // this avoid showing loading screen when the extension is not installed. Otherwise, we'll have to wait for three + // retries (which are sure to fail because the extension is not installed) + if (isLoadingExtensions) return + if (!pgmqExtensionInstalled) return + if (isLoading) return + if (isError) return + + return ( + <> +
+ + {queues.length === 0 ? ( +
+

No queues created yet

+ +
+ ) : ( +
+
+ } + value={searchQuery || ''} + className="w-64" + onChange={(e) => setSearchQuery(e.target.value)} + /> + + +
+ + + Name + + Type + + + Created at + + + + } + body={} + /> + + )} + + + setIsClosingCreateQueueSheet(true)}> + + { + setIsClosingCreateQueueSheet(false) + setCreateQueueSheetShown(false) + }} + isClosing={isClosingCreateQueueSheet} + setIsClosing={setIsClosingCreateQueueSheet} + /> + + + {pgmqExtension ? ( + setShowEnableExtensionModal(false)} + /> + ) : null} + + ) +} diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesRows.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesRows.tsx new file mode 100644 index 00000000000..ec68ed69772 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesRows.tsx @@ -0,0 +1,70 @@ +import dayjs from 'dayjs' +import { includes, sortBy } from 'lodash' +import { ChevronRight } from 'lucide-react' + +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import Table from 'components/to-be-cleaned/Table' +import { PostgresQueue } from 'data/database-queues/database-queues-query' +import { DATETIME_FORMAT } from 'lib/constants' +import { useRouter } from 'next/router' + +interface QueuesRowsProps { + queues: PostgresQueue[] + filterString: string +} + +export const QueuesRows = ({ queues: fetchedQueues, filterString }: QueuesRowsProps) => { + const router = useRouter() + const { project: selectedProject } = useProjectContext() + + const filteredQueues = fetchedQueues.filter((x) => + includes(x.queue_name.toLowerCase(), filterString.toLowerCase()) + ) + const queues = sortBy(filteredQueues, (func) => func.queue_name.toLocaleLowerCase()) + + if (queues.length === 0 && filterString.length > 0) { + return ( + + +

No results found

+

+ Your search for "{filterString}" did not return any results +

+
+
+ ) + } + + return ( + <> + {queues.map((q) => { + const type = q.is_partitioned ? 'Partitioned' : q.is_unlogged ? 'Unlogged' : 'Regular' + + return ( + { + router.push(`/project/${selectedProject?.ref}/integrations/queues/${q.queue_name}`) + }} + className="hover:" + > + +

{q.queue_name}

+
+ +

+ {type} +

+
+ +

{dayjs(q.created_at).format(DATETIME_FORMAT)}

+
+ + + +
+ ) + })} + + ) +} diff --git a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/DeleteQueue.tsx b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/DeleteQueue.tsx new file mode 100644 index 00000000000..20c9e6812f8 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/DeleteQueue.tsx @@ -0,0 +1,62 @@ +import { useRouter } from 'next/router' +import { toast } from 'sonner' + +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useDatabaseQueueDeleteMutation } from 'data/database-queues/database-queues-delete-mutation' +import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' + +interface DeleteQueueProps { + queueName: string + visible: boolean + onClose: () => void +} + +const DeleteQueue = ({ queueName, visible, onClose }: DeleteQueueProps) => { + const { project } = useProjectContext() + const router = useRouter() + + const { mutate: deleteDatabaseQueue, isLoading } = useDatabaseQueueDeleteMutation({ + onSuccess: () => { + toast.success(`Successfully removed queue ${queueName}`) + router.push(`/project/${project?.ref}/integrations/queues`) + onClose() + }, + }) + + async function handleDelete() { + if (!project) return console.error('Project is required') + + deleteDatabaseQueue({ + queueName: queueName, + projectRef: project.ref, + connectionString: project.connectionString, + }) + } + + if (!queueName) { + return null + } + + return ( + onClose()} + onConfirm={handleDelete} + title="Delete this queue" + loading={isLoading} + confirmLabel={`Delete queue ${queueName}`} + confirmPlaceholder="Type in name of queue" + confirmString={queueName ?? 'Unknown'} + text={ + <> + This will delete the queue{' '} + {queueName} + + } + alert={{ title: 'You cannot recover this queue and its messages once deleted.' }} + /> + ) +} + +export default DeleteQueue diff --git a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/MessageDetailsPanel.tsx b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/MessageDetailsPanel.tsx new file mode 100644 index 00000000000..e7fed0b97e2 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/MessageDetailsPanel.tsx @@ -0,0 +1,148 @@ +import { isNil, noop } from 'lodash' +import { Archive, X } from 'lucide-react' +import { useState } from 'react' + +import { useParams } from 'common' +import { MonacoEditor } from 'components/grid/components/common/MonacoEditor' +import { RowAction, RowData } from 'components/interfaces/Auth/Users/UserOverview' +import { useDatabaseQueueMessageArchiveMutation } from 'data/database-queues/database-queue-messages-archive-mutation' +import { PostgresQueueMessage } from 'data/database-queues/database-queue-messages-infinite-query' +import dayjs from 'dayjs' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { prettifyJSON } from 'lib/helpers' +import { + Button, + ResizablePanel, + Separator, + Tabs_Shadcn_, + TabsContent_Shadcn_, + TabsList_Shadcn_, + TabsTrigger_Shadcn_, +} from 'ui' + +export const DATE_FORMAT = 'DD MMM, YYYY HH:mm' + +interface MessageDetailsPanelProps { + selectedMessage: PostgresQueueMessage + setSelectedMessage: (value: number | null) => void +} + +const tryFormatInitialValue = (value: string) => { + try { + const jsonValue = JSON.parse(value) + return JSON.stringify(jsonValue) + } catch (err) { + if (typeof value === 'string') { + return value.replaceAll(`\"`, `"`) + } else { + return JSON.stringify(value) + } + } +} + +export const MessageDetailsPanel = ({ + selectedMessage, + setSelectedMessage, +}: MessageDetailsPanelProps) => { + const { id: _id } = useParams() + const { name: queueName } = useParams() + const project = useSelectedProject() + + const { mutate, isLoading, isSuccess } = useDatabaseQueueMessageArchiveMutation() + + const initialValue = JSON.stringify(selectedMessage?.message) + const jsonString = prettifyJSON(!isNil(initialValue) ? tryFormatInitialValue(initialValue) : '') + + const [view, setView] = useState<'details' | 'suggestion'>('details') + + return ( + setSelectedMessage(null)} + className="bg-studio border-t pointer-events-auto" + > + + } + /> + + + + + setDeleteQueueModalShown(false)} + /> + + ) +} + +QueueMessagesPage.getLayout = (page) => ( + {page} +) + +export default QueueMessagesPage diff --git a/apps/studio/pages/project/[ref]/integrations/queues/index.tsx b/apps/studio/pages/project/[ref]/integrations/queues/index.tsx new file mode 100644 index 00000000000..de0b4eb246f --- /dev/null +++ b/apps/studio/pages/project/[ref]/integrations/queues/index.tsx @@ -0,0 +1,30 @@ +import { QueuesListing } from 'components/interfaces/Integrations/Queues/QueuesListing' +import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' +import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' +import type { NextPageWithLayout } from 'types' + +const QueuesPage: NextPageWithLayout = () => { + // TODO: Change this to the correct permissions + // const canReadFunctions = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'functions') + // const isPermissionsLoaded = usePermissionsLoaded() + + // if (isPermissionsLoaded && !canReadFunctions) { + // return + // } + + return ( + + +
+ +
+
+
+ ) +} + +QueuesPage.getLayout = (page) => ( + {page} +) + +export default QueuesPage diff --git a/apps/studio/pages/project/[ref]/integrations/wrappers/new.tsx b/apps/studio/pages/project/[ref]/integrations/wrappers/new.tsx index fbf85a115a6..babc1819c1e 100644 --- a/apps/studio/pages/project/[ref]/integrations/wrappers/new.tsx +++ b/apps/studio/pages/project/[ref]/integrations/wrappers/new.tsx @@ -1,7 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { CreateWrapper } from 'components/interfaces/Database' -import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' +import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' import NoPermission from 'components/ui/NoPermission' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' @@ -17,6 +17,8 @@ const DatabaseWrappersNew: NextPageWithLayout = () => { return } -DatabaseWrappersNew.getLayout = (page) => {page} +DatabaseWrappersNew.getLayout = (page) => ( + {page} +) export default DatabaseWrappersNew From 0b5573ecb51a2772440fb0c720cc729208742174 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 8 Nov 2024 20:17:41 +0800 Subject: [PATCH 18/86] Add lat long for aws regions (#30376) * Remove console log * Add comment --- .../InstanceConfiguration.constants.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts index 36c994eeb44..40dc9aaa882 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts @@ -25,7 +25,7 @@ export const REPLICA_STATUS: { INIT_READ_REPLICA_FAILED: 'INIT_READ_REPLICA_FAILED', } -// [Joshen] Coordinates from https://github.com/jsonmaur/aws-regions/issues/11 +// [Joshen] Coordinates from https://github.com/tobilg/aws-edge-locations/blob/main/data/aws-edge-locations.json // In the format of [lon, lat] export const AWS_REGIONS_COORDINATES: { [key: string]: [number, number] } = { SOUTHEAST_ASIA: [103.8, 1.37], @@ -40,10 +40,10 @@ export const AWS_REGIONS_COORDINATES: { [key: string]: [number, number] } = { SOUTH_ASIA: [72.88, 19.08], OCEANIA: [151.2, -33.86], SOUTH_AMERICA: [-46.38, -23.34], - - // CENTRAL_EU_2: To find, + // [Joshen] These regions are not supported yet, just need to uncomment them once ready + // CENTRAL_EU_2: [8.54, 47.45], // EAST_US_2: [-83, 39.96], - // NORTH_EU: To find, + // NORTH_EU: [17.91, 59.65], // WEST_EU_3: [2.35, 48.86], } From 2e6809a34083fb9dc2e5342b96b4c92a715b01bb Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Fri, 8 Nov 2024 09:29:33 -0330 Subject: [PATCH 19/86] Chore/extension disable warning (#30338) Add warning in extension disable confirm modal --- .../Database/Extensions/ExtensionCard.tsx | 22 ++++++++++++++----- .../Extensions/Extensions.constants.ts | 4 ++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx b/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx index 75c3dc010c5..a393c9eb2d5 100644 --- a/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx @@ -1,5 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Book, Github, Loader2, Settings } from 'lucide-react' +import { Book, Github, Loader2, Settings, AlertCircle } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' import { toast } from 'sonner' @@ -9,9 +9,11 @@ import { useDatabaseExtensionDisableMutation } from 'data/database-extensions/da import { DatabaseExtension } from 'data/database-extensions/database-extensions-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { extensions } from 'shared-data' -import { Button, cn, Switch } from 'ui' +import { Button, cn, Switch, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_ } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import EnableExtensionModal from './EnableExtensionModal' +import { EXTENSION_DISABLE_WARNINGS } from './Extensions.constants' +import { Alert } from '@ui/components/shadcn/ui/alert' interface ExtensionCardProps { extension: DatabaseExtension @@ -166,9 +168,19 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => { onCancel={() => setIsDisableModalOpen(false)} onConfirm={() => onConfirmDisable()} > -

- Are you sure you want to turn OFF the "{extension.name}" extension? -

+
+

+ Are you sure you want to turn OFF the "{extension.name}" extension? +

+ {EXTENSION_DISABLE_WARNINGS[extension.name] && ( + + + + {EXTENSION_DISABLE_WARNINGS[extension.name]} + + + )} +
) diff --git a/apps/studio/components/interfaces/Database/Extensions/Extensions.constants.ts b/apps/studio/components/interfaces/Database/Extensions/Extensions.constants.ts index 09083125b0f..5d662357a5d 100644 --- a/apps/studio/components/interfaces/Database/Extensions/Extensions.constants.ts +++ b/apps/studio/components/interfaces/Database/Extensions/Extensions.constants.ts @@ -19,3 +19,7 @@ export const HIDDEN_EXTENSIONS = [ export const SEARCH_TERMS: Record = { vector: ['pgvector', 'pg_vector'], } + +export const EXTENSION_DISABLE_WARNINGS: Record = { + pg_cron: 'Disabling this extension will delete all scheduled jobs. This cannot be undone.', +} From 9cdabe188e26b4e4f36c31936e9c9c3ea03ed914 Mon Sep 17 00:00:00 2001 From: Enyosolutions Date: Fri, 8 Nov 2024 16:50:11 +0000 Subject: [PATCH 20/86] Doc: update documentation for secret name. (#30370) --- apps/docs/content/guides/auth/auth-hooks.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/auth/auth-hooks.mdx b/apps/docs/content/guides/auth/auth-hooks.mdx index cee056a29f6..9d35a938a5e 100644 --- a/apps/docs/content/guides/auth/auth-hooks.mdx +++ b/apps/docs/content/guides/auth/auth-hooks.mdx @@ -241,7 +241,7 @@ secrets = "env(SEND_SMS_HOOK_SECRET)" Fill in the Hook Secret in `supabase/functions/.env` ```bash -SEND_SMS_HOOK_SECRETS='v1,whsec_' +SEND_SMS_HOOK_SECRET='v1,whsec_' ``` Start the function locally: From d7c6eb1739eac88357cdc7f66b04a40453bfd54c Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Sun, 10 Nov 2024 18:52:50 +0800 Subject: [PATCH 21/86] fix: remove reference to Hugo templating in Auth Email guide (#30354) fix: remove reference to Hugo teplating --- apps/docs/content/guides/auth/auth-email-templates.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/auth/auth-email-templates.mdx b/apps/docs/content/guides/auth/auth-email-templates.mdx index f185d2a5f90..265ffd32807 100644 --- a/apps/docs/content/guides/auth/auth-email-templates.mdx +++ b/apps/docs/content/guides/auth/auth-email-templates.mdx @@ -86,7 +86,7 @@ const { ## Customization -Supabase Auth makes use of [Go Templates](https://pkg.go.dev/text/template). This means it is possible to conditionally render information based on template properties.You may wish to checkout this [guide by Hugo](https://gohugo.io/templates/introduction/) for a guide on the templating language. +Supabase Auth makes use of [Go Templates](https://pkg.go.dev/text/template). This means it is possible to conditionally render information based on template properties. ### Send different email to early access users From 3369164f9660cd2f18fbce493d0840e911008f3a Mon Sep 17 00:00:00 2001 From: Monica Khoury <99693443+monicakh@users.noreply.github.com> Date: Mon, 11 Nov 2024 09:24:46 +0200 Subject: [PATCH 22/86] Fix: unify Client ID handling for Google provider (#29950) * Fix: unify Client ID handling for Google provider * Ensure no spaces in Client IDs * Update apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx Co-authored-by: Kang Ming * Fix Apple provider client IDs * Update apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx Co-authored-by: Kang Ming * Update apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx Co-authored-by: Kang Ming * Remove Secret key requirement * Fix markdown component usage + apple client id --------- Co-authored-by: Kang Ming Co-authored-by: Joshen Lim --- .../Auth/AuthProvidersForm/FormField.tsx | 22 ++- .../Auth/AuthProvidersForm/ProviderForm.tsx | 8 +- .../Auth/AuthProvidersFormValidation.tsx | 145 +++++------------- 3 files changed, 51 insertions(+), 124 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/FormField.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersForm/FormField.tsx index e474cea8e5e..11fdbf2264f 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersForm/FormField.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersForm/FormField.tsx @@ -3,6 +3,7 @@ import { Eye, EyeOff } from 'lucide-react' import { useEffect, useState } from 'react' import ReactMarkdown from 'react-markdown' +import { Markdown } from 'components/interfaces/Markdown' import { DatePicker } from 'components/ui/DatePicker' import { BASE_PATH } from 'lib/constants' import { Button, Input, InputNumber, Listbox, Toggle } from 'ui' @@ -109,16 +110,15 @@ const FormField = ({ label={properties.title} labelOptional={ properties.descriptionOptional ? ( - - {properties.descriptionOptional} - + ) : null } descriptionText={ properties.description ? ( - - {properties.description} - + ) : null } actions={ @@ -140,6 +140,7 @@ const FormField = ({ } /> ) + case 'multiline-string': return ( - {properties.description} - + ) : null } actions={ @@ -183,6 +182,7 @@ const FormField = ({ } /> ) + case 'number': return ( - {properties.description} - + ) : null } actions={ diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx index 1718070d3a5..dd66b17e3be 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx @@ -28,6 +28,7 @@ import { import { ProviderCollapsibleClasses } from './AuthProvidersForm.constants' import type { Provider } from './AuthProvidersForm.types' import FormField from './FormField' +import { Markdown } from 'components/interfaces/Markdown' export interface ProviderFormProps { config: components['schemas']['GoTrueConfigResponse'] @@ -281,9 +282,10 @@ const ProviderForm = ({ config, provider }: ProviderFormProps) => { : `${apiUrl}/auth/v1/callback` } descriptionText={ - - {provider.misc.helper} - + } /> diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx index f804a333cc3..343753f8eb8 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx @@ -399,23 +399,16 @@ const EXTERNAL_PROVIDER_APPLE = { type: 'boolean', }, EXTERNAL_APPLE_CLIENT_ID: { - title: 'Service ID (for OAuth)', - description: `Client identifier used in the OAuth flow on the web. -[Learn more](https://developer.apple.com/documentation/sign_in_with_apple/configuring_your_environment_for_sign_in_with_apple)`, + title: 'Client IDs', + description: `Comma separated list of allowed Apple app ((Web, OAuth, iOS, macOS, watchOS, or tvOS) bundle IDs for native sign in, or service IDs for Sign in with Apple JS. [Learn more](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js)`, type: 'string', }, EXTERNAL_APPLE_SECRET: { title: 'Secret Key (for OAuth)', - description: `Secret key used in the OAuth flow. -[Learn more](https://supabase.com/docs/guides/auth/social-login/auth-apple#generate-a-client_secret)`, + description: `Secret key used in the OAuth flow. [Learn more](https://supabase.com/docs/guides/auth/social-login/auth-apple#generate-a-client_secret)`, type: 'string', isSecret: true, }, - EXTERNAL_APPLE_ADDITIONAL_CLIENT_IDS: { - title: 'Authorized Client IDs (iOS, macOS, watchOS, tvOS bundle IDs or service IDs)', - description: `Comma separated list of allowed Apple app bundle IDs for native sign in, or service IDs for Sign in with Apple JS. [Learn more](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js)`, - type: 'string', - }, }, validationSchema: object().shape({ EXTERNAL_APPLE_ENABLED: boolean().required(), @@ -426,7 +419,6 @@ const EXTERNAL_PROVIDER_APPLE = { }, then: (schema) => schema - .required('Secret key is required when using the OAuth flow.') .matches(/^[a-z0-9_-]+([.][a-z0-9_-]+){2}$/i, 'Secret key should be a JWT.') .test({ message: 'Secret key is not a correctly generated JWT.', @@ -473,49 +465,27 @@ const EXTERNAL_PROVIDER_APPLE = { }, }), }) - .when( - [ - 'EXTERNAL_APPLE_ENABLED', - 'EXTERNAL_APPLE_ADDITIONAL_CLIENT_IDS', - 'EXTERNAL_APPLE_CLIENT_ID', - ], - { - is: ( - EXTERNAL_APPLE_ENABLED: boolean, - EXTERNAL_APPLE_ADDITIONAL_CLIENT_IDS: string, - EXTERNAL_APPLE_CLIENT_ID: string - ) => { - return ( - EXTERNAL_APPLE_ENABLED && - !!EXTERNAL_APPLE_ADDITIONAL_CLIENT_IDS && - !EXTERNAL_APPLE_CLIENT_ID - ) - }, - then: (schema) => - schema.matches( - /^$/, - 'Secret Key should only be set if Service ID for OAuth is provided.' - ), - } - ), - EXTERNAL_APPLE_CLIENT_ID: string().matches( - /^[a-z0-9.-]+$/i, - 'Invalid characters. Apple recommends a reverse-domain name style string (e.g. com.example.app).' - ), - EXTERNAL_APPLE_ADDITIONAL_CLIENT_IDS: string() - .matches( - /^([.a-z0-9-]+(,\s*[.a-z0-9-]+)*,*\s*)?$/i, - 'Invalid characters. Apple recommends a reverse-domain name style string (e.g. com.example.app). You must only use explicit bundle IDs, asterisks (*) are not allowed.' - ) .when(['EXTERNAL_APPLE_ENABLED', 'EXTERNAL_APPLE_CLIENT_ID'], { is: (EXTERNAL_APPLE_ENABLED: boolean, EXTERNAL_APPLE_CLIENT_ID: string) => { return EXTERNAL_APPLE_ENABLED && !EXTERNAL_APPLE_CLIENT_ID }, then: (schema) => - schema.required( - 'At least one Authorized Client ID is required when not using the OAuth flow.' + schema.matches( + /^$/, + 'Secret Key should only be set if Service ID for OAuth is provided.' ), }), + EXTERNAL_APPLE_CLIENT_ID: string() + .matches(/^\S+$/, 'Client IDs should not contain spaces.') + .matches( + /^([a-z0-9-]+\.[a-z0-9-]+(\.[a-z0-9-]+)*(,[a-z0-9-]+\.[a-z0-9-]+(\.[a-z0-9-]+)*)*)$/i, + 'Invalid characters. Each ID should follow a reverse-domain style string (e.g. com.example.app). Use commas to separate multiple IDs.' + ) + .when('EXTERNAL_APPLE_ENABLED', { + is: true, + then: (schema) => + schema.required('At least one Client ID is required when Apple sign-in is enabled.'), + }), }), misc: { iconKey: 'apple-icon', @@ -833,8 +803,9 @@ const EXTERNAL_PROVIDER_GOOGLE = { type: 'boolean', }, EXTERNAL_GOOGLE_CLIENT_ID: { - title: 'Client ID (for OAuth)', - description: 'Client ID to use with the OAuth flow on the web.', + title: 'Client IDs', + description: + 'Comma-separated list of client IDs for Web, OAuth, Android apps, One Tap, and Chrome extensions.', type: 'string', }, EXTERNAL_GOOGLE_SECRET: { @@ -843,78 +814,34 @@ const EXTERNAL_PROVIDER_GOOGLE = { type: 'string', isSecret: true, }, - EXTERNAL_GOOGLE_ADDITIONAL_CLIENT_IDS: { - title: 'Authorized Client IDs (for Android, One Tap, and Chrome extensions)', - description: - 'Comma separated list of client IDs of Android apps, One Tap or Chrome extensions that are allowed to log in to your project.', - type: 'string', - }, EXTERNAL_GOOGLE_SKIP_NONCE_CHECK: { title: 'Skip nonce checks', description: - "Allows ID tokens with any nonce to be accepted, which is less secure. Useful in situations where you don't have access to the nonce used to issue the ID token, such with iOS.", + "Allows ID tokens with any nonce to be accepted, which is less secure. Useful in situations where you don't have access to the nonce used to issue the ID token, such as with iOS.", type: 'boolean', }, }, validationSchema: object().shape({ EXTERNAL_GOOGLE_ENABLED: boolean().required(), - EXTERNAL_GOOGLE_SECRET: string() - .when(['EXTERNAL_GOOGLE_ENABLED', 'EXTERNAL_GOOGLE_CLIENT_ID'], { - is: (EXTERNAL_GOOGLE_ENABLED: boolean, EXTERNAL_GOOGLE_CLIENT_ID: string) => { - return EXTERNAL_GOOGLE_ENABLED && !!EXTERNAL_GOOGLE_CLIENT_ID - }, - then: (schema) => - schema - .matches( - /^[a-z0-9.\/_-]*$/i, - 'Invalid characters. Google OAuth Client Secrets usually contain letters, numbers, dots, dashes and underscores.' - ) - .required('Client Secret is required when using the OAuth flow.'), - }) - .when( - [ - 'EXTERNAL_GOOGLE_ENABLED', - 'EXTERNAL_GOOGLE_ADDITIONAL_CLIENT_IDS', - 'EXTERNAL_GOOGLE_CLIENT_ID', - ], - { - is: ( - EXTERNAL_GOOGLE_ENABLED: boolean, - EXTERNAL_GOOGLE_ADDITIONAL_CLIENT_IDS: string, - EXTERNAL_GOOGLE_CLIENT_ID: string - ) => { - return ( - EXTERNAL_GOOGLE_ENABLED && - !!EXTERNAL_GOOGLE_ADDITIONAL_CLIENT_IDS && - !EXTERNAL_GOOGLE_CLIENT_ID - ) - }, - then: (schema) => - schema.matches( - /^$/, - 'Client Secret should only be set when Client ID for OAuth is set.' - ), - } - ), - EXTERNAL_GOOGLE_CLIENT_ID: string().matches( - /^([a-z0-9-]+([.][a-z0-9-]+)+)?$/i, - 'Invalid characters. Google OAuth Client IDs are usually a domain-name (e.g. 01234567890-abcdefghijklmnopqrstuvwxyz012345.apps.googleusercontent.com).' - ), - EXTERNAL_GOOGLE_ADDITIONAL_CLIENT_IDS: string() + EXTERNAL_GOOGLE_CLIENT_ID: string() + .matches(/^\S+$/, 'Client IDs should not contain spaces.') .matches( - /^([a-z0-9-]+([.][a-z0-9-]+)*(,\s*[a-z0-9-]+([.][a-z0-9-]+)*)*,*\s*)?$/i, - 'Invalid characters. Google Client IDs are usually a domain-name style string (e.g. com.example.com.app or *.apps.googleusercontent.com).' + /^([a-z0-9-]+\.[a-z0-9-]+(\.[a-z0-9-]+)*(,[a-z0-9-]+\.[a-z0-9-]+(\.[a-z0-9-]+)*)*)$/i, + 'Invalid characters. Google Client IDs should be a comma-separated list of domain-like strings.' ) - - .when(['EXTERNAL_GOOGLE_ENABLED', 'EXTERNAL_GOOGLE_CLIENT_ID'], { - is: (EXTERNAL_GOOGLE_ENABLED: boolean, EXTERNAL_GOOGLE_CLIENT_ID: string) => { - return EXTERNAL_GOOGLE_ENABLED && !EXTERNAL_GOOGLE_CLIENT_ID - }, + .when('EXTERNAL_GOOGLE_ENABLED', { + is: true, then: (schema) => - schema.required( - 'At least one Authorized Client ID is required when not using the OAuth flow.' - ), + schema.required('At least one Client ID is required when Google sign-in is enabled.'), }), + EXTERNAL_GOOGLE_SECRET: string().when('EXTERNAL_GOOGLE_ENABLED', { + is: true, + then: (schema) => + schema.matches( + /^[a-z0-9.\/_-]*$/i, + 'Invalid characters. Google OAuth Client Secrets usually contain letters, numbers, dots, dashes, and underscores.' + ), + }), EXTERNAL_GOOGLE_SKIP_NONCE_CHECK: boolean().required(), }), misc: { From caeb09b369f7419d2b16783c90608827db3b57dc Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 11 Nov 2024 16:37:49 +0800 Subject: [PATCH 23/86] Add new regions for RR deployment (#30409) --- .../interfaces/Auth/AuthProvidersFormValidation.tsx | 2 +- .../InstanceConfiguration.constants.ts | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx index 343753f8eb8..dcb72820e2a 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx @@ -400,7 +400,7 @@ const EXTERNAL_PROVIDER_APPLE = { }, EXTERNAL_APPLE_CLIENT_ID: { title: 'Client IDs', - description: `Comma separated list of allowed Apple app ((Web, OAuth, iOS, macOS, watchOS, or tvOS) bundle IDs for native sign in, or service IDs for Sign in with Apple JS. [Learn more](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js)`, + description: `Comma separated list of allowed Apple app (Web, OAuth, iOS, macOS, watchOS, or tvOS) bundle IDs for native sign in, or service IDs for Sign in with Apple JS. [Learn more](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js)`, type: 'string', }, EXTERNAL_APPLE_SECRET: { diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts index 40dc9aaa882..a15daadee5d 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants.ts @@ -40,11 +40,10 @@ export const AWS_REGIONS_COORDINATES: { [key: string]: [number, number] } = { SOUTH_ASIA: [72.88, 19.08], OCEANIA: [151.2, -33.86], SOUTH_AMERICA: [-46.38, -23.34], - // [Joshen] These regions are not supported yet, just need to uncomment them once ready - // CENTRAL_EU_2: [8.54, 47.45], - // EAST_US_2: [-83, 39.96], - // NORTH_EU: [17.91, 59.65], - // WEST_EU_3: [2.35, 48.86], + CENTRAL_EU_2: [8.54, 47.45], + EAST_US_2: [-83, 39.96], + NORTH_EU: [17.91, 59.65], + WEST_EU_3: [2.35, 48.86], } export const FLY_REGIONS_COORDINATES: { [key: string]: [number, number] } = { From b842621f3a24b18bae873df5da0643954ae14420 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Mon, 11 Nov 2024 19:46:35 +0800 Subject: [PATCH 24/86] docs: update oauth guides (#30411) --- .../guides/auth/social-login/auth-apple.mdx | 10 +++++----- .../guides/auth/social-login/auth-google.mdx | 14 +++++++------- apps/docs/spec/supabase_dart_v2.yml | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/docs/content/guides/auth/social-login/auth-apple.mdx b/apps/docs/content/guides/auth/social-login/auth-apple.mdx index df5ba7c745b..5463db69e7d 100644 --- a/apps/docs/content/guides/auth/social-login/auth-apple.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-apple.mdx @@ -111,7 +111,7 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app, 1. Have an **App ID** which uniquely identifies the app you are building. You can create a new App ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/bundleId) section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example `com.example.app`. Make sure you configure Sign in with Apple for the App ID you created or already have, in the Capabilities list. At this time Supabase Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as _bundle IDs._) 2. Obtain a **Services ID** attached to the App ID that uniquely identifies the website. Use this value as the client ID when initializing Sign in with Apple JS. You can create a new Services ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/serviceId) section in the Apple Developer Console (use the filter menu in the upper right side to see all Services IDs). These usually are a reverse domain name string, for example `com.example.app.website`. 3. Configure Website URLs for the newly created **Services ID**. The web domain you should use is the domain your website is hosted on. The redirect URL must also point to a page on your website that will receive the callback from Apple. - 4. Register the Services ID you created to your project's [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers) under _Authorized Client IDs_. + 4. Register the Services ID you created to your project's [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers) under _Client IDs_. If you're using Sign in with Apple JS you do not need to configure the OAuth settings. @@ -184,12 +184,12 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app, - When testing with Expo Go, the Expo App ID `host.exp.Exponent` will be used. Make sure to add this to the "Authorized Client IDs" list in your [Supabase dashboard Apple provider configuration](https://supabase.com/dashboard/project/_/auth/providers)! + When testing with Expo Go, the Expo App ID `host.exp.Exponent` will be used. Make sure to add this to the "Client IDs" list in your [Supabase dashboard Apple provider configuration](https://supabase.com/dashboard/project/_/auth/providers)! 1. Have an **App ID** which uniquely identifies the app you are building. You can create a new App ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/bundleId) section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example `com.example.app`. Make sure you configure Sign in with Apple for the App ID you created or already have, in the Capabilities list. At this time Supabase Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as _bundle IDs._) - 2. Register all of the App IDs that will be using your Supabase project in the [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers) under _Authorized Client IDs_. + 2. Register all of the App IDs that will be using your Supabase project in the [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers) under _Client IDs_. @@ -243,7 +243,7 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app, ### Configuration [#flutter-configuration-native-app] 1. Have an **App ID** which uniquely identifies the app you are building. You can create a new App ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/bundleId) section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example `com.example.app`. Make sure you configure Sign in with Apple for the App ID you created or already have, in the Capabilities list. At this time Supabase Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as _bundle IDs._) - 2. Register all of the App IDs that will be using your Supabase project in the [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers) under _Authorized Client IDs_. + 2. Register all of the App IDs that will be using your Supabase project in the [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers) under _Client IDs_. ## Apple sign in on Android, Web, Windows and Linux @@ -339,7 +339,7 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app, ### Configuration [#swift-configuration-native-app] 1. Have an **App ID** which uniquely identifies the app you are building. You can create a new App ID from the [Identifiers](https://developer.apple.com/account/resources/identifiers/list/bundleId) section in the Apple Developer Console (use the filter menu in the upper right side to see all App IDs). These usually are a reverse domain name string, for example `com.example.app`. Make sure you configure Sign in with Apple for the App ID you created or already have, in the Capabilities list. At this time Supabase Auth does not support Server-to-Server notification endpoints, so you should leave that setting blank. (In the past App IDs were referred to as _bundle IDs._) - 2. Register all of the App IDs that will be using your Supabase project in the [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers) under _Authorized Client IDs_. + 2. Register all of the App IDs that will be using your Supabase project in the [Apple provider configuration in the Supabase dashboard](https://supabase.com/dashboard/project/_/auth/providers) under _Client IDs_. diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx index d7500402e8b..260a32c341e 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -69,7 +69,7 @@ To use Google's pre-built signin buttons: 1. Click `Create credentials` and choose `OAuth Client ID`. 1. For application type, choose `Web application`. 1. Under **Authorized JavaScript origins** and **Authorized redirect URLs**, add your site URL. This is the URL of the website where the signin button will appear, _not_ your Supabase project domain. If you're testing in localhost, ensure that you have `http://localhost` set in the **Authorized JavaScript origins** section as well. This is important when integrating with Google One-Tap to ensure you can use it locally. -1. When you finish configuring your credentials, you will be shown your client ID. Add this to the **Authorized Client IDs** field in the [Google Auth Provider section of the Supabase Dashboard](/dashboard/project/_/auth/providers). Leave the OAuth client ID and secret blank. You don't need them when using Google's pre-built approach. +1. When you finish configuring your credentials, you will be shown your client ID. Add this to the **Client IDs** field in the [Google Auth Provider section of the Supabase Dashboard](/dashboard/project/_/auth/providers). Leave the OAuth client ID and secret blank. You don't need them when using Google's pre-built approach. @@ -80,7 +80,7 @@ To use Google's pre-built signin buttons: - You will have a different set of SHA-1 certificate fingerprint for testing locally and going to production. Make sure to add both to the Google Cloud Console. and add all of the Client IDs to Supabase dashboard. - For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple AppStore. 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service. - 3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Authorized Client IDs_. + 3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Client IDs_. Note that you do not have to configure the OAuth flow in the Supabase Dashboard in order to use native sign in. @@ -96,7 +96,7 @@ To use Google's pre-built signin buttons: - You will have a different set of SHA-1 certificate fingerprint for testing locally and going to production. Make sure to add both to the Google Cloud Console. and add all of the Client IDs to Supabase dashboard. - For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple AppStore. 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service. - 3. Add only the web client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Authorized Client IDs_. If you need iOS support, enable the `Skip nonce check` option. + 3. Add only the web client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Client IDs_. If you need iOS support, enable the `Skip nonce check` option. 4. For iOS, add the `CFBundleURLTypes` attributes below into the `/ios/Runner/Info.plist` file. ```xml @@ -168,7 +168,7 @@ To use Google's pre-built signin buttons: 1. Follow the integration instructions on the [get started with Google Sign-In](https://developers.google.com/identity/sign-in/ios/start-integrating) for iOS guide. 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service. - 3. Add only the web client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Authorized Client IDs_. Enable the `Skip nonce check` option. + 3. Add only the web client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Client IDs_. Enable the `Skip nonce check` option. @@ -178,7 +178,7 @@ To use Google's pre-built signin buttons: 1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. You need two client IDs, a web client ID and Android client ID. When creating a new Android OAuth client ID, use the instructions on screen to provide the SHA-1 certificate fingerprint used to sign your Android app. 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service. - 3. Finally, add the web client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Authorized Client IDs_. + 3. Finally, add the web client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Client IDs_. Note that you do not have to configure the OAuth flow in the Supabase Dashboard in order to use native sign in. @@ -190,7 +190,7 @@ To use Google's pre-built signin buttons: - For iOS, use the instructions on screen to provide the app Bundle ID, and App Store ID and Team ID if the app is already published on the Apple AppStore. 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. In particular, make sure you have set up links to your app's privacy policy and terms of service. - 3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Authorized Client IDs_. + 3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Client IDs_. Note that you do not have to configure the OAuth flow in the Supabase Dashboard in order to use native sign in. @@ -202,7 +202,7 @@ To use Google's pre-built signin buttons: 1. Configure OAuth credentials for your Google Cloud project in the [Credentials](https://console.cloud.google.com/apis/credentials) page of the console. When creating a new OAuth client ID, choose _Chrome extension_ for the application type. For _Item ID_ provide the unique ID of your Chrome extension. You can get this by calling `chrome.runtime.id` within the extension, or from the Web Store URL of the extension. For example, the [Google Translate extension](https://chrome.google.com/webstore/detail/google-translate/aapbdbdomjkkjkaonfhkkikfgjllcleb) has the Web Store URL `https://chrome.google.com/webstore/detail/google-translate/aapbdbdomjkkjkaonfhkkikfgjllcleb` and the last part `aapbdbdomjkkjkaonfhkkikfgjllcleb` is its unique ID. 2. Configure the [OAuth Consent Screen](https://console.cloud.google.com/apis/credentials/consent). This information is shown to the user when giving consent to your app. - 3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Authorized Client IDs_. + 3. Finally, add the client ID from step 1 in the [Google provider on the Supabase Dashboard](https://supabase.com/dashboard/project/_/auth/providers), under _Client IDs_. Note that you do not have to configure the OAuth flow in the Supabase Dashboard to sign in with Google inside Chrome extensions. diff --git a/apps/docs/spec/supabase_dart_v2.yml b/apps/docs/spec/supabase_dart_v2.yml index 565c92b924d..adbcca2ddec 100644 --- a/apps/docs/spec/supabase_dart_v2.yml +++ b/apps/docs/spec/supabase_dart_v2.yml @@ -594,7 +594,7 @@ functions: - [Steps to obtain Android client ID](https://developers.google.com/identity/sign-in/android/start-integrating#configure_a_project) - [Steps to obtain iOS client ID](https://developers.google.com/identity/sign-in/ios/start-integrating#get_an_oauth_client_id) - Add the web client ID to the `Authentication -> Providers -> Google -> Authorized Client IDs` section in your Supabase dashboard + Add the web client ID to the `Authentication -> Providers -> Google -> Client IDs` section in your Supabase dashboard code: | ```dart import 'package:google_sign_in/google_sign_in.dart'; From 936fe4f5a19edb2c3cf7aa1f08a2d3a46c31b603 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:10:47 +0000 Subject: [PATCH 25/86] feat: update mgmt api docs (#30405) Co-authored-by: phamhieu <689843+phamhieu@users.noreply.github.com> --- apps/docs/spec/api_v1_openapi.json | 3 +++ .../transforms/api_v1_openapi_deparsed.json | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/apps/docs/spec/api_v1_openapi.json b/apps/docs/spec/api_v1_openapi.json index f7387e4701c..75df6edc0d6 100644 --- a/apps/docs/spec/api_v1_openapi.json +++ b/apps/docs/spec/api_v1_openapi.json @@ -1540,6 +1540,7 @@ } } }, + "403": { "description": "" }, "500": { "description": "Failed to retrieve project's service health status" } }, "tags": ["Projects"], @@ -3461,6 +3462,7 @@ "max_worker_processes": { "type": "integer", "minimum": 0, "maximum": 262143 }, "shared_buffers": { "type": "string" }, "statement_timeout": { "type": "string" }, + "track_commit_timestamp": { "type": "boolean" }, "wal_keep_size": { "type": "string" }, "wal_sender_timeout": { "type": "string" }, "work_mem": { "type": "string" }, @@ -3487,6 +3489,7 @@ "max_worker_processes": { "type": "integer", "minimum": 0, "maximum": 262143 }, "shared_buffers": { "type": "string" }, "statement_timeout": { "type": "string" }, + "track_commit_timestamp": { "type": "boolean" }, "wal_keep_size": { "type": "string" }, "wal_sender_timeout": { "type": "string" }, "work_mem": { "type": "string" }, diff --git a/apps/docs/spec/transforms/api_v1_openapi_deparsed.json b/apps/docs/spec/transforms/api_v1_openapi_deparsed.json index 3d07a743f9d..692ac2af2b2 100644 --- a/apps/docs/spec/transforms/api_v1_openapi_deparsed.json +++ b/apps/docs/spec/transforms/api_v1_openapi_deparsed.json @@ -4488,6 +4488,9 @@ } } }, + "403": { + "description": "" + }, "500": { "description": "Failed to retrieve project's service health status" } @@ -4720,6 +4723,9 @@ "statement_timeout": { "type": "string" }, + "track_commit_timestamp": { + "type": "boolean" + }, "wal_keep_size": { "type": "string" }, @@ -4835,6 +4841,9 @@ "statement_timeout": { "type": "string" }, + "track_commit_timestamp": { + "type": "boolean" + }, "wal_keep_size": { "type": "string" }, @@ -4927,6 +4936,9 @@ "statement_timeout": { "type": "string" }, + "track_commit_timestamp": { + "type": "boolean" + }, "wal_keep_size": { "type": "string" }, @@ -11882,6 +11894,9 @@ "statement_timeout": { "type": "string" }, + "track_commit_timestamp": { + "type": "boolean" + }, "wal_keep_size": { "type": "string" }, @@ -11963,6 +11978,9 @@ "statement_timeout": { "type": "string" }, + "track_commit_timestamp": { + "type": "boolean" + }, "wal_keep_size": { "type": "string" }, From f072beb97dc323a354f3ff6f21d15723ee0d1bc3 Mon Sep 17 00:00:00 2001 From: Rizky Faturriza <56538058+rfaturriza@users.noreply.github.com> Date: Tue, 12 Nov 2024 01:17:21 +0700 Subject: [PATCH 26/86] chore: Fix typo _req at connect-to-postgres.mdx (#30389) --- apps/docs/content/guides/functions/connect-to-postgres.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/functions/connect-to-postgres.mdx b/apps/docs/content/guides/functions/connect-to-postgres.mdx index 12478874d5c..22b0ada259b 100644 --- a/apps/docs/content/guides/functions/connect-to-postgres.mdx +++ b/apps/docs/content/guides/functions/connect-to-postgres.mdx @@ -16,7 +16,7 @@ The `supabase-js` client is a great option for connecting to your Supabase datab ```ts index.ts import { createClient } from 'jsr:@supabase/supabase-js@2' -Deno.serve(async (_req) => { +Deno.serve(async (req) => { try { const supabase = createClient( Deno.env.get('SUPABASE_URL') ?? '', From 3f08a033d9b7641d7edbe65f253f1baa6f481e0b Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:18:42 -0500 Subject: [PATCH 27/86] fix: autofix action (#30421) Autofix action is failing with error message: ``` fatal: You are not currently on a branch. To push the history leading to the current (detached HEAD) state now, use git push origin HEAD: ``` This is because the checkout action on a PR by default checks out the merge commit, which is not a branch. Instead, check out [`github.head_ref`](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#github-context), which will point to the source of the PR. --------- Co-authored-by: github-tidy-bot --- .github/workflows/autofix_linters.yml | 4 ++-- apps/docs/app/api/ai/docs/route.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/autofix_linters.yml b/.github/workflows/autofix_linters.yml index fa02d5c9f8d..d181e750d80 100644 --- a/.github/workflows/autofix_linters.yml +++ b/.github/workflows/autofix_linters.yml @@ -18,10 +18,10 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event_name == 'pull_request' && (github.event.label.name == 'autofix') }} steps: - - uses: actions/checkout@v4 with: sparse-checkout: apps + ref: ${{ github.head_ref }} - uses: actions/setup-node@v4 with: @@ -39,7 +39,7 @@ jobs: git config --global user.email 'github-tidy-bot@supabase.com' if [[ `git status --porcelain` ]]; then echo "[bot] Changes detected, committing." - + echo "[bot] Running in non-squash mode." git commit -am "ci: Autofix updates from GitHub workflow" git push diff --git a/apps/docs/app/api/ai/docs/route.ts b/apps/docs/app/api/ai/docs/route.ts index 3b1df3ee2a7..b88d7837f82 100644 --- a/apps/docs/app/api/ai/docs/route.ts +++ b/apps/docs/app/api/ai/docs/route.ts @@ -6,7 +6,7 @@ import OpenAI from 'openai' export const runtime = 'edge' /* To avoid OpenAI errors, restrict to the Vercel Edge Function regions that overlap with the OpenAI API regions. - + Reference for Vercel regions: https://vercel.com/docs/edge-network/regions#region-list Reference for OpenAI regions: https://help.openai.com/en/articles/5347006-openai-api-supported-countries-and-territories */ From e40dbbbfb09a08cac722f73d7f0834ccfc3fd1d5 Mon Sep 17 00:00:00 2001 From: Bo Lu Date: Tue, 12 Nov 2024 09:52:43 +1100 Subject: [PATCH 28/86] fix: rss feed datetime format for RFC822 (#30289) * fix: rss feed datetime format for RFC822 * gen rss --------- Co-authored-by: Francesco Sansalvadore --- apps/www/lib/rss.tsx | 4 +- .../planetpg-angelico_de_los_reyes-rss.xml | 10 +- apps/www/public/planetpg-ant_wilson-rss.xml | 4 +- apps/www/public/planetpg-bo_lu-rss.xml | 4 +- apps/www/public/planetpg-burggraf-rss.xml | 6 +- apps/www/public/planetpg-egor_romanov-rss.xml | 6 +- apps/www/public/planetpg-filipe-rss.xml | 4 +- apps/www/public/planetpg-gregnr-rss.xml | 8 +- apps/www/public/planetpg-michel-rss.xml | 4 +- apps/www/public/planetpg-oli_rice-rss.xml | 18 +- .../public/planetpg-paul_copplestone-rss.xml | 12 +- apps/www/public/planetpg-pavel-rss.xml | 12 +- apps/www/public/planetpg-philipp-rss.xml | 4 +- .../public/planetpg-raminder_singh-rss.xml | 6 +- apps/www/public/planetpg-stas-rss.xml | 4 +- apps/www/public/planetpg-steve_chavez-rss.xml | 6 +- apps/www/public/planetpg-victor-rss.xml | 8 +- apps/www/public/rss.xml | 611 +++++++++--------- 18 files changed, 369 insertions(+), 362 deletions(-) diff --git a/apps/www/lib/rss.tsx b/apps/www/lib/rss.tsx index 63231355d1a..75305b14abc 100644 --- a/apps/www/lib/rss.tsx +++ b/apps/www/lib/rss.tsx @@ -25,7 +25,7 @@ const generateRssItem = (post: any): string => { const formattedDate = dayjs(post.date) .utcOffset(0, true) .startOf('day') - .format('ddd, DD MMM YYYY HH:mm:ss [-7 GMT]') + .format('ddd, DD MMM YYYY HH:mm:ss [-0700]') return ` https://supabase.com${encodedPath} @@ -45,7 +45,7 @@ export const generateRss = (posts: any[], authorID?: string): string => { const formattedDate = dayjs(posts[0].date) .utcOffset(0, true) .startOf('day') - .format('ddd, DD MMM YYYY HH:mm:ss [-7 GMT]') + .format('ddd, DD MMM YYYY HH:mm:ss [-0700]') if (authorID) { return ` diff --git a/apps/www/public/planetpg-angelico_de_los_reyes-rss.xml b/apps/www/public/planetpg-angelico_de_los_reyes-rss.xml index 5203aaf7474..e88eb92e451 100644 --- a/apps/www/public/planetpg-angelico_de_los_reyes-rss.xml +++ b/apps/www/public/planetpg-angelico_de_los_reyes-rss.xml @@ -5,35 +5,35 @@ https://supabase.com/blog Latest Postgres news from Angelico de los Reyes at Supabase en - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/postgres-point-in-time-recovery Point in Time Recovery is now available for Pro projects https://supabase.com/blog/postgres-point-in-time-recovery We're making PITR available for more projects, with a new Dashboard UI that makes it simple to use. - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/continuous-postgresql-backup-walg Continuous PostgreSQL Backups using WAL-G https://supabase.com/blog/continuous-postgresql-backup-walg Have you ever wanted to restore your database's state to a particular moment in time? This post explains how, using WAL-G. - Sun, 02 Aug 2020 00:00:00 GMT + Sun, 02 Aug 2020 00:00:00 -0700 https://supabase.com/blog/postgresql-templates What are PostgreSQL Templates? https://supabase.com/blog/postgresql-templates What are PostgreSQL templates and what are they used for? - Thu, 09 Jul 2020 00:00:00 GMT + Thu, 09 Jul 2020 00:00:00 -0700 https://supabase.com/blog/postgresql-physical-logical-backups Physical vs Logical Backups in PostgreSQL https://supabase.com/blog/postgresql-physical-logical-backups What are physical and logical backups in Postgres? - Tue, 07 Jul 2020 00:00:00 GMT + Tue, 07 Jul 2020 00:00:00 -0700 diff --git a/apps/www/public/planetpg-ant_wilson-rss.xml b/apps/www/public/planetpg-ant_wilson-rss.xml index ac377af0323..fb9703e4fd7 100644 --- a/apps/www/public/planetpg-ant_wilson-rss.xml +++ b/apps/www/public/planetpg-ant_wilson-rss.xml @@ -5,14 +5,14 @@ https://supabase.com/blog Latest Postgres news from Ant Wilson at Supabase en - Sat, 27 Feb 2021 00:00:00 GMT + Sat, 27 Feb 2021 00:00:00 -0700 https://supabase.com/blog/cracking-postgres-interview Cracking PostgreSQL Interview Questions https://supabase.com/blog/cracking-postgres-interview Understand the top PostgreSQL Interview Questions - Sat, 27 Feb 2021 00:00:00 GMT + Sat, 27 Feb 2021 00:00:00 -0700 diff --git a/apps/www/public/planetpg-bo_lu-rss.xml b/apps/www/public/planetpg-bo_lu-rss.xml index b1830dc3b2b..23af34717a9 100644 --- a/apps/www/public/planetpg-bo_lu-rss.xml +++ b/apps/www/public/planetpg-bo_lu-rss.xml @@ -5,14 +5,14 @@ https://supabase.com/blog Latest Postgres news from Bo Lu at Supabase en - Thu, 14 Dec 2023 00:00:00 GMT + Thu, 14 Dec 2023 00:00:00 -0700 https://supabase.com/blog/supabase-wrappers-v02 Supabase Wrappers v0.2: Query Pushdown & Remote Subqueries https://supabase.com/blog/supabase-wrappers-v02 Supabase Wrappers v0.2 brings more Wrappers, query pushdown, remote subquery, and more - Thu, 14 Dec 2023 00:00:00 GMT + Thu, 14 Dec 2023 00:00:00 -0700 diff --git a/apps/www/public/planetpg-burggraf-rss.xml b/apps/www/public/planetpg-burggraf-rss.xml index 42d1268f6ce..988a7979ca6 100644 --- a/apps/www/public/planetpg-burggraf-rss.xml +++ b/apps/www/public/planetpg-burggraf-rss.xml @@ -5,21 +5,21 @@ https://supabase.com/blog Latest Postgres news from Mark Burggraf at Supabase en - Thu, 24 Nov 2022 00:00:00 GMT + Thu, 24 Nov 2022 00:00:00 -0700 https://supabase.com/blog/sql-or-nosql-both-with-postgresql SQL or NoSQL? Why not use both (with PostgreSQL)? https://supabase.com/blog/sql-or-nosql-both-with-postgresql How to turn Postgres into an easy-to-use NoSQL database that retains all the power of SQL - Thu, 24 Nov 2022 00:00:00 GMT + Thu, 24 Nov 2022 00:00:00 -0700 https://supabase.com/blog/postgres-wasm Postgres WASM by Snaplet and Supabase https://supabase.com/blog/postgres-wasm We're open sourcing postgres-wasm, a PostgresQL server that runs inside a browser, with our friends at Snaplet. - Mon, 03 Oct 2022 00:00:00 GMT + Mon, 03 Oct 2022 00:00:00 -0700 diff --git a/apps/www/public/planetpg-egor_romanov-rss.xml b/apps/www/public/planetpg-egor_romanov-rss.xml index 89e43a00460..d834d73e259 100644 --- a/apps/www/public/planetpg-egor_romanov-rss.xml +++ b/apps/www/public/planetpg-egor_romanov-rss.xml @@ -5,21 +5,21 @@ https://supabase.com/blog Latest Postgres news from Egor Romanov at Supabase en - Thu, 03 Aug 2023 00:00:00 GMT + Thu, 03 Aug 2023 00:00:00 -0700 https://supabase.com/blog/fewer-dimensions-are-better-pgvector pgvector: Fewer dimensions are better https://supabase.com/blog/fewer-dimensions-are-better-pgvector Increase performance in pgvector by using embedding vectors with fewer dimensions - Thu, 03 Aug 2023 00:00:00 GMT + Thu, 03 Aug 2023 00:00:00 -0700 https://supabase.com/blog/pgvector-performance pgvector 0.4.0 performance https://supabase.com/blog/pgvector-performance There's been a lot of talk about pgvector performance lately, so we took some datasets and pushed pgvector to the limits to find out its strengths and limitations. - Thu, 13 Jul 2023 00:00:00 GMT + Thu, 13 Jul 2023 00:00:00 -0700 diff --git a/apps/www/public/planetpg-filipe-rss.xml b/apps/www/public/planetpg-filipe-rss.xml index b5fc4b14c38..3d1b4a12410 100644 --- a/apps/www/public/planetpg-filipe-rss.xml +++ b/apps/www/public/planetpg-filipe-rss.xml @@ -5,14 +5,14 @@ https://supabase.com/blog Latest Postgres news from Filipe Cabaço at Supabase en - Tue, 09 Jan 2024 00:00:00 GMT + Tue, 09 Jan 2024 00:00:00 -0700 https://supabase.com/blog/elixir-clustering-using-postgres Elixir clustering using Postgres https://supabase.com/blog/elixir-clustering-using-postgres Learn about our approach to connecting multiple nodes in Elixir using Postgres - Tue, 09 Jan 2024 00:00:00 GMT + Tue, 09 Jan 2024 00:00:00 -0700 diff --git a/apps/www/public/planetpg-gregnr-rss.xml b/apps/www/public/planetpg-gregnr-rss.xml index 146d5b7b30a..06249facd04 100644 --- a/apps/www/public/planetpg-gregnr-rss.xml +++ b/apps/www/public/planetpg-gregnr-rss.xml @@ -5,28 +5,28 @@ https://supabase.com/blog Latest Postgres news from Greg Richardson at Supabase en - Wed, 06 Sep 2023 00:00:00 GMT + Wed, 06 Sep 2023 00:00:00 -0700 https://supabase.com/blog/increase-performance-pgvector-hnsw pgvector v0.5.0: Faster semantic search with HNSW indexes https://supabase.com/blog/increase-performance-pgvector-hnsw Increase performance in pgvector using HNSW indexes - Wed, 06 Sep 2023 00:00:00 GMT + Wed, 06 Sep 2023 00:00:00 -0700 https://supabase.com/blog/fewer-dimensions-are-better-pgvector pgvector: Fewer dimensions are better https://supabase.com/blog/fewer-dimensions-are-better-pgvector Increase performance in pgvector by using embedding vectors with fewer dimensions - Thu, 03 Aug 2023 00:00:00 GMT + Thu, 03 Aug 2023 00:00:00 -0700 https://supabase.com/blog/openai-embeddings-postgres-vector Storing OpenAI embeddings in Postgres with pgvector https://supabase.com/blog/openai-embeddings-postgres-vector An example of how to build an AI-powered search engine using OpenAI's embeddings and PostgreSQL. - Mon, 06 Feb 2023 00:00:00 GMT + Mon, 06 Feb 2023 00:00:00 -0700 diff --git a/apps/www/public/planetpg-michel-rss.xml b/apps/www/public/planetpg-michel-rss.xml index 892920165d7..c262278a76c 100644 --- a/apps/www/public/planetpg-michel-rss.xml +++ b/apps/www/public/planetpg-michel-rss.xml @@ -5,14 +5,14 @@ https://supabase.com/blog Latest Postgres news from Michel Pelletier at Supabase en - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/vault-now-in-beta Supabase Vault is now in Beta https://supabase.com/blog/vault-now-in-beta A Postgres extension to store encrypted secrets and encrypt data. - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 diff --git a/apps/www/public/planetpg-oli_rice-rss.xml b/apps/www/public/planetpg-oli_rice-rss.xml index e82c46d8274..94814a3ba35 100644 --- a/apps/www/public/planetpg-oli_rice-rss.xml +++ b/apps/www/public/planetpg-oli_rice-rss.xml @@ -5,63 +5,63 @@ https://supabase.com/blog Latest Postgres news from Oliver Rice at Supabase en - Wed, 13 Dec 2023 00:00:00 GMT + Wed, 13 Dec 2023 00:00:00 -0700 https://supabase.com/blog/postgrest-12 PostgREST 12 https://supabase.com/blog/postgrest-12 PostgREST 12 is out and we take a look at some of the major new features like JWT Caching and Aggregate Functions - Wed, 13 Dec 2023 00:00:00 GMT + Wed, 13 Dec 2023 00:00:00 -0700 https://supabase.com/blog/pg-graphql-postgres-functions pg_graphql: Postgres functions now supported https://supabase.com/blog/pg-graphql-postgres-functions pg_graphql now supports the most requested feature: Postgres functions a.k.a. User Defined Functions (UDFs) - Tue, 12 Dec 2023 00:00:00 GMT + Tue, 12 Dec 2023 00:00:00 -0700 https://supabase.com/blog/fewer-dimensions-are-better-pgvector pgvector: Fewer dimensions are better https://supabase.com/blog/fewer-dimensions-are-better-pgvector Increase performance in pgvector by using embedding vectors with fewer dimensions - Thu, 03 Aug 2023 00:00:00 GMT + Thu, 03 Aug 2023 00:00:00 -0700 https://supabase.com/blog/whats-new-in-pg-graphql-v1-2 What's New in pg_graphql v1.2 https://supabase.com/blog/whats-new-in-pg-graphql-v1-2 New Features in the v1.2 release of pg_graphql - Fri, 21 Apr 2023 00:00:00 GMT + Fri, 21 Apr 2023 00:00:00 -0700 https://supabase.com/blog/type-constraints-in-65-lines-of-sql Type Constraints in 65 lines of SQL https://supabase.com/blog/type-constraints-in-65-lines-of-sql Creating validated data types in Postgres - Fri, 17 Feb 2023 00:00:00 GMT + Fri, 17 Feb 2023 00:00:00 -0700 https://supabase.com/blog/pg-graphql-v1 pg_graphql v1.0 https://supabase.com/blog/pg-graphql-v1 Announcing the v1.0 release of pg_graphql - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/pg-jsonschema-a-postgres-extension-for-json-validation pg_jsonschema: JSON Schema support for Postgres https://supabase.com/blog/pg-jsonschema-a-postgres-extension-for-json-validation Today we're releasing pg_jsonschema, a Postgres extension for JSON validation. - Fri, 19 Aug 2022 00:00:00 GMT + Fri, 19 Aug 2022 00:00:00 -0700 https://supabase.com/blog/postgres-audit Postgres Auditing in 150 lines of SQL https://supabase.com/blog/postgres-audit PostgreSQL has a robust set of features which we can leverage to create a generic auditing solution in 150 lines of SQL. - Tue, 08 Mar 2022 00:00:00 GMT + Tue, 08 Mar 2022 00:00:00 -0700 diff --git a/apps/www/public/planetpg-paul_copplestone-rss.xml b/apps/www/public/planetpg-paul_copplestone-rss.xml index a4ce92c4a90..83d1c76ea10 100644 --- a/apps/www/public/planetpg-paul_copplestone-rss.xml +++ b/apps/www/public/planetpg-paul_copplestone-rss.xml @@ -5,42 +5,42 @@ https://supabase.com/blog Latest Postgres news from Paul Copplestone at Supabase en - Mon, 01 May 2023 00:00:00 GMT + Mon, 01 May 2023 00:00:00 -0700 https://supabase.com/blog/postgres-pluggable-strorage Next steps for Postgres pluggable storage https://supabase.com/blog/postgres-pluggable-strorage Exploring history of Postgres pluggable storage and the possibility of landing it in the Postgres core. - Mon, 01 May 2023 00:00:00 GMT + Mon, 01 May 2023 00:00:00 -0700 https://supabase.com/blog/postgres-crdt pg_crdt - an experimental CRDT extension for Postgres https://supabase.com/blog/postgres-crdt Embedding Yjs and Automerge into Postgres for collaborative applications. - Sat, 10 Dec 2022 00:00:00 GMT + Sat, 10 Dec 2022 00:00:00 -0700 https://supabase.com/blog/partial-postgresql-data-dumps-with-rls Partial data dumps using Postgres Row Level Security https://supabase.com/blog/partial-postgresql-data-dumps-with-rls Using RLS to create seed files for local PostgreSQL testing. - Tue, 28 Jun 2022 00:00:00 GMT + Tue, 28 Jun 2022 00:00:00 -0700 https://supabase.com/blog/postgres-as-a-cron-server Postgres as a CRON Server https://supabase.com/blog/postgres-as-a-cron-server Running repetitive tasks with your Postgres database. - Fri, 05 Mar 2021 00:00:00 GMT + Fri, 05 Mar 2021 00:00:00 -0700 https://supabase.com/blog/postgresql-views Postgres Views https://supabase.com/blog/postgresql-views Creating and using a view in PostgreSQL. - Wed, 18 Nov 2020 00:00:00 GMT + Wed, 18 Nov 2020 00:00:00 -0700 diff --git a/apps/www/public/planetpg-pavel-rss.xml b/apps/www/public/planetpg-pavel-rss.xml index 28c61c317e3..89e4bbad08e 100644 --- a/apps/www/public/planetpg-pavel-rss.xml +++ b/apps/www/public/planetpg-pavel-rss.xml @@ -5,42 +5,42 @@ https://supabase.com/blog Latest Postgres news from Pavel Borisov at Supabase en - Thu, 02 May 2024 00:00:00 GMT + Thu, 02 May 2024 00:00:00 -0700 https://supabase.com/blog/pgvector-0-7-0 What's new in pgvector v0.7.0 https://supabase.com/blog/pgvector-0-7-0 Exploring new features in pgvector v0.7.0 - Thu, 02 May 2024 00:00:00 GMT + Thu, 02 May 2024 00:00:00 -0700 https://supabase.com/blog/postgres-bloat Postgres Bloat Minimization https://supabase.com/blog/postgres-bloat Understanding and minimizing Postgres table bloat - Fri, 26 Apr 2024 00:00:00 GMT + Fri, 26 Apr 2024 00:00:00 -0700 https://supabase.com/blog/pgvector-performance pgvector 0.4.0 performance https://supabase.com/blog/pgvector-performance There's been a lot of talk about pgvector performance lately, so we took some datasets and pushed pgvector to the limits to find out its strengths and limitations. - Thu, 13 Jul 2023 00:00:00 GMT + Thu, 13 Jul 2023 00:00:00 -0700 https://supabase.com/blog/new-in-postgres-15 What's new in Postgres 15? https://supabase.com/blog/new-in-postgres-15 Describes the release of Postgres 15, new features and reasons to use it - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/postgresql-commitfest What is PostgreSQL commitfest and how to contribute https://supabase.com/blog/postgresql-commitfest A time-tested method for contributing to the core Postgres code - Thu, 27 Oct 2022 00:00:00 GMT + Thu, 27 Oct 2022 00:00:00 -0700 diff --git a/apps/www/public/planetpg-philipp-rss.xml b/apps/www/public/planetpg-philipp-rss.xml index 8eff743cc1c..ea73d940ac7 100644 --- a/apps/www/public/planetpg-philipp-rss.xml +++ b/apps/www/public/planetpg-philipp-rss.xml @@ -5,14 +5,14 @@ https://supabase.com/blog Latest Postgres news from Philipp Steinrötter at Supabase en - Fri, 08 Dec 2023 00:00:00 GMT + Fri, 08 Dec 2023 00:00:00 -0700 https://supabase.com/blog/postgres-language-server-implementing-parser Postgres Language Server: implementing the Parser https://supabase.com/blog/postgres-language-server-implementing-parser A detailed analysis of our iterations to implement a Parser for Postgres - Fri, 08 Dec 2023 00:00:00 GMT + Fri, 08 Dec 2023 00:00:00 -0700 diff --git a/apps/www/public/planetpg-raminder_singh-rss.xml b/apps/www/public/planetpg-raminder_singh-rss.xml index db535fe564e..5261c854590 100644 --- a/apps/www/public/planetpg-raminder_singh-rss.xml +++ b/apps/www/public/planetpg-raminder_singh-rss.xml @@ -5,21 +5,21 @@ https://supabase.com/blog Latest Postgres news from Raminder Singh at Supabase en - Thu, 11 Apr 2024 00:00:00 GMT + Thu, 11 Apr 2024 00:00:00 -0700 https://supabase.com/blog/postgres-roles-and-privileges Postgres Roles and Privileges https://supabase.com/blog/postgres-roles-and-privileges A guide to Postgres roles and privileges - Thu, 11 Apr 2024 00:00:00 GMT + Thu, 11 Apr 2024 00:00:00 -0700 https://supabase.com/blog/how-pg-graphql-works How pg_graphql works https://supabase.com/blog/how-pg-graphql-works An insight into the internals of GraphQL in Postgres using pg_graphql, and how you can contribute. - Wed, 24 Jan 2024 00:00:00 GMT + Wed, 24 Jan 2024 00:00:00 -0700 diff --git a/apps/www/public/planetpg-stas-rss.xml b/apps/www/public/planetpg-stas-rss.xml index 3cf40b487a5..55416c3e1b7 100644 --- a/apps/www/public/planetpg-stas-rss.xml +++ b/apps/www/public/planetpg-stas-rss.xml @@ -5,14 +5,14 @@ https://supabase.com/blog Latest Postgres news from Stanislav Muzhyk at Supabase en - Wed, 13 Dec 2023 00:00:00 GMT + Wed, 13 Dec 2023 00:00:00 -0700 https://supabase.com/blog/supavisor-postgres-connection-pooler Supavisor 1.0: a scalable connection pooler for Postgres https://supabase.com/blog/supavisor-postgres-connection-pooler Supavisor is now used across all projects, providing a scalable and cloud-native Postgres connection pooler that can handle millions of connections - Wed, 13 Dec 2023 00:00:00 GMT + Wed, 13 Dec 2023 00:00:00 -0700 diff --git a/apps/www/public/planetpg-steve_chavez-rss.xml b/apps/www/public/planetpg-steve_chavez-rss.xml index 5260c2a3f57..288cdacabf0 100644 --- a/apps/www/public/planetpg-steve_chavez-rss.xml +++ b/apps/www/public/planetpg-steve_chavez-rss.xml @@ -5,21 +5,21 @@ https://supabase.com/blog Latest Postgres news from Steve Chavez at Supabase en - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/postgrest-11-prerelease PostgREST 11 pre-release https://supabase.com/blog/postgrest-11-prerelease Describes new features of PostgREST 11 pre-release - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/roles-postgres-hooks Protecting reserved roles with PostgreSQL Hooks https://supabase.com/blog/roles-postgres-hooks Using Postgres Hooks to protect functionality in your Postgres database. - Fri, 02 Jul 2021 00:00:00 GMT + Fri, 02 Jul 2021 00:00:00 -0700 diff --git a/apps/www/public/planetpg-victor-rss.xml b/apps/www/public/planetpg-victor-rss.xml index ab36b7d424b..dd25775e9e2 100644 --- a/apps/www/public/planetpg-victor-rss.xml +++ b/apps/www/public/planetpg-victor-rss.xml @@ -5,28 +5,28 @@ https://supabase.com/blog Latest Postgres news from Victor at Supabase en - Fri, 14 Oct 2022 00:00:00 GMT + Fri, 14 Oct 2022 00:00:00 -0700 https://supabase.com/blog/postgres-full-text-search-vs-the-rest Postgres Full Text Search vs the rest https://supabase.com/blog/postgres-full-text-search-vs-the-rest Comparing one of the most popular Postgres features against alternatives - Fri, 14 Oct 2022 00:00:00 GMT + Fri, 14 Oct 2022 00:00:00 -0700 https://supabase.com/blog/choosing-a-postgres-primary-key Choosing a Postgres Primary Key https://supabase.com/blog/choosing-a-postgres-primary-key Turns out the question of which identifier to use as a Primary Key is complicated -- we're going to dive into some of the complexity and inherent trade-offs, and figure things out - Thu, 08 Sep 2022 00:00:00 GMT + Thu, 08 Sep 2022 00:00:00 -0700 https://supabase.com/blog/seen-by-in-postgresql Implementing "seen by" functionality with Postgres https://supabase.com/blog/seen-by-in-postgresql Different approaches for tracking visitor counts with PostgreSQL. - Mon, 18 Jul 2022 00:00:00 GMT + Mon, 18 Jul 2022 00:00:00 -0700 diff --git a/apps/www/public/rss.xml b/apps/www/public/rss.xml index 3e9567a0fbf..2f3e39b195a 100644 --- a/apps/www/public/rss.xml +++ b/apps/www/public/rss.xml @@ -5,2114 +5,2121 @@ https://supabase.com Latest news from Supabase en - Thu, 10 Oct 2024 00:00:00 GMT + Wed, 30 Oct 2024 00:00:00 -0700 + https://supabase.com/blog/supabase-clickhouse-partnership + ClickHouse Partnership, improved Postgres Replication, and Disk Management + https://supabase.com/blog/supabase-clickhouse-partnership + Improving the developer experience between Postgres and ClickHouse. + Wed, 30 Oct 2024 00:00:00 -0700 + + https://supabase.com/blog/database-build-live-share Live Share: Connect to in-browser PGlite with any Postgres client https://supabase.com/blog/database-build-live-share Connect any Postgres client to your postgres.new databases. - Thu, 10 Oct 2024 00:00:00 GMT + Thu, 10 Oct 2024 00:00:00 -0700 https://supabase.com/blog/mongodb-realm-and-device-sync-alternatives MongoDB Realm & Device Sync Alternatives - Supabase https://supabase.com/blog/mongodb-realm-and-device-sync-alternatives Learn how Supabase can help you transition from MongoDB Realm and Device Sync. - Wed, 09 Oct 2024 00:00:00 GMT + Wed, 09 Oct 2024 00:00:00 -0700 https://supabase.com/blog/offline-first-flutter-apps Building offline-first mobile apps with Supabase, Flutter and Brick https://supabase.com/blog/offline-first-flutter-apps Brick is an all-in-one data manager for Flutter that handles querying and uploading between Supabase and local caches like SQLite. Using Brick, developers can focus on implementing the application without worrying about translating or storing their data. - Tue, 08 Oct 2024 00:00:00 GMT + Tue, 08 Oct 2024 00:00:00 -0700 https://supabase.com/blog/lw12-hackathon-winners Supabase Launch Week 12 Hackathon https://supabase.com/blog/lw12-hackathon-winners Announcing the winners of the Supabase Launch Week 12 Hackathon. - Mon, 30 Sep 2024 00:00:00 GMT + Mon, 30 Sep 2024 00:00:00 -0700 https://supabase.com/blog/local-first-expo-legend-state Local-first Realtime Apps with Expo and Legend-State https://supabase.com/blog/local-first-expo-legend-state Build local-first realtime web and mobile apps with Expo, Legend-State, and Supabase. - Mon, 23 Sep 2024 00:00:00 GMT + Mon, 23 Sep 2024 00:00:00 -0700 https://supabase.com/blog/edge-functions-faster-smaller Edge Functions are now 2x smaller and boot 3x faster https://supabase.com/blog/edge-functions-faster-smaller Redeploy your Edge Functions with the CLI v1.192.5 for a peformance boost - Thu, 12 Sep 2024 00:00:00 GMT + Thu, 12 Sep 2024 00:00:00 -0700 https://supabase.com/blog/flutter-uber-clone Building an Uber Clone with Flutter and Supabase https://supabase.com/blog/flutter-uber-clone Learn how to handle real-time geospatial data using Supabase Realtime and Flutter. - Thu, 05 Sep 2024 00:00:00 GMT + Thu, 05 Sep 2024 00:00:00 -0700 https://supabase.com/blog/in-browser-semantic-search-pglite In-Browser Semantic AI Search with PGlite and Transformers.js https://supabase.com/blog/in-browser-semantic-search-pglite Use pgvector in PGlite and combine it with Huggingface Transformers.js for a fully local, in-browser semantic search functionality! - Thu, 29 Aug 2024 00:00:00 GMT + Thu, 29 Aug 2024 00:00:00 -0700 https://supabase.com/blog/supabase-vercel-partnership Supabase + Vercel Partnership https://supabase.com/blog/supabase-vercel-partnership Vercel just added official First-Party Integrations. We're one of them. - Wed, 28 Aug 2024 00:00:00 GMT + Wed, 28 Aug 2024 00:00:00 -0700 https://supabase.com/blog/supabase-lw12-hackathon Supabase Launch Week 12 Hackathon https://supabase.com/blog/supabase-lw12-hackathon Build an Open Source Project over 10 days. 5 prize categories. - Mon, 26 Aug 2024 00:00:00 GMT + Mon, 26 Aug 2024 00:00:00 -0700 https://supabase.com/blog/mozilla-llamafile-in-supabase-edge-functions Mozilla Llamafile in Supabase Edge Functions https://supabase.com/blog/mozilla-llamafile-in-supabase-edge-functions Use Mozilla Llamafile OpenAI API compatible server in Supabase Edge Functions. - Wed, 21 Aug 2024 00:00:00 GMT + Wed, 21 Aug 2024 00:00:00 -0700 https://supabase.com/blog/launch-week-12-top-10 Top 10 Launches of Launch Week 12 https://supabase.com/blog/launch-week-12-top-10 Highlights from Launch Week 12 - Fri, 16 Aug 2024 00:00:00 GMT + Fri, 16 Aug 2024 00:00:00 -0700 https://supabase.com/blog/platform-access-control Introducing New Platform Access Control https://supabase.com/blog/platform-access-control Granular permissions for adding users to specific projects in an Supabase organization. - Fri, 16 Aug 2024 00:00:00 GMT + Fri, 16 Aug 2024 00:00:00 -0700 https://supabase.com/blog/postgres-foreign-data-wrappers-with-wasm Postgres Foreign Data Wrappers with Wasm https://supabase.com/blog/postgres-foreign-data-wrappers-with-wasm Build Wasm foreign data wrapper with Wrappers and use it on Supabase - Fri, 16 Aug 2024 00:00:00 GMT + Fri, 16 Aug 2024 00:00:00 -0700 https://supabase.com/blog/postgrest-12-2 PostgREST 12.2: Prometheus metrics https://supabase.com/blog/postgrest-12-2 New features in the latest 12.2 release of PostgREST - Fri, 16 Aug 2024 00:00:00 GMT + Fri, 16 Aug 2024 00:00:00 -0700 https://supabase.com/blog/python-support Supabase Python https://supabase.com/blog/python-support Supabase Python is now officially supported in Supabase. - Fri, 16 Aug 2024 00:00:00 GMT + Fri, 16 Aug 2024 00:00:00 -0700 https://supabase.com/blog/supabase-book-by-david-lorenz The Supabase Book by David Lorenz https://supabase.com/blog/supabase-book-by-david-lorenz Learn Supabase by building a Multi-Tenant platform. - Fri, 16 Aug 2024 00:00:00 GMT + Fri, 16 Aug 2024 00:00:00 -0700 https://supabase.com/blog/vec2pg vec2pg: Migrate to pgvector from Pinecone and Qdrant https://supabase.com/blog/vec2pg Migrate vector data from vector DBs to Supabase - Fri, 16 Aug 2024 00:00:00 GMT + Fri, 16 Aug 2024 00:00:00 -0700 https://supabase.com/blog/log-drains Introducing Log Drains https://supabase.com/blog/log-drains Log Drains for exporting product logs is now available under Public Alpha - Thu, 15 Aug 2024 00:00:00 GMT + Thu, 15 Aug 2024 00:00:00 -0700 https://supabase.com/blog/pg-graphql-1-5-7 pg_graphql 1.5.7: pagination and multi-tenancy support https://supabase.com/blog/pg-graphql-1-5-7 Latest features of pg_graphql - Thu, 15 Aug 2024 00:00:00 GMT + Thu, 15 Aug 2024 00:00:00 -0700 https://supabase.com/blog/snaplet-is-now-open-source Snaplet is now open source https://supabase.com/blog/snaplet-is-now-open-source Snaplet is closing their business and opening their source code - Wed, 14 Aug 2024 00:00:00 GMT + Wed, 14 Aug 2024 00:00:00 -0700 https://supabase.com/blog/third-party-auth-mfa-phone-send-hooks Supabase Auth: Bring-your-own Auth0, Cognito, or Firebase https://supabase.com/blog/third-party-auth-mfa-phone-send-hooks Use Firebase Auth, Auth0 or AWS Cognito (Amplify) with your Supabase project, secure your users with SMS based MFA, and use send hooks. - Wed, 14 Aug 2024 00:00:00 GMT + Wed, 14 Aug 2024 00:00:00 -0700 https://supabase.com/blog/supabase-realtime-broadcast-and-presence-authorization Supabase Realtime: Broadcast and Presence Authorization https://supabase.com/blog/supabase-realtime-broadcast-and-presence-authorization Secure Realtime Broadcast and Presence with Authorization - Tue, 13 Aug 2024 00:00:00 GMT + Tue, 13 Aug 2024 00:00:00 -0700 https://supabase.com/blog/github-copilot-extension-for-vs-code Official Supabase extension for VS Code and GitHub Copilot https://supabase.com/blog/github-copilot-extension-for-vs-code Today we're launching a new GitHub Copilot extension for VS Code to make your development with Supabase even more delightful. - Mon, 12 Aug 2024 00:00:00 GMT + Mon, 12 Aug 2024 00:00:00 -0700 https://supabase.com/blog/postgres-new postgres.new: In-browser Postgres with an AI interface https://supabase.com/blog/postgres-new Introducing postgres.new, the in-browser Postgres sandbox with AI assistance. - Mon, 12 Aug 2024 00:00:00 GMT + Mon, 12 Aug 2024 00:00:00 -0700 https://supabase.com/blog/supabase-js-on-jsr Announcing Supabase on JSR https://supabase.com/blog/supabase-js-on-jsr Supabase is now available on the open source JavaScript Registry (JSR). - Tue, 16 Jul 2024 00:00:00 GMT + Tue, 16 Jul 2024 00:00:00 -0700 https://supabase.com/blog/hardening-supabase Supabase Security Suite https://supabase.com/blog/hardening-supabase Learn how to use range columns in Postgres to simplify time-based queries and add constraints to prevent overlaps. - Thu, 11 Jul 2024 00:00:00 GMT + Thu, 11 Jul 2024 00:00:00 -0700 https://supabase.com/blog/range-columns Simplifying Time-Based Queries with Range Columns https://supabase.com/blog/range-columns Learn how to use range columns in Postgres to simplify time-based queries and add constraints to prevent overlaps. - Thu, 11 Jul 2024 00:00:00 GMT + Thu, 11 Jul 2024 00:00:00 -0700 https://supabase.com/blog/postgres-realtime-location-sharing-with-maplibre Postgres Realtime location sharing with MapLibre https://supabase.com/blog/postgres-realtime-location-sharing-with-maplibre Use Supabase Realtime to draw live location data onto the map with MapLibre GL JS. - Thu, 04 Jul 2024 00:00:00 GMT + Thu, 04 Jul 2024 00:00:00 -0700 https://supabase.com/blog/postgis-generate-vector-tiles Generate Vector Tiles with PostGIS https://supabase.com/blog/postgis-generate-vector-tiles Use PostGIS to programmatically generate Mapbox Vector Tiles and render them with MapLibre GL. - Wed, 26 Jun 2024 00:00:00 GMT + Wed, 26 Jun 2024 00:00:00 -0700 https://supabase.com/blog/self-host-maps-storage-protomaps Self-host Maps with Protomaps and Supabase Storage https://supabase.com/blog/self-host-maps-storage-protomaps Host Protomaps PMTiles on Supabase Storage and render them with MapLibre GL. - Wed, 19 Jun 2024 00:00:00 GMT + Wed, 19 Jun 2024 00:00:00 -0700 https://supabase.com/blog/calcom-platform-starter-kit-nextjs-supabase Cal.com launches Expert Marketplace built with Next.js and Supabase. https://supabase.com/blog/calcom-platform-starter-kit-nextjs-supabase Cal.com and Supabase team up to build an open-source platform starter kit for developers. - Tue, 18 Jun 2024 00:00:00 GMT + Tue, 18 Jun 2024 00:00:00 -0700 https://supabase.com/blog/meetup-kahoot-alternative The open source Kahoot alternative https://supabase.com/blog/meetup-kahoot-alternative How we built a Kahoot alternative for the Supabase community meetups. - Thu, 09 May 2024 00:00:00 GMT + Thu, 09 May 2024 00:00:00 -0700 https://supabase.com/blog/pgvector-0-7-0 What's new in pgvector v0.7.0 https://supabase.com/blog/pgvector-0-7-0 Exploring new features in pgvector v0.7.0 - Thu, 02 May 2024 00:00:00 GMT + Thu, 02 May 2024 00:00:00 -0700 https://supabase.com/blog/supabase-oss-hackathon-winners Open Source Hackathon 2024 winners https://supabase.com/blog/supabase-oss-hackathon-winners Announcing the winners of the Open Source Hackathon 2024! - Tue, 30 Apr 2024 00:00:00 GMT + Tue, 30 Apr 2024 00:00:00 -0700 https://supabase.com/blog/postgres-bloat Postgres Bloat Minimization https://supabase.com/blog/postgres-bloat Understanding and minimizing Postgres table bloat - Fri, 26 Apr 2024 00:00:00 GMT + Fri, 26 Apr 2024 00:00:00 -0700 https://supabase.com/blog/exploring-support-tooling Exploring Support Tooling at Supabase: A Dive into SLA Buddy https://supabase.com/blog/exploring-support-tooling In this post, we explore the support tooling at Supabase, and how we use SLA Buddy to monitor our SLAs. - Thu, 25 Apr 2024 00:00:00 GMT + Thu, 25 Apr 2024 00:00:00 -0700 https://supabase.com/blog/nix-postgres Packaging Supabase with Nix https://supabase.com/blog/nix-postgres - Thu, 25 Apr 2024 00:00:00 GMT + Thu, 25 Apr 2024 00:00:00 -0700 https://supabase.com/blog/s3-compatible-storage Supabase Storage: now supports the S3 protocol https://supabase.com/blog/s3-compatible-storage Supabase Storage is now officially an S3-Compatible Storage Provider. - Thu, 18 Apr 2024 00:00:00 GMT + Thu, 18 Apr 2024 00:00:00 -0700 https://supabase.com/blog/ga-week-summary Top 10 Launches from Supabase GA Week https://supabase.com/blog/ga-week-summary A recap of the most important launches and updates from the week. - Thu, 18 Apr 2024 00:00:00 GMT + Thu, 18 Apr 2024 00:00:00 -0700 https://supabase.com/blog/security-performance-advisor Supabase Security Advisor & Performance Advisor https://supabase.com/blog/security-performance-advisor We're making it easier to build a secure and high-performing application. - Thu, 18 Apr 2024 00:00:00 GMT + Thu, 18 Apr 2024 00:00:00 -0700 https://supabase.com/blog/anonymous-sign-ins Supabase Auth now supports Anonymous Sign-ins https://supabase.com/blog/anonymous-sign-ins Sign in as an anonymous user to authenticate with Supabase - Wed, 17 Apr 2024 00:00:00 GMT + Wed, 17 Apr 2024 00:00:00 -0700 https://supabase.com/blog/ai-inference-now-available-in-supabase-edge-functions AI Inference now available in Supabase Edge Functions https://supabase.com/blog/ai-inference-now-available-in-supabase-edge-functions Use embeddings and large language models on the edge with Supabase Edge Functions. - Tue, 16 Apr 2024 00:00:00 GMT + Tue, 16 Apr 2024 00:00:00 -0700 https://supabase.com/blog/branching-publicly-available Branching now Publicly Available https://supabase.com/blog/branching-publicly-available Supabase Branching is now available on Pro Plan and above. - Mon, 15 Apr 2024 00:00:00 GMT + Mon, 15 Apr 2024 00:00:00 -0700 https://supabase.com/blog/supabase-acquires-oriole Oriole joins Supabase https://supabase.com/blog/supabase-acquires-oriole The Oriole team are joining Supabase to build a faster storage engine for Postgres. - Mon, 15 Apr 2024 00:00:00 GMT + Mon, 15 Apr 2024 00:00:00 -0700 https://supabase.com/blog/supabase-aws-marketplace Supabase on the AWS Marketplace https://supabase.com/blog/supabase-aws-marketplace Supabase is now available on the AWS Marketplace, Simplifying Procurement for Enterprise Customers. - Mon, 15 Apr 2024 00:00:00 GMT + Mon, 15 Apr 2024 00:00:00 -0700 https://supabase.com/blog/supabase-bootstrap Supabase Bootstrap: the fastest way to launch a new project https://supabase.com/blog/supabase-bootstrap Launch a new hosted Supabase project directly from the CLI using pre-built applications. - Mon, 15 Apr 2024 00:00:00 GMT + Mon, 15 Apr 2024 00:00:00 -0700 https://supabase.com/blog/supabase-swift Supabase Swift https://supabase.com/blog/supabase-swift Supabase Swift is now officially supported. - Mon, 15 Apr 2024 00:00:00 GMT + Mon, 15 Apr 2024 00:00:00 -0700 https://supabase.com/blog/supabase-oss-hackathon Supabase Open Source Hackathon 2024 https://supabase.com/blog/supabase-oss-hackathon Build an Open Source Project over 10 days. 5 prize categories. - Fri, 12 Apr 2024 00:00:00 GMT + Fri, 12 Apr 2024 00:00:00 -0700 https://supabase.com/blog/postgres-roles-and-privileges Postgres Roles and Privileges https://supabase.com/blog/postgres-roles-and-privileges A guide to Postgres roles and privileges - Thu, 11 Apr 2024 00:00:00 GMT + Thu, 11 Apr 2024 00:00:00 -0700 https://supabase.com/blog/pg-paper-dump Announcing Data Preservation Service https://supabase.com/blog/pg-paper-dump Secure, reliable and timeless backups - Mon, 01 Apr 2024 00:00:00 GMT + Mon, 01 Apr 2024 00:00:00 -0700 https://supabase.com/blog/semantic-image-search-amazon-bedrock Implementing semantic image search with Amazon Titan and Supabase Vector https://supabase.com/blog/semantic-image-search-amazon-bedrock Implementing semantic image search with Amazon Titan and Supabase Vector in Python. - Tue, 26 Mar 2024 00:00:00 GMT + Tue, 26 Mar 2024 00:00:00 -0700 https://supabase.com/blog/postgrest-aggregate-functions PostgREST Aggregate Functions https://supabase.com/blog/postgrest-aggregate-functions Summarize your data by performing calculations across groups of rows in PostgREST - Thu, 29 Feb 2024 00:00:00 GMT + Thu, 29 Feb 2024 00:00:00 -0700 https://supabase.com/blog/content-recommendation-with-flutter Build a content recommendation app with Flutter and OpenAI https://supabase.com/blog/content-recommendation-with-flutter Build a movie-viewing app that recommends another movie based on what the user is viewing using OpenAI, Flutter and Supabase. - Mon, 26 Feb 2024 00:00:00 GMT + Mon, 26 Feb 2024 00:00:00 -0700 https://supabase.com/blog/automating-performance-tests Automating performance tests https://supabase.com/blog/automating-performance-tests Learn about our story to get to the automated performance testing. - Wed, 21 Feb 2024 00:00:00 GMT + Wed, 21 Feb 2024 00:00:00 -0700 https://supabase.com/blog/matryoshka-embeddings Matryoshka embeddings: faster OpenAI vector search using Adaptive Retrieval https://supabase.com/blog/matryoshka-embeddings Use Adaptive Retrieval to improve query performance with OpenAI's new embedding models - Tue, 13 Feb 2024 00:00:00 GMT + Tue, 13 Feb 2024 00:00:00 -0700 https://supabase.com/blog/nosql-mongodb-compatibility-with-ferretdb-and-flydotio NoSQL Postgres: Add MongoDB compatibility to your Supabase projects with FerretDB https://supabase.com/blog/nosql-mongodb-compatibility-with-ferretdb-and-flydotio NoSQL Postgres: Add MongoDB compatibility to your Supabase projects with FerretDB - Wed, 31 Jan 2024 00:00:00 GMT + Wed, 31 Jan 2024 00:00:00 -0700 https://supabase.com/blog/pgvector-fast-builds pgvector 0.6.0: 30x faster with parallel index builds https://supabase.com/blog/pgvector-fast-builds pgvector 0.6.0 brings a significant improvement: parallel index builds for HNSW. Building an HNSW index is now up to 30x faster for unlogged tables. - Tue, 30 Jan 2024 00:00:00 GMT + Tue, 30 Jan 2024 00:00:00 -0700 https://supabase.com/blog/ruby-on-rails-postgres Getting started with Ruby on Rails and Postgres on Supabase https://supabase.com/blog/ruby-on-rails-postgres Learn how to create a new Rails app and connect it to a Supabase Postgres database. - Mon, 29 Jan 2024 00:00:00 GMT + Mon, 29 Jan 2024 00:00:00 -0700 https://supabase.com/blog/flutter-figma-clone Create a Figma Clone app with Flutter and Supabase Realtime https://supabase.com/blog/flutter-figma-clone A tutorial on how to build a collaborative design app like Figma using Flutter and Supabase Realtime. - Fri, 26 Jan 2024 00:00:00 GMT + Fri, 26 Jan 2024 00:00:00 -0700 https://supabase.com/blog/how-pg-graphql-works How pg_graphql works https://supabase.com/blog/how-pg-graphql-works An insight into the internals of GraphQL in Postgres using pg_graphql, and how you can contribute. - Wed, 24 Jan 2024 00:00:00 GMT + Wed, 24 Jan 2024 00:00:00 -0700 https://supabase.com/blog/laravel-postgres Getting started with Laravel and Postgres https://supabase.com/blog/laravel-postgres Learn how to create a new Laravel PHP app and connect it to a Supabase PostgreSQL database. - Mon, 22 Jan 2024 00:00:00 GMT + Mon, 22 Jan 2024 00:00:00 -0700 https://supabase.com/blog/what-is-saml-authentication What is SAML? A practical guide to the authentication protocol https://supabase.com/blog/what-is-saml-authentication Learn what is SAML authentication, how it differentiates from SSO, SAML with Postgres, and more. - Wed, 17 Jan 2024 00:00:00 GMT + Wed, 17 Jan 2024 00:00:00 -0700 https://supabase.com/blog/react-query-nextjs-app-router-cache-helpers Using React Query with Next.js App Router and Supabase Cache Helpers https://supabase.com/blog/react-query-nextjs-app-router-cache-helpers Learn how to use React Query in Next.js Client & Server Components for data fetching with Supabase. - Fri, 12 Jan 2024 00:00:00 GMT + Fri, 12 Jan 2024 00:00:00 -0700 https://supabase.com/blog/ipv6 Brace yourself, IPv6 is coming https://supabase.com/blog/ipv6 On February 1st 2024, AWS will start charging for IPv4 addresses. This is a big deal for the internet, and we're here to help you prepare. - Fri, 12 Jan 2024 00:00:00 GMT + Fri, 12 Jan 2024 00:00:00 -0700 https://supabase.com/blog/elixir-clustering-using-postgres Elixir clustering using Postgres https://supabase.com/blog/elixir-clustering-using-postgres Learn about our approach to connecting multiple nodes in Elixir using Postgres - Tue, 09 Jan 2024 00:00:00 GMT + Tue, 09 Jan 2024 00:00:00 -0700 https://supabase.com/blog/beta-update-december-2023 Supabase Beta December 2023 https://supabase.com/blog/beta-update-december-2023 A Launch Week X rundown with all the fantastic goodies we shipped - Fri, 05 Jan 2024 00:00:00 GMT + Fri, 05 Jan 2024 00:00:00 -0700 https://supabase.com/blog/launch-week-x-hackathon-winners Launch Week X Hackathon Winners https://supabase.com/blog/launch-week-x-hackathon-winners Announcing the winners of the Launch Week X Hackathon! - Thu, 04 Jan 2024 00:00:00 GMT + Thu, 04 Jan 2024 00:00:00 -0700 https://supabase.com/blog/launch-week-x-best-launches Top 10 Launches of LWX https://supabase.com/blog/launch-week-x-best-launches Our CEO takes a look at his favorite ships from LWX - Tue, 19 Dec 2023 00:00:00 GMT + Tue, 19 Dec 2023 00:00:00 -0700 https://supabase.com/blog/client-libraries-v2 Supabase Libraries V2: Python, Swift, Kotlin, Flutter, and Typescript https://supabase.com/blog/client-libraries-v2 Swift, Kotlin, C#, and Python are now stable and moving to the v2 API. - Fri, 15 Dec 2023 00:00:00 GMT + Fri, 15 Dec 2023 00:00:00 -0700 https://supabase.com/blog/introducing-read-replicas Introducing Read Replicas https://supabase.com/blog/introducing-read-replicas We are launching support for Postgres Read Replicas - Fri, 15 Dec 2023 00:00:00 GMT + Fri, 15 Dec 2023 00:00:00 -0700 https://supabase.com/blog/postgres-on-fly-by-supabase Fly Postgres, managed by Supabase https://supabase.com/blog/postgres-on-fly-by-supabase A managed Postgres offering developed by Supabase and Fly.io - Fri, 15 Dec 2023 00:00:00 GMT + Fri, 15 Dec 2023 00:00:00 -0700 https://supabase.com/blog/supabase-auth-identity-linking-hooks Supabase Auth: Identity Linking, Hooks, and HaveIBeenPwned integration https://supabase.com/blog/supabase-auth-identity-linking-hooks Four major Auth features: Identity Linking, Session Control, Leaked Password Protection, and Hooks - Thu, 14 Dec 2023 00:00:00 GMT + Thu, 14 Dec 2023 00:00:00 -0700 https://supabase.com/blog/supabase-wrappers-v02 Supabase Wrappers v0.2: Query Pushdown & Remote Subqueries https://supabase.com/blog/supabase-wrappers-v02 Supabase Wrappers v0.2 brings more Wrappers, query pushdown, remote subquery, and more - Thu, 14 Dec 2023 00:00:00 GMT + Thu, 14 Dec 2023 00:00:00 -0700 https://supabase.com/blog/postgrest-12 PostgREST 12 https://supabase.com/blog/postgrest-12 PostgREST 12 is out and we take a look at some of the major new features like JWT Caching and Aggregate Functions - Wed, 13 Dec 2023 00:00:00 GMT + Wed, 13 Dec 2023 00:00:00 -0700 https://supabase.com/blog/supabase-branching Supabase Branching https://supabase.com/blog/supabase-branching A Postgres database for every Pull Request. - Wed, 13 Dec 2023 00:00:00 GMT + Wed, 13 Dec 2023 00:00:00 -0700 https://supabase.com/blog/supavisor-postgres-connection-pooler Supavisor 1.0: a scalable connection pooler for Postgres https://supabase.com/blog/supavisor-postgres-connection-pooler Supavisor is now used across all projects, providing a scalable and cloud-native Postgres connection pooler that can handle millions of connections - Wed, 13 Dec 2023 00:00:00 GMT + Wed, 13 Dec 2023 00:00:00 -0700 https://supabase.com/blog/edge-functions-node-npm Edge Functions: Node and native npm compatibility https://supabase.com/blog/edge-functions-node-npm We're adding Node and native npm compatibility for Edge Functions. - Tue, 12 Dec 2023 00:00:00 GMT + Tue, 12 Dec 2023 00:00:00 -0700 https://supabase.com/blog/pg-graphql-postgres-functions pg_graphql: Postgres functions now supported https://supabase.com/blog/pg-graphql-postgres-functions pg_graphql now supports the most requested feature: Postgres functions a.k.a. User Defined Functions (UDFs) - Tue, 12 Dec 2023 00:00:00 GMT + Tue, 12 Dec 2023 00:00:00 -0700 https://supabase.com/blog/studio-introducing-assistant Supabase Studio: AI Assistant and User Impersonation https://supabase.com/blog/studio-introducing-assistant We're introducing the next generation of our AI Assistant and some features that will help get your ideas into code even faster. - Mon, 11 Dec 2023 00:00:00 GMT + Mon, 11 Dec 2023 00:00:00 -0700 https://supabase.com/blog/how-design-works-at-supabase How design works at Supabase https://supabase.com/blog/how-design-works-at-supabase The transformative journey of Supabase's Design team and its unique culture to enhance the output and quality of the entire company. - Fri, 08 Dec 2023 00:00:00 GMT + Fri, 08 Dec 2023 00:00:00 -0700 https://supabase.com/blog/postgres-language-server-implementing-parser Postgres Language Server: implementing the Parser https://supabase.com/blog/postgres-language-server-implementing-parser A detailed analysis of our iterations to implement a Parser for Postgres - Fri, 08 Dec 2023 00:00:00 GMT + Fri, 08 Dec 2023 00:00:00 -0700 https://supabase.com/blog/beta-update-november-2023 Supabase Beta November 2023 https://supabase.com/blog/beta-update-november-2023 Launch Week X is coming! But we still have cool updates that we couldn't fit next week. - Tue, 05 Dec 2023 00:00:00 GMT + Tue, 05 Dec 2023 00:00:00 -0700 https://supabase.com/blog/community-meetups-lwx Launch Week X Community Meetups https://supabase.com/blog/community-meetups-lwx The Supabase Community Meetups are back, and this time we've got more events happening all over the world! - Tue, 05 Dec 2023 00:00:00 GMT + Tue, 05 Dec 2023 00:00:00 -0700 https://supabase.com/blog/supabase-hackathon-lwx Supabase Launch Week X Hackathon https://supabase.com/blog/supabase-hackathon-lwx Build an Open Source Project over 10 days. 5 prize categories. - Tue, 05 Dec 2023 00:00:00 GMT + Tue, 05 Dec 2023 00:00:00 -0700 https://supabase.com/blog/automatic-cli-login Automatic CLI login https://supabase.com/blog/automatic-cli-login Explore the technical implementation and security measures behind CLI's new automatic login feature. - Fri, 01 Dec 2023 00:00:00 GMT + Fri, 01 Dec 2023 00:00:00 -0700 https://supabase.com/blog/oauth2-login-python-flask-apps GitHub OAuth in your Python Flask app https://supabase.com/blog/oauth2-login-python-flask-apps A step-by-step guide on building Login with Github into your Python apps. - Tue, 21 Nov 2023 00:00:00 GMT + Tue, 21 Nov 2023 00:00:00 -0700 https://supabase.com/blog/react-native-authentication Getting started with React Native authentication https://supabase.com/blog/react-native-authentication Learn how to implement authentication in your React Native applications. - Thu, 16 Nov 2023 00:00:00 GMT + Thu, 16 Nov 2023 00:00:00 -0700 https://supabase.com/blog/beta-update-october-2023 Supabase Beta October 2023 https://supabase.com/blog/beta-update-october-2023 Brand-new features, community content, and (more importantly) the date for our next Launch Week. - Mon, 06 Nov 2023 00:00:00 GMT + Mon, 06 Nov 2023 00:00:00 -0700 https://supabase.com/blog/supabase-is-now-compatible-with-nextjs-14 Supabase is now compatible with Next.js 14 https://supabase.com/blog/supabase-is-now-compatible-with-nextjs-14 The fastest way to build apps with Next.js 14 and Supabase - Wed, 01 Nov 2023 00:00:00 GMT + Wed, 01 Nov 2023 00:00:00 -0700 https://supabase.com/blog/pgvector-vs-pinecone pgvector vs Pinecone: cost and performance https://supabase.com/blog/pgvector-vs-pinecone Direct performance comparison between pgvector and Pinecone. - Tue, 10 Oct 2023 00:00:00 GMT + Tue, 10 Oct 2023 00:00:00 -0700 https://supabase.com/blog/react-native-offline-first-watermelon-db Offline-first React Native Apps with Expo, WatermelonDB, and Supabase https://supabase.com/blog/react-native-offline-first-watermelon-db Store your data locally and sync it with Postgres using WatermelonDB! - Sun, 08 Oct 2023 00:00:00 GMT + Sun, 08 Oct 2023 00:00:00 -0700 https://supabase.com/blog/beta-update-september-2023 Supabase Beta September 2023 https://supabase.com/blog/beta-update-september-2023 September was packed with so many new features and releases that it might have seemed like another Launch Week, but it wasn't! - Wed, 04 Oct 2023 00:00:00 GMT + Wed, 04 Oct 2023 00:00:00 -0700 https://supabase.com/blog/postgres-dynamic-table-partitioning Dynamic Table Partitioning in Postgres https://supabase.com/blog/postgres-dynamic-table-partitioning Learn how to scale large postgres tables in place and increase query performance. - Tue, 03 Oct 2023 00:00:00 GMT + Tue, 03 Oct 2023 00:00:00 -0700 https://supabase.com/blog/beta-update-august-2023 Supabase Beta August 2023 https://supabase.com/blog/beta-update-august-2023 Launch Week 8 review and more things we shipped 🚀 - Fri, 08 Sep 2023 00:00:00 GMT + Fri, 08 Sep 2023 00:00:00 -0700 https://supabase.com/blog/increase-performance-pgvector-hnsw pgvector v0.5.0: Faster semantic search with HNSW indexes https://supabase.com/blog/increase-performance-pgvector-hnsw Increase performance in pgvector using HNSW indexes - Wed, 06 Sep 2023 00:00:00 GMT + Wed, 06 Sep 2023 00:00:00 -0700 https://supabase.com/blog/organization-based-billing Organization-based Billing, Project Transfers, Team Plan https://supabase.com/blog/organization-based-billing Introducing the new Organization-based Billing - Thu, 31 Aug 2023 00:00:00 GMT + Thu, 31 Aug 2023 00:00:00 -0700 https://supabase.com/blog/launch-week-8-hackathon-winners Launch Week 8 Hackathon Winners https://supabase.com/blog/launch-week-8-hackathon-winners Announcing the winners of the Launch Week 8 Hackathon! - Thu, 24 Aug 2023 00:00:00 GMT + Thu, 24 Aug 2023 00:00:00 -0700 https://supabase.com/blog/launch-week-8-community-highlights Launch Week 8 Community Highlights https://supabase.com/blog/launch-week-8-community-highlights Highlights from the community for the past 4 months. - Fri, 11 Aug 2023 00:00:00 GMT + Fri, 11 Aug 2023 00:00:00 -0700 https://supabase.com/blog/supabase-soc2-hipaa Supabase is now HIPAA and SOC2 Type 2 compliant https://supabase.com/blog/supabase-soc2-hipaa This documents our journey from SOC2 Type 1 to SOC2 Type2 and HIPAA compliance. You can start building healthcare apps on Supabase today. - Fri, 11 Aug 2023 00:00:00 GMT + Fri, 11 Aug 2023 00:00:00 -0700 https://supabase.com/blog/supavisor-1-million Supavisor: Scaling Postgres to 1 Million Connections https://supabase.com/blog/supavisor-1-million Supavisor is a scalable, cloud-native Postgres connection pooler. We connected a million clients to it to see how it performs. - Fri, 11 Aug 2023 00:00:00 GMT + Fri, 11 Aug 2023 00:00:00 -0700 https://supabase.com/blog/supabase-integrations-marketplace Supabase Integrations Marketplace https://supabase.com/blog/supabase-integrations-marketplace Become a Supabase Integrations Partner: Publish OAuth Apps and Build with Supabase. - Thu, 10 Aug 2023 00:00:00 GMT + Thu, 10 Aug 2023 00:00:00 -0700 https://supabase.com/blog/using-supabase-with-vercel Vercel Integration and Next.js App Router Support https://supabase.com/blog/using-supabase-with-vercel Using Supabase with Vercel and Next.js is now a lot easier. - Thu, 10 Aug 2023 00:00:00 GMT + Thu, 10 Aug 2023 00:00:00 -0700 https://supabase.com/blog/supabase-studio-3-0 Supabase Studio 3.0: AI SQL Editor, Schema Diagrams, and new Wrappers https://supabase.com/blog/supabase-studio-3-0 Supabase Studio now comes with an AI assisted SQL Editor, schema diagrams, and much more. - Wed, 09 Aug 2023 00:00:00 GMT + Wed, 09 Aug 2023 00:00:00 -0700 https://supabase.com/blog/supabase-local-dev Supabase Local Dev: migrations, branching, and observability https://supabase.com/blog/supabase-local-dev New features to streamline the interaction between CLI, code editors, and remote databases. - Tue, 08 Aug 2023 00:00:00 GMT + Tue, 08 Aug 2023 00:00:00 -0700 https://supabase.com/blog/hugging-face-supabase Hugging Face is now supported in Supabase https://supabase.com/blog/hugging-face-supabase We've added support Hugging Face support in our Python Vector Client and Edge Functions. - Mon, 07 Aug 2023 00:00:00 GMT + Mon, 07 Aug 2023 00:00:00 -0700 https://supabase.com/blog/why-supabase-remote Why we'll stay remote https://supabase.com/blog/why-supabase-remote Offices are making a comeback, just not at Supabase. - Sat, 05 Aug 2023 00:00:00 GMT + Sat, 05 Aug 2023 00:00:00 -0700 https://supabase.com/blog/interactive-constellation-threejs-react-three-fiber Coding the stars - an interactive constellation with Three.js and React Three Fiber https://supabase.com/blog/interactive-constellation-threejs-react-three-fiber How we built a constellation of stars with Three.js and React Three Fiber. - Fri, 04 Aug 2023 00:00:00 GMT + Fri, 04 Aug 2023 00:00:00 -0700 https://supabase.com/blog/fewer-dimensions-are-better-pgvector pgvector: Fewer dimensions are better https://supabase.com/blog/fewer-dimensions-are-better-pgvector Increase performance in pgvector by using embedding vectors with fewer dimensions - Thu, 03 Aug 2023 00:00:00 GMT + Thu, 03 Aug 2023 00:00:00 -0700 https://supabase.com/blog/beta-update-july-2023 Supabase Beta July 2023 https://supabase.com/blog/beta-update-july-2023 Launch Week 8 is coming - but we still shipped some goodies during July - Wed, 02 Aug 2023 00:00:00 GMT + Wed, 02 Aug 2023 00:00:00 -0700 https://supabase.com/blog/react-native-storage React Native file upload with Supabase Storage https://supabase.com/blog/react-native-storage Learn how to implement authentication and file upload in a React Native app. - Tue, 01 Aug 2023 00:00:00 GMT + Tue, 01 Aug 2023 00:00:00 -0700 https://supabase.com/blog/supabase-lw8-hackathon Supabase Launch Week 8 Hackathon https://supabase.com/blog/supabase-lw8-hackathon Build an Open Source Project over 10 days. 5 prize categories. - Tue, 25 Jul 2023 00:00:00 GMT + Tue, 25 Jul 2023 00:00:00 -0700 https://supabase.com/blog/flutter-authentication Getting started with Flutter authentication https://supabase.com/blog/flutter-authentication Learn how authentication on Flutter works through Google sign in with Supabase auth. - Tue, 18 Jul 2023 00:00:00 GMT + Tue, 18 Jul 2023 00:00:00 -0700 https://supabase.com/blog/pgvector-performance pgvector 0.4.0 performance https://supabase.com/blog/pgvector-performance There's been a lot of talk about pgvector performance lately, so we took some datasets and pushed pgvector to the limits to find out its strengths and limitations. - Thu, 13 Jul 2023 00:00:00 GMT + Thu, 13 Jul 2023 00:00:00 -0700 https://supabase.com/blog/postgrest-11-1-release What is new in PostgREST v11.1? https://supabase.com/blog/postgrest-11-1-release Impersonated Role Settings, Configurable Isolation Level, improved Bulk Insert, and more - Wed, 12 Jul 2023 00:00:00 GMT + Wed, 12 Jul 2023 00:00:00 -0700 https://supabase.com/blog/supabase-beta-update-june-2023 Supabase Beta June 2023 https://supabase.com/blog/supabase-beta-update-june-2023 A plethora of announcements... read till the end to find out when is the new Launch Week - Thu, 06 Jul 2023 00:00:00 GMT + Thu, 06 Jul 2023 00:00:00 -0700 https://supabase.com/blog/native-mobile-auth Native Mobile Auth Support for Google and Apple Sign in https://supabase.com/blog/native-mobile-auth Supabase auth adds full support for native mobile sign in with Apple and Google. - Tue, 27 Jun 2023 00:00:00 GMT + Tue, 27 Jun 2023 00:00:00 -0700 https://supabase.com/blog/supabase-beta-update-may-2023 Supabase Beta May 2023 https://supabase.com/blog/supabase-beta-update-may-2023 Learn about the great things we shipped last month. Spoiler alert... lots of AI. - Fri, 09 Jun 2023 00:00:00 GMT + Fri, 09 Jun 2023 00:00:00 -0700 https://supabase.com/blog/flutter-hackathon-winners Flutter Hackathon Winners https://supabase.com/blog/flutter-hackathon-winners Announcing the winners of the Flutter Hackathon! - Mon, 29 May 2023 00:00:00 GMT + Mon, 29 May 2023 00:00:00 -0700 https://supabase.com/blog/vecs Supabase Vecs: a vector client for Postgres https://supabase.com/blog/vecs Introducing Supabase Vecs, a PostgreSQL vector client - Mon, 29 May 2023 00:00:00 GMT + Mon, 29 May 2023 00:00:00 -0700 https://supabase.com/blog/chatgpt-plugins-support-postgres ChatGPT plugins now support Postgres & Supabase https://supabase.com/blog/chatgpt-plugins-support-postgres Supabase recently contributed to the OpenAI Retrieval Plugin repo with a Postgres and a Supabase implementation to help developers build ChatGPT plugins using pgvector. - Thu, 25 May 2023 00:00:00 GMT + Thu, 25 May 2023 00:00:00 -0700 https://supabase.com/blog/building-chatgpt-plugins-template Building ChatGPT Plugins with Supabase Edge Runtime https://supabase.com/blog/building-chatgpt-plugins-template We're releasing a ChatGPT plugin template written in TypeScript and running on Deno! - Mon, 15 May 2023 00:00:00 GMT + Mon, 15 May 2023 00:00:00 -0700 https://supabase.com/blog/flutter-hackathon Flutter Hackathon https://supabase.com/blog/flutter-hackathon Build Flutter apps and win limited edition swag. - Fri, 12 May 2023 00:00:00 GMT + Fri, 12 May 2023 00:00:00 -0700 https://supabase.com/blog/supabase-beta-update-april-2023 Supabase Beta April 2023 https://supabase.com/blog/supabase-beta-update-april-2023 A review of Launch Week 7 and more exciting updates from last month. - Tue, 09 May 2023 00:00:00 GMT + Tue, 09 May 2023 00:00:00 -0700 https://supabase.com/blog/flutter-multi-factor-authentication Securing your Flutter apps with Multi-Factor Authentication https://supabase.com/blog/flutter-multi-factor-authentication Build a Flutter app where the user is required to authenticate using Multi-Factor Authentication. - Thu, 04 May 2023 00:00:00 GMT + Thu, 04 May 2023 00:00:00 -0700 https://supabase.com/blog/postgres-pluggable-strorage Next steps for Postgres pluggable storage https://supabase.com/blog/postgres-pluggable-strorage Exploring history of Postgres pluggable storage and the possibility of landing it in the Postgres core. - Mon, 01 May 2023 00:00:00 GMT + Mon, 01 May 2023 00:00:00 -0700 https://supabase.com/blog/launch-week-7-hackathon-winners Launch Week 7 Hackathon Winners https://supabase.com/blog/launch-week-7-hackathon-winners Announcing the winners of the Launch Week 7 Hackathon! - Mon, 24 Apr 2023 00:00:00 GMT + Mon, 24 Apr 2023 00:00:00 -0700 https://supabase.com/blog/whats-new-in-pg-graphql-v1-2 What's New in pg_graphql v1.2 https://supabase.com/blog/whats-new-in-pg-graphql-v1-2 New Features in the v1.2 release of pg_graphql - Fri, 21 Apr 2023 00:00:00 GMT + Fri, 21 Apr 2023 00:00:00 -0700 https://supabase.com/blog/dbdev dbdev: PostgreSQL Package Manager https://supabase.com/blog/dbdev We're publicly previewing dbdev, a PostgreSQL package manager. - Fri, 14 Apr 2023 00:00:00 GMT + Fri, 14 Apr 2023 00:00:00 -0700 https://supabase.com/blog/launch-week-7-community-highlights Launch Week 7 Community Highlights https://supabase.com/blog/launch-week-7-community-highlights We're honored to work with, sponsor, and support incredible people and tools. Here is a highlight of the last 3 months. - Fri, 14 Apr 2023 00:00:00 GMT + Fri, 14 Apr 2023 00:00:00 -0700 https://supabase.com/blog/pg-tle Trusted Language Extensions for Postgres https://supabase.com/blog/pg-tle We're collaborating with AWS to bring Trusted Language Extensions to Postgres. - Fri, 14 Apr 2023 00:00:00 GMT + Fri, 14 Apr 2023 00:00:00 -0700 https://supabase.com/blog/supabase-studio-2.0 Supabase Studio 2.0: help when you need it most https://supabase.com/blog/supabase-studio-2.0 Supabase Studio now comes with ChatGPT, and GraphiQL built in, Cascade Deletes, and Foreign Key Selectors, and much more. - Fri, 14 Apr 2023 00:00:00 GMT + Fri, 14 Apr 2023 00:00:00 -0700 https://supabase.com/blog/supabase-auth-sso-pkce Supabase Auth: SSO, Mobile, and Server-side support https://supabase.com/blog/supabase-auth-sso-pkce Supacharging Supabase Auth with Sign in with Apple on iOS, Single-Sign-On support with SAML 2.0, and PKCE for server-side rendering and mobile auth. - Thu, 13 Apr 2023 00:00:00 GMT + Thu, 13 Apr 2023 00:00:00 -0700 https://supabase.com/blog/storage-v3-resumable-uploads Supabase Storage v3: Resumable Uploads with support for 50GB files https://supabase.com/blog/storage-v3-resumable-uploads Storage V3 with lots of new features including resumable uploads, more image transformationsm a Next.js image loader and more. - Wed, 12 Apr 2023 00:00:00 GMT + Wed, 12 Apr 2023 00:00:00 -0700 https://supabase.com/blog/edge-runtime-self-hosted-deno-functions Supabase Edge Runtime: Self-hosted Deno Functions https://supabase.com/blog/edge-runtime-self-hosted-deno-functions We are open-sourcing Supabase Edge Runtime allowing you to host your Edge Functions anywhere. - Tue, 11 Apr 2023 00:00:00 GMT + Tue, 11 Apr 2023 00:00:00 -0700 https://supabase.com/blog/supabase-logs-self-hosted Supabase Logs: open source logging server https://supabase.com/blog/supabase-logs-self-hosted We're releasing Supabase Logs for both self-hosted users and CLI development. - Mon, 10 Apr 2023 00:00:00 GMT + Mon, 10 Apr 2023 00:00:00 -0700 https://supabase.com/blog/supabase-beta-update-march-2023 Supabase Beta March 2023 https://supabase.com/blog/supabase-beta-update-march-2023 We are in full shipping mode 🛥️… Launch Week 7 can’t come quickly enough! - Sat, 08 Apr 2023 00:00:00 GMT + Sat, 08 Apr 2023 00:00:00 -0700 https://supabase.com/blog/designing-with-ai-midjourney Designing with AI: Generating unique artwork for every user https://supabase.com/blog/designing-with-ai-midjourney Using MidJourney to generative artwork and serving ticket images with Edge Functions - Fri, 07 Apr 2023 00:00:00 GMT + Fri, 07 Apr 2023 00:00:00 -0700 https://supabase.com/blog/launch-week-7-hackathon The Supabase AI Hackathon https://supabase.com/blog/launch-week-7-hackathon Build an Open Source Project over 10 days. 5 prize categories. - Fri, 07 Apr 2023 00:00:00 GMT + Fri, 07 Apr 2023 00:00:00 -0700 https://supabase.com/blog/infinite-scroll-with-nextjs-framer-motion Infinite scroll with Next.js, Framer Motion, and Supabase https://supabase.com/blog/infinite-scroll-with-nextjs-framer-motion Lazy load and paginate data on scroll with Next.js and a sprinkle of Framer Motion magic ✨ - Tue, 04 Apr 2023 00:00:00 GMT + Tue, 04 Apr 2023 00:00:00 -0700 https://supabase.com/blog/supaclub SupaClub https://supabase.com/blog/supaclub The Worlds First Software Engineering Nightclub. - Sat, 01 Apr 2023 00:00:00 GMT + Sat, 01 Apr 2023 00:00:00 -0700 https://supabase.com/blog/supabase-beta-update-february-2023 Supabase Beta February 2023 https://supabase.com/blog/supabase-beta-update-february-2023 There is something for everybody this month - AI, Auth, Database, Edge Functions, GraphQL … you name it! - Thu, 09 Mar 2023 00:00:00 GMT + Thu, 09 Mar 2023 00:00:00 -0700 https://supabase.com/blog/geo-queries-with-postgis-in-ionic-angular Geo Queries with PostGIS in Ionic Angular https://supabase.com/blog/geo-queries-with-postgis-in-ionic-angular Using the PostGIS extension to build a cross-platform application with Ionic Angular. - Wed, 01 Mar 2023 00:00:00 GMT + Wed, 01 Mar 2023 00:00:00 -0700 https://supabase.com/blog/type-constraints-in-65-lines-of-sql Type Constraints in 65 lines of SQL https://supabase.com/blog/type-constraints-in-65-lines-of-sql Creating validated data types in Postgres - Fri, 17 Feb 2023 00:00:00 GMT + Fri, 17 Feb 2023 00:00:00 -0700 https://supabase.com/blog/case-study-happyteams HappyTeams unlocks better performance and reduces cost with Supabase https://supabase.com/blog/case-study-happyteams How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back - Thu, 16 Feb 2023 00:00:00 GMT + Thu, 16 Feb 2023 00:00:00 -0700 https://supabase.com/blog/flutter-real-time-multiplayer-game How to build a real-time multiplayer game with Flutter Flame https://supabase.com/blog/flutter-real-time-multiplayer-game Build a real-time multiplayer game using Flutter, Flame, and Supabase realtime. - Tue, 14 Feb 2023 00:00:00 GMT + Tue, 14 Feb 2023 00:00:00 -0700 https://supabase.com/blog/supabase-beta-january-2023 Supabase Beta January 2023 https://supabase.com/blog/supabase-beta-january-2023 New Postgres extensions, pg_graphql updates, changes to Edge Functions, and more! - Wed, 08 Feb 2023 00:00:00 GMT + Wed, 08 Feb 2023 00:00:00 -0700 https://supabase.com/blog/chatgpt-supabase-docs Supabase Clippy: ChatGPT for Supabase Docs https://supabase.com/blog/chatgpt-supabase-docs Creating a ChatGPT interface for the Supabase documentation. - Tue, 07 Feb 2023 00:00:00 GMT + Tue, 07 Feb 2023 00:00:00 -0700 https://supabase.com/blog/openai-embeddings-postgres-vector Storing OpenAI embeddings in Postgres with pgvector https://supabase.com/blog/openai-embeddings-postgres-vector An example of how to build an AI-powered search engine using OpenAI's embeddings and PostgreSQL. - Mon, 06 Feb 2023 00:00:00 GMT + Mon, 06 Feb 2023 00:00:00 -0700 https://supabase.com/blog/supabase-beta-december-2022 Supabase Beta December 2022 https://supabase.com/blog/supabase-beta-december-2022 This month the Beta Update is a Launch Week 6 Special, where we review the cascade of announcements. - Thu, 05 Jan 2023 00:00:00 GMT + Thu, 05 Jan 2023 00:00:00 -0700 https://supabase.com/blog/launch-week-6-hackathon-winners Launch Week 6 Hackathon Winners https://supabase.com/blog/launch-week-6-hackathon-winners Announcing the winners of the Launch Week 6 Hackathon! - Tue, 03 Jan 2023 00:00:00 GMT + Tue, 03 Jan 2023 00:00:00 -0700 https://supabase.com/blog/custom-domain-names Custom Domain Names https://supabase.com/blog/custom-domain-names Change your Supabase project's domain name to your own domain. - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/launch-week-6-community-day Community Day https://supabase.com/blog/launch-week-6-community-day Wrapping up Launch Week 6 with contributors, partners, and friends. - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/launch-week-6-wrap-up Launch Week 6: Wrap Up https://supabase.com/blog/launch-week-6-wrap-up That's a wrap on Supabase Launch Week Day 6. Here's everything we shipped in one long blog post. - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/new-in-postgres-15 What's new in Postgres 15? https://supabase.com/blog/new-in-postgres-15 Describes the release of Postgres 15, new features and reasons to use it - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/pg-graphql-v1 pg_graphql v1.0 https://supabase.com/blog/pg-graphql-v1 Announcing the v1.0 release of pg_graphql - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/postgres-point-in-time-recovery Point in Time Recovery is now available for Pro projects https://supabase.com/blog/postgres-point-in-time-recovery We're making PITR available for more projects, with a new Dashboard UI that makes it simple to use. - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/postgrest-11-prerelease PostgREST 11 pre-release https://supabase.com/blog/postgrest-11-prerelease Describes new features of PostgREST 11 pre-release - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/vault-now-in-beta Supabase Vault is now in Beta https://supabase.com/blog/vault-now-in-beta A Postgres extension to store encrypted secrets and encrypt data. - Fri, 16 Dec 2022 00:00:00 GMT + Fri, 16 Dec 2022 00:00:00 -0700 https://supabase.com/blog/postgres-foreign-data-wrappers-rust Supabase Wrappers, a Postgres FDW framework written in Rust https://supabase.com/blog/postgres-foreign-data-wrappers-rust A framework for building Postgres Foreign Data Wrappers which connects to Stripe, Firebase, Clickhouse, and more. - Thu, 15 Dec 2022 00:00:00 GMT + Thu, 15 Dec 2022 00:00:00 -0700 https://supabase.com/blog/mfa-auth-via-rls Multi-factor Authentication via Row Level Security Enforcement https://supabase.com/blog/mfa-auth-via-rls MFA Auth with enforcement via RLS - Wed, 14 Dec 2022 00:00:00 GMT + Wed, 14 Dec 2022 00:00:00 -0700 https://supabase.com/blog/storage-image-resizing-smart-cdn Supabase Storage v2: Image resizing and Smart CDN https://supabase.com/blog/storage-image-resizing-smart-cdn We're introducing new features for Supabase Storage: Image resizing and a Smart CDN. - Tue, 13 Dec 2022 00:00:00 GMT + Tue, 13 Dec 2022 00:00:00 -0700 https://supabase.com/blog/new-supabase-docs-built-with-nextjs New Supabase Docs, built with Next.js https://supabase.com/blog/new-supabase-docs-built-with-nextjs We've redesigned our Docs and migrated to Next.js - Mon, 12 Dec 2022 00:00:00 GMT + Mon, 12 Dec 2022 00:00:00 -0700 https://supabase.com/blog/postgres-crdt pg_crdt - an experimental CRDT extension for Postgres https://supabase.com/blog/postgres-crdt Embedding Yjs and Automerge into Postgres for collaborative applications. - Sat, 10 Dec 2022 00:00:00 GMT + Sat, 10 Dec 2022 00:00:00 -0700 https://supabase.com/blog/launch-week-6-hackathon Launch Week 6 Hackathon https://supabase.com/blog/launch-week-6-hackathon Build an Open Source Project, Win $1500 and the Supabase Darkmode Keyboard - Fri, 09 Dec 2022 00:00:00 GMT + Fri, 09 Dec 2022 00:00:00 -0700 https://supabase.com/blog/who-we-hire Who We Hire at Supabase https://supabase.com/blog/who-we-hire Traits we look for to maintain a culture of shipping fast and often - Fri, 09 Dec 2022 00:00:00 GMT + Fri, 09 Dec 2022 00:00:00 -0700 https://supabase.com/blog/supabase-beta-november-2022 Supabase Beta November 2022 https://supabase.com/blog/supabase-beta-november-2022 We are preparing everything for Launch Week 6, but we still had time to ship some goodies this month! - Wed, 07 Dec 2022 00:00:00 GMT + Wed, 07 Dec 2022 00:00:00 -0700 https://supabase.com/blog/the-supabase-content-storm The Supabase Content Storm https://supabase.com/blog/the-supabase-content-storm We worked with +30 content creators to drop a mountain of content simultaneously. - Tue, 06 Dec 2022 00:00:00 GMT + Tue, 06 Dec 2022 00:00:00 -0700 https://supabase.com/blog/sql-or-nosql-both-with-postgresql SQL or NoSQL? Why not use both (with PostgreSQL)? https://supabase.com/blog/sql-or-nosql-both-with-postgresql How to turn Postgres into an easy-to-use NoSQL database that retains all the power of SQL - Thu, 24 Nov 2022 00:00:00 GMT + Thu, 24 Nov 2022 00:00:00 -0700 https://supabase.com/blog/flutter-authorization-with-rls Flutter Authorization with RLS https://supabase.com/blog/flutter-authorization-with-rls Learn how you can secure your Flutter app using Supabase Row Level Security. - Tue, 22 Nov 2022 00:00:00 GMT + Tue, 22 Nov 2022 00:00:00 -0700 https://supabase.com/blog/fetching-and-caching-supabase-data-in-next-js-server-components Fetching and caching Supabase data in Next.js 13 Server Components https://supabase.com/blog/fetching-and-caching-supabase-data-in-next-js-server-components Next.js 13 introduces new data fetching and caching methods to enable React Server Components and Suspense. - Thu, 17 Nov 2022 00:00:00 GMT + Thu, 17 Nov 2022 00:00:00 -0700 https://supabase.com/blog/authentication-in-ionic-angular Authentication in Ionic Angular with Supabase https://supabase.com/blog/authentication-in-ionic-angular Learn how to build an Ionic Angular app with authentication, Row Level Security, and Magic Link auth. - Tue, 08 Nov 2022 00:00:00 GMT + Tue, 08 Nov 2022 00:00:00 -0700 https://supabase.com/blog/supabase-beta-update-october-2022 Supabase Beta October 2022 https://supabase.com/blog/supabase-beta-update-october-2022 New SDKs, quickstarts, Functions tricks, and more. But, more importantly, Launch Week 6️ has a date! - Wed, 02 Nov 2022 00:00:00 GMT + Wed, 02 Nov 2022 00:00:00 -0700 https://supabase.com/blog/postgresql-commitfest What is PostgreSQL commitfest and how to contribute https://supabase.com/blog/postgresql-commitfest A time-tested method for contributing to the core Postgres code - Thu, 27 Oct 2022 00:00:00 GMT + Thu, 27 Oct 2022 00:00:00 -0700 https://supabase.com/blog/supabase-flutter-sdk-v1-released supabase-flutter v1 Released https://supabase.com/blog/supabase-flutter-sdk-v1-released We've released supabase-flutter v1. More intuitive way of accessing Supabase from your Flutter application. - Fri, 21 Oct 2022 00:00:00 GMT + Fri, 21 Oct 2022 00:00:00 -0700 https://supabase.com/blog/supabase-js-v2-released supabase-js v2 Released https://supabase.com/blog/supabase-js-v2-released We've released supabase-js v2. Updated examples, quickstarts, and an improved experience. - Thu, 20 Oct 2022 00:00:00 GMT + Thu, 20 Oct 2022 00:00:00 -0700 https://supabase.com/blog/postgres-full-text-search-vs-the-rest Postgres Full Text Search vs the rest https://supabase.com/blog/postgres-full-text-search-vs-the-rest Comparing one of the most popular Postgres features against alternatives - Fri, 14 Oct 2022 00:00:00 GMT + Fri, 14 Oct 2022 00:00:00 -0700 https://supabase.com/blog/supabase-beta-update-september-2022 Supabase Beta September 2022 https://supabase.com/blog/supabase-beta-update-september-2022 We were too focused on clearing out the backlog so we didn't ship anything new last month... or did we?! - Wed, 05 Oct 2022 00:00:00 GMT + Wed, 05 Oct 2022 00:00:00 -0700 https://supabase.com/blog/postgres-wasm Postgres WASM by Snaplet and Supabase https://supabase.com/blog/postgres-wasm We're open sourcing postgres-wasm, a PostgresQL server that runs inside a browser, with our friends at Snaplet. - Mon, 03 Oct 2022 00:00:00 GMT + Mon, 03 Oct 2022 00:00:00 -0700 https://supabase.com/blog/choosing-a-postgres-primary-key Choosing a Postgres Primary Key https://supabase.com/blog/choosing-a-postgres-primary-key Turns out the question of which identifier to use as a Primary Key is complicated -- we're going to dive into some of the complexity and inherent trade-offs, and figure things out - Thu, 08 Sep 2022 00:00:00 GMT + Thu, 08 Sep 2022 00:00:00 -0700 https://supabase.com/blog/supabase-beta-update-august-2022 Supabase Beta August 2022 https://supabase.com/blog/supabase-beta-update-august-2022 Launch Week Special. See everything we shipped, plus winners of the Hackathon and the extended Community Highlights - Wed, 07 Sep 2022 00:00:00 GMT + Wed, 07 Sep 2022 00:00:00 -0700 https://supabase.com/blog/launch-week-5-hackathon-winners Launch Week 5 Hackathon Winners https://supabase.com/blog/launch-week-5-hackathon-winners Announcing the winners of the Launch Week 5 Hackathon! - Thu, 25 Aug 2022 00:00:00 GMT + Thu, 25 Aug 2022 00:00:00 -0700 https://supabase.com/blog/building-a-realtime-trello-board-with-supabase-and-angular Building a Realtime Trello Board with Supabase and Angular https://supabase.com/blog/building-a-realtime-trello-board-with-supabase-and-angular Go beyond the hello world example with this real world project. - Wed, 24 Aug 2022 00:00:00 GMT + Wed, 24 Aug 2022 00:00:00 -0700 https://supabase.com/blog/launch-week-5-community-day Community Day https://supabase.com/blog/launch-week-5-community-day Wrapping up Launch Week 5 with contributors, partners, and friends. - Fri, 19 Aug 2022 00:00:00 GMT + Fri, 19 Aug 2022 00:00:00 -0700 https://supabase.com/blog/launch-week-5-one-more-thing One more thing https://supabase.com/blog/launch-week-5-one-more-thing Let's be honest, it's never just one more thing. - Fri, 19 Aug 2022 00:00:00 GMT + Fri, 19 Aug 2022 00:00:00 -0700 https://supabase.com/blog/pg-jsonschema-a-postgres-extension-for-json-validation pg_jsonschema: JSON Schema support for Postgres https://supabase.com/blog/pg-jsonschema-a-postgres-extension-for-json-validation Today we're releasing pg_jsonschema, a Postgres extension for JSON validation. - Fri, 19 Aug 2022 00:00:00 GMT + Fri, 19 Aug 2022 00:00:00 -0700 https://supabase.com/blog/postgrest-v10 PostgREST v10: EXPLAIN and Improved Relationship Detection https://supabase.com/blog/postgrest-v10 Today, PostgREST 10 was released. Let's take a look at some of the new features that go hand in hand with supabase-js v2. - Fri, 19 Aug 2022 00:00:00 GMT + Fri, 19 Aug 2022 00:00:00 -0700 https://supabase.com/blog/supabase-vault Supabase Vault https://supabase.com/blog/supabase-vault Today we're announcing Vault, a Postgres extension for managing secrets and encryption inside your database. - Fri, 19 Aug 2022 00:00:00 GMT + Fri, 19 Aug 2022 00:00:00 -0700 https://supabase.com/blog/supabase-realtime-multiplayer-general-availability Realtime: Multiplayer Edition https://supabase.com/blog/supabase-realtime-multiplayer-general-availability Announcing the general availability of Realtime's Broadcast and Presence. - Thu, 18 Aug 2022 00:00:00 GMT + Thu, 18 Aug 2022 00:00:00 -0700 https://supabase.com/blog/supabase-soc2 Supabase is SOC2 compliant https://supabase.com/blog/supabase-soc2 Supabase is now SOC2 compliant. Learn how we got here and what it means for our customers. - Wed, 17 Aug 2022 00:00:00 GMT + Wed, 17 Aug 2022 00:00:00 -0700 https://supabase.com/blog/supabase-js-v2 supabase-js v2 https://supabase.com/blog/supabase-js-v2 A look at supabase-js v2, which brings type support and focuses on quality-of-life improvements for developers. - Tue, 16 Aug 2022 00:00:00 GMT + Tue, 16 Aug 2022 00:00:00 -0700 https://supabase.com/blog/supabase-cli-v1-and-admin-api-beta Supabase CLI v1 and Management API Beta https://supabase.com/blog/supabase-cli-v1-and-admin-api-beta We are moving Supabase CLI v1 out of beta, and releasing Management API beta. - Mon, 15 Aug 2022 00:00:00 GMT + Mon, 15 Aug 2022 00:00:00 -0700 https://supabase.com/blog/supabase-series-b Supabase Series B https://supabase.com/blog/supabase-series-b Supabase raised $80M in May, bringing our total funding to $116M. - Fri, 12 Aug 2022 00:00:00 GMT + Fri, 12 Aug 2022 00:00:00 -0700 https://supabase.com/blog/launch-week-5-hackathon Launch Week 5 Hackathon https://supabase.com/blog/launch-week-5-hackathon Build to win $1500 - Friday 12th to Monday 21st August 2022 - Wed, 10 Aug 2022 00:00:00 GMT + Wed, 10 Aug 2022 00:00:00 -0700 https://supabase.com/blog/slack-consolidate-slackbot-to-consolidate-messages Slack Consolidate: a slackbot built with Python and Supabase https://supabase.com/blog/slack-consolidate-slackbot-to-consolidate-messages A slackbot to consolidate messages from different channels using Supabase, Slack SDK and Python - Tue, 09 Aug 2022 00:00:00 GMT + Tue, 09 Aug 2022 00:00:00 -0700 https://supabase.com/blog/supabase-beta-update-july-2022 Supabase Beta July 2022 https://supabase.com/blog/supabase-beta-update-july-2022 Launch Week Golden Tickets, Flutter SDK 1.0 Developer Preview and more... - Wed, 03 Aug 2022 00:00:00 GMT + Wed, 03 Aug 2022 00:00:00 -0700 https://supabase.com/blog/supabase-flutter-sdk-1-developer-preview Supabase Flutter SDK 1.0 Developer Preview https://supabase.com/blog/supabase-flutter-sdk-1-developer-preview Supabase Flutter SDK is getting a major update and we need your help making it better. - Tue, 02 Aug 2022 00:00:00 GMT + Tue, 02 Aug 2022 00:00:00 -0700 https://supabase.com/blog/seen-by-in-postgresql Implementing "seen by" functionality with Postgres https://supabase.com/blog/seen-by-in-postgresql Different approaches for tracking visitor counts with PostgreSQL. - Mon, 18 Jul 2022 00:00:00 GMT + Mon, 18 Jul 2022 00:00:00 -0700 https://supabase.com/blog/supabase-auth-helpers-with-sveltekit-support Revamped Auth Helpers for Supabase (with SvelteKit support) https://supabase.com/blog/supabase-auth-helpers-with-sveltekit-support Supabase Auth Helpers now have improved developer experience, Sveltekit support, and more. - Wed, 13 Jul 2022 00:00:00 GMT + Wed, 13 Jul 2022 00:00:00 -0700 https://supabase.com/blog/beta-update-june-2022 Supabase Beta June 2022 https://supabase.com/blog/beta-update-june-2022 Auth Helpers, Unlimited Free Projects, CLI and more... - Wed, 06 Jul 2022 00:00:00 GMT + Wed, 06 Jul 2022 00:00:00 -0700 https://supabase.com/blog/flutter-tutorial-building-a-chat-app Flutter Tutorial: building a Flutter chat app https://supabase.com/blog/flutter-tutorial-building-a-chat-app Learn how to build a Flutter chat app with open source and scalable backend (inc. auth, realtime, database, and more). - Thu, 30 Jun 2022 00:00:00 GMT + Thu, 30 Jun 2022 00:00:00 -0700 https://supabase.com/blog/visualizing-supabase-data-using-metabase Visualizing Supabase Data using Metabase https://supabase.com/blog/visualizing-supabase-data-using-metabase How to create different kinds of charts out of data stored in Supabase using Metabase. - Wed, 29 Jun 2022 00:00:00 GMT + Wed, 29 Jun 2022 00:00:00 -0700 https://supabase.com/blog/partial-postgresql-data-dumps-with-rls Partial data dumps using Postgres Row Level Security https://supabase.com/blog/partial-postgresql-data-dumps-with-rls Using RLS to create seed files for local PostgreSQL testing. - Tue, 28 Jun 2022 00:00:00 GMT + Tue, 28 Jun 2022 00:00:00 -0700 https://supabase.com/blog/loading-data-supabase-python Python data loading with Supabase https://supabase.com/blog/loading-data-supabase-python An example of how to load data into Supabase using supabase-py - Fri, 17 Jun 2022 00:00:00 GMT + Fri, 17 Jun 2022 00:00:00 -0700 https://supabase.com/blog/beta-update-may-2022 Supabase Beta May 2022 https://supabase.com/blog/beta-update-may-2022 Product and community updates including wildcard auth redirects, edge functions with webhooks, and Prometheus endpoints for everybody. - Wed, 01 Jun 2022 00:00:00 GMT + Wed, 01 Jun 2022 00:00:00 -0700 https://supabase.com/blog/how-supabase-accelerates-development-of-all-pull-together How Mike Lyndon is using Supabase to accelerate development of AllPullTogether https://supabase.com/blog/how-supabase-accelerates-development-of-all-pull-together Mike Lyndon is learning web development as he builds AllPullTogether, and Supabase is helping him accelerate what he can accomplish. - Thu, 26 May 2022 00:00:00 GMT + Thu, 26 May 2022 00:00:00 -0700 https://supabase.com/blog/partner-gallery-works-with-supabase Works With Supabase - announcing our Partner Gallery https://supabase.com/blog/partner-gallery-works-with-supabase Introducing our Partner Gallery - open source and made with Supabase. - Wed, 20 Apr 2022 00:00:00 GMT + Wed, 20 Apr 2022 00:00:00 -0700 https://supabase.com/blog/bring-the-func-hackathon-winners Bring the Func Hackathon Winners 2022 https://supabase.com/blog/bring-the-func-hackathon-winners Celebrating many amazing OSS Hackathon projects using GraphQL and Edge Functions. - Mon, 18 Apr 2022 00:00:00 GMT + Mon, 18 Apr 2022 00:00:00 -0700 https://supabase.com/blog/beta-update-march-2022 Supabase Beta March 2022 https://supabase.com/blog/beta-update-march-2022 Functions, GraphQL, and much more. - Fri, 15 Apr 2022 00:00:00 GMT + Fri, 15 Apr 2022 00:00:00 -0700 https://supabase.com/blog/supabase-realtime-with-multiplayer-features Supabase Realtime, with Multiplayer Features https://supabase.com/blog/supabase-realtime-with-multiplayer-features Today we're announced Realtime, with multiplayer features. Realtime enables broadcast, presence, and listening to database changes delivered over WebSockets. - Fri, 01 Apr 2022 00:00:00 GMT + Fri, 01 Apr 2022 00:00:00 -0700 https://supabase.com/blog/hackathon-bring-the-func Hackathon: Bring the Func(🕺) https://supabase.com/blog/hackathon-bring-the-func Build open-source projects with our latest features, win limited edition swag and plant a tree! - Fri, 01 Apr 2022 00:00:00 GMT + Fri, 01 Apr 2022 00:00:00 -0700 https://supabase.com/blog/supabrew Supabrew - Never Code Thirsty https://supabase.com/blog/supabrew A light and refreshing non-alcoholic beer for devs. - Fri, 01 Apr 2022 00:00:00 GMT + Fri, 01 Apr 2022 00:00:00 -0700 https://supabase.com/blog/supabase-edge-functions Edge Functions are now available in Supabase https://supabase.com/blog/supabase-edge-functions Today we're launching Edge Functions. Edge Functions let you execute Typescript code close to your users, no matter where they're located. - Thu, 31 Mar 2022 00:00:00 GMT + Thu, 31 Mar 2022 00:00:00 -0700 https://supabase.com/blog/supabase-enterprise Introducing Supabase Enterprise https://supabase.com/blog/supabase-enterprise Today we are releasing Supabase Enterprise, a suite of features to scale your project. - Wed, 30 Mar 2022 00:00:00 GMT + Wed, 30 Mar 2022 00:00:00 -0700 https://supabase.com/blog/graphql-now-available GraphQL is now available in Supabase https://supabase.com/blog/graphql-now-available GraphQL support is now in general availability on the Supabase platform via our open source PostgreSQL extension, pg_graphql. - Tue, 29 Mar 2022 00:00:00 GMT + Tue, 29 Mar 2022 00:00:00 -0700 https://supabase.com/blog/community-day-lw4 Community Day https://supabase.com/blog/community-day-lw4 Kicking off Launch Week 4 with contributors, partners, and friends. - Mon, 28 Mar 2022 00:00:00 GMT + Mon, 28 Mar 2022 00:00:00 -0700 https://supabase.com/blog/should-i-open-source-my-company Should I Open Source my Company? https://supabase.com/blog/should-i-open-source-my-company The unexpected upsides of building in public - Fri, 25 Mar 2022 00:00:00 GMT + Fri, 25 Mar 2022 00:00:00 -0700 https://supabase.com/blog/supabase-launch-week-four Supabase Launch Week 4 https://supabase.com/blog/supabase-launch-week-four Launch Week 4: One new feature every day for an entire week. Starting Monday 28th March. - Fri, 25 Mar 2022 00:00:00 GMT + Fri, 25 Mar 2022 00:00:00 -0700 https://supabase.com/blog/postgres-audit Postgres Auditing in 150 lines of SQL https://supabase.com/blog/postgres-audit PostgreSQL has a robust set of features which we can leverage to create a generic auditing solution in 150 lines of SQL. - Tue, 08 Mar 2022 00:00:00 GMT + Tue, 08 Mar 2022 00:00:00 -0700 https://supabase.com/blog/supabase-beta-january-2022 Supabase Beta January 2022 https://supabase.com/blog/supabase-beta-january-2022 New auth providers, SMS providers, and new videos. - Tue, 22 Feb 2022 00:00:00 GMT + Tue, 22 Feb 2022 00:00:00 -0700 https://supabase.com/blog/product-hunt-golden-kitty-awards-2021 Golden Kitty Awards Ceremony Watch Party with Supabase https://supabase.com/blog/product-hunt-golden-kitty-awards-2021 Hang out with us while watching the Product Hunt Golden Kitty Awards Ceremony - Thu, 20 Jan 2022 00:00:00 GMT + Thu, 20 Jan 2022 00:00:00 -0700 https://supabase.com/blog/supabase-beta-december-2021 Supabase Beta December 2021 https://supabase.com/blog/supabase-beta-december-2021 New crypto extension, Postgres videos, and a bunch of cool integrations. - Thu, 20 Jan 2022 00:00:00 GMT + Thu, 20 Jan 2022 00:00:00 -0700 https://supabase.com/blog/holiday-hackdays-winners-2021 Holiday Hackdays Winners 2021 https://supabase.com/blog/holiday-hackdays-winners-2021 Celebrating many amazing projects submitted to our Holiday Hackdays Hackathon. - Fri, 17 Dec 2021 00:00:00 GMT + Fri, 17 Dec 2021 00:00:00 -0700 https://supabase.com/blog/beta-november-2021-launch-week-recap Supabase Beta November 2021: Launch Week Recap https://supabase.com/blog/beta-november-2021-launch-week-recap We wrapped up November with Supabase's third Launch Week. Here's all the awesome stuff that got shipped ... - Wed, 15 Dec 2021 00:00:00 GMT + Wed, 15 Dec 2021 00:00:00 -0700 https://supabase.com/blog/launch-week-three-friday-five-more-things Five more things https://supabase.com/blog/launch-week-three-friday-five-more-things It's never just one more thing! - Fri, 03 Dec 2021 00:00:00 GMT + Fri, 03 Dec 2021 00:00:00 -0700 https://supabase.com/blog/pg-graphql pg_graphql: A GraphQL extension for PostgreSQL https://supabase.com/blog/pg-graphql GraphQL support is in development for PostgreSQL + Supabase. - Fri, 03 Dec 2021 00:00:00 GMT + Fri, 03 Dec 2021 00:00:00 -0700 https://supabase.com/blog/supabase-holiday-hackdays-hackathon Kicking off the Holiday Hackdays https://supabase.com/blog/supabase-holiday-hackdays-hackathon Build cool stuff and celebrate open-source software with us during the Holiday Hackdays! - Fri, 03 Dec 2021 00:00:00 GMT + Fri, 03 Dec 2021 00:00:00 -0700 https://supabase.com/blog/supabase-acquires-logflare Supabase acquires Logflare https://supabase.com/blog/supabase-acquires-logflare Today, we're ecstatic to announce that Logflare is joining Supabase. - Thu, 02 Dec 2021 00:00:00 GMT + Thu, 02 Dec 2021 00:00:00 -0700 https://supabase.com/blog/realtime-row-level-security-in-postgresql Realtime Postgres RLS now available on Supabase https://supabase.com/blog/realtime-row-level-security-in-postgresql Realtime database changes are now broadcast to authenticated users, respecting the same PostgreSQL policies that you use for Row Level Security. - Wed, 01 Dec 2021 00:00:00 GMT + Wed, 01 Dec 2021 00:00:00 -0700 https://supabase.com/blog/supabase-studio Supabase Studio https://supabase.com/blog/supabase-studio The same Dashboard that you're using on our Platform is now available for local development and Self-Hosting. - Tue, 30 Nov 2021 00:00:00 GMT + Tue, 30 Nov 2021 00:00:00 -0700 https://supabase.com/blog/community-day-lw3 Community Day https://supabase.com/blog/community-day-lw3 Kicking off launch week by highlighting the communities around Supabase. - Mon, 29 Nov 2021 00:00:00 GMT + Mon, 29 Nov 2021 00:00:00 -0700 https://supabase.com/blog/whats-new-in-postgres-14 New in PostgreSQL 14: What every developer should know https://supabase.com/blog/whats-new-in-postgres-14 A quick look at some new features and functionality in PostgreSQL 14. - Sun, 28 Nov 2021 00:00:00 GMT + Sun, 28 Nov 2021 00:00:00 -0700 https://supabase.com/blog/postgrest-9 PostgREST 9 https://supabase.com/blog/postgrest-9 New features and updates in PostgREST version 9. - Sat, 27 Nov 2021 00:00:00 GMT + Sat, 27 Nov 2021 00:00:00 -0700 https://supabase.com/blog/supabase-how-we-launch How we launch at Supabase https://supabase.com/blog/supabase-how-we-launch The history and methodology of Supabase Launch Week. - Fri, 26 Nov 2021 00:00:00 GMT + Fri, 26 Nov 2021 00:00:00 -0700 https://supabase.com/blog/supabase-launch-week-the-trilogy Supabase Launch Week III: Holiday Special https://supabase.com/blog/supabase-launch-week-the-trilogy Tis the season to be shipping. - Fri, 26 Nov 2021 00:00:00 GMT + Fri, 26 Nov 2021 00:00:00 -0700 https://supabase.com/blog/supabase-beta-october-2021 Supabase Beta October 2021 https://supabase.com/blog/supabase-beta-october-2021 Three new Auth providers, multi-schema support, and we're gearing up for another Launch Week. - Sun, 07 Nov 2021 00:00:00 GMT + Sun, 07 Nov 2021 00:00:00 -0700 https://supabase.com/blog/supabase-series-a Supabase $30m Series A https://supabase.com/blog/supabase-series-a Supabase just raised $30M, bringing our total funding to $36M. - Thu, 28 Oct 2021 00:00:00 GMT + Thu, 28 Oct 2021 00:00:00 -0700 https://supabase.com/blog/replenysh-time-to-value-in-less-than-24-hours Replenysh uses Supabase to implement OTP in less than 24-hours https://supabase.com/blog/replenysh-time-to-value-in-less-than-24-hours Learn how Replenysh uses Supabase to power the circular economy, redefining how brands interact with their customers and products. - Tue, 19 Oct 2021 00:00:00 GMT + Tue, 19 Oct 2021 00:00:00 -0700 https://supabase.com/blog/hacktoberfest-hackathon-winners-2021 Hacktoberfest Hackathon Winners 2021 https://supabase.com/blog/hacktoberfest-hackathon-winners-2021 Celebrating many amazing projects submitted to our Hacktoberfest Hackathon. - Thu, 14 Oct 2021 00:00:00 GMT + Thu, 14 Oct 2021 00:00:00 -0700 https://supabase.com/blog/supabase-beta-sept-2021 Supabase Beta Sept 2021 https://supabase.com/blog/supabase-beta-sept-2021 Hackathon, Aborting request, UI updates, and now Hiring. - Mon, 04 Oct 2021 00:00:00 GMT + Mon, 04 Oct 2021 00:00:00 -0700 https://supabase.com/blog/supabase-hacktoberfest-hackathon-2021 Supabase Hacktoberfest Hackathon 2021 https://supabase.com/blog/supabase-hacktoberfest-hackathon-2021 We're running another Supabase Hackathon during Hacktoberfest! - Tue, 28 Sep 2021 00:00:00 GMT + Tue, 28 Sep 2021 00:00:00 -0700 https://supabase.com/blog/supabase-beta-august-2021 Supabase Beta August 2021 https://supabase.com/blog/supabase-beta-august-2021 Fundraising, Realtime Security, custom SMS templates, and deployments in South Korea. - Fri, 10 Sep 2021 00:00:00 GMT + Fri, 10 Sep 2021 00:00:00 -0700 https://supabase.com/blog/supabase-beta-july-2021 Supabase Beta July 2021 https://supabase.com/blog/supabase-beta-july-2021 Discord Logins, Vercel Integration, Full text search, and OAuth guides. - Thu, 12 Aug 2021 00:00:00 GMT + Thu, 12 Aug 2021 00:00:00 -0700 https://supabase.com/blog/hackathon-winners Open Source Hackathon Winners https://supabase.com/blog/hackathon-winners Let the medal ceremony begin for the best projects submitted during the Supabase Hackathon. - Mon, 09 Aug 2021 00:00:00 GMT + Mon, 09 Aug 2021 00:00:00 -0700 https://supabase.com/blog/1-the-supabase-hackathon The Supabase Hackathon https://supabase.com/blog/1-the-supabase-hackathon A whole week of Hacking for Fun and Prizes. - Fri, 30 Jul 2021 00:00:00 GMT + Fri, 30 Jul 2021 00:00:00 -0700 https://supabase.com/blog/supabase-functions-updates Updates for Supabase Functions https://supabase.com/blog/supabase-functions-updates The question on everyone's mind - are we launching Supabase Functions? Well, it's complicated. - Fri, 30 Jul 2021 00:00:00 GMT + Fri, 30 Jul 2021 00:00:00 -0700 https://supabase.com/blog/supabase-swag-store Supabase Swag Store https://supabase.com/blog/supabase-swag-store Today we are officially launching the Supabase Swag Store. - Fri, 30 Jul 2021 00:00:00 GMT + Fri, 30 Jul 2021 00:00:00 -0700 https://supabase.com/blog/supabase-reports-and-metrics Supabase Reports and Metrics https://supabase.com/blog/supabase-reports-and-metrics We're exposing a full set of metrics in your projects, so that you can build better (and faster) products for your users. - Thu, 29 Jul 2021 00:00:00 GMT + Thu, 29 Jul 2021 00:00:00 -0700 https://supabase.com/blog/mobbin-supabase-200000-users Mobbin uses Supabase to authenticate 200,000 users https://supabase.com/blog/mobbin-supabase-200000-users Learn how Mobbin migrated 200,000 users from Firebase for a better authentication experience. - Wed, 28 Jul 2021 00:00:00 GMT + Wed, 28 Jul 2021 00:00:00 -0700 https://supabase.com/blog/supabase-auth-passwordless-sms-login Supabase Auth v2: Phone Auth now available https://supabase.com/blog/supabase-auth-passwordless-sms-login Phone Auth is available today on all new and existing Supabase projects. - Wed, 28 Jul 2021 00:00:00 GMT + Wed, 28 Jul 2021 00:00:00 -0700 https://supabase.com/blog/spot-flutter-with-postgres Spot: a video sharing app built with Flutter https://supabase.com/blog/spot-flutter-with-postgres Spot is a geolocation-based video-sharing app with some social networking features. - Tue, 27 Jul 2021 00:00:00 GMT + Tue, 27 Jul 2021 00:00:00 -0700 https://supabase.com/blog/storage-beta Supabase Storage now in Beta https://supabase.com/blog/storage-beta Supabase Storage moves into Beta. - Tue, 27 Jul 2021 00:00:00 GMT + Tue, 27 Jul 2021 00:00:00 -0700 https://supabase.com/blog/epsilon3-self-hosting Epsilon3 Self-Host Supabase To Revolutionize Space Operations https://supabase.com/blog/epsilon3-self-hosting Learn how the team at Epsilon3 use Supabase to help teams execute secure and reliable operations in an industry that project spend runs into the billions. - Mon, 26 Jul 2021 00:00:00 GMT + Mon, 26 Jul 2021 00:00:00 -0700 https://supabase.com/blog/supabase-community-day Supabase Community Day https://supabase.com/blog/supabase-community-day Community Day - Mon, 26 Jul 2021 00:00:00 GMT + Mon, 26 Jul 2021 00:00:00 -0700 https://supabase.com/blog/supabase-postgres-13 Supabase is now on Postgres 13.3 https://supabase.com/blog/supabase-postgres-13 From today, new Supabase projects will be on a version of Supabase Postgres that runs on Postgres 13.3. - Mon, 26 Jul 2021 00:00:00 GMT + Mon, 26 Jul 2021 00:00:00 -0700 https://supabase.com/blog/supabase-launch-week-sql Supabase Launch Week II: The SQL https://supabase.com/blog/supabase-launch-week-sql Five days of Supabase. Again. - Thu, 22 Jul 2021 00:00:00 GMT + Thu, 22 Jul 2021 00:00:00 -0700 https://supabase.com/blog/roles-postgres-hooks Protecting reserved roles with PostgreSQL Hooks https://supabase.com/blog/roles-postgres-hooks Using Postgres Hooks to protect functionality in your Postgres database. - Fri, 02 Jul 2021 00:00:00 GMT + Fri, 02 Jul 2021 00:00:00 -0700 https://supabase.com/blog/supabase-beta-may-2021 Supabase Beta May 2021 https://supabase.com/blog/supabase-beta-may-2021 Apple &amp; Twitter Logins, Supabase Grid, Go &amp; Swift Libraries. - Wed, 02 Jun 2021 00:00:00 GMT + Wed, 02 Jun 2021 00:00:00 -0700 https://supabase.com/blog/supabase-beta-june-2021 Supabase Beta June 2021 https://supabase.com/blog/supabase-beta-june-2021 Discord Logins, Vercel Integration, Full text search, and OAuth guides. - Wed, 02 Jun 2021 00:00:00 GMT + Wed, 02 Jun 2021 00:00:00 -0700 https://supabase.com/blog/supabase-beta-april-2021 Supabase Beta April 2021 https://supabase.com/blog/supabase-beta-april-2021 Supabase "gardening" - stability, security, and community support. - Wed, 05 May 2021 00:00:00 GMT + Wed, 05 May 2021 00:00:00 -0700 https://supabase.com/blog/supabase-beta-march-2021 Supabase Beta March 2021 https://supabase.com/blog/supabase-beta-march-2021 Launch week, Storage, Supabase CLI, Connection Pooling, Supabase UI, and Pricing. - Tue, 06 Apr 2021 00:00:00 GMT + Tue, 06 Apr 2021 00:00:00 -0700 https://supabase.com/blog/supabase-dot-com Supabase Dot Com https://supabase.com/blog/supabase-dot-com The Supabase Domain name is changing. - Fri, 02 Apr 2021 00:00:00 GMT + Fri, 02 Apr 2021 00:00:00 -0700 https://supabase.com/blog/supabase-pgbouncer PgBouncer is now available in Supabase https://supabase.com/blog/supabase-pgbouncer Better support for Serverless and Postgres. - Fri, 02 Apr 2021 00:00:00 GMT + Fri, 02 Apr 2021 00:00:00 -0700 https://supabase.com/blog/supabase-workflows Workflows are coming to Supabase https://supabase.com/blog/supabase-workflows Functions are great, but you know what's better? - Fri, 02 Apr 2021 00:00:00 GMT + Fri, 02 Apr 2021 00:00:00 -0700 https://supabase.com/blog/supabase-nft-marketplace Supabase Launches NFT Marketplace https://supabase.com/blog/supabase-nft-marketplace A fully encrypted NFT platform to protect and transact your digital assets - Thu, 01 Apr 2021 00:00:00 GMT + Thu, 01 Apr 2021 00:00:00 -0700 https://supabase.com/blog/supabase-cli Supabase CLI https://supabase.com/blog/supabase-cli Local development, database migrations, and self-hosting. - Wed, 31 Mar 2021 00:00:00 GMT + Wed, 31 Mar 2021 00:00:00 -0700 https://supabase.com/blog/supabase-storage Storage is now available in Supabase https://supabase.com/blog/supabase-storage Launching Supabase Storage and how you can use it in your apps - Tue, 30 Mar 2021 00:00:00 GMT + Tue, 30 Mar 2021 00:00:00 -0700 https://supabase.com/blog/pricing Supabase Beta Pricing https://supabase.com/blog/pricing Supabase launches Beta pricing structure - Mon, 29 Mar 2021 00:00:00 GMT + Mon, 29 Mar 2021 00:00:00 -0700 https://supabase.com/blog/angels-of-supabase Angels of Supabase https://supabase.com/blog/angels-of-supabase Meet the investors of Supabase. - Thu, 25 Mar 2021 00:00:00 GMT + Thu, 25 Mar 2021 00:00:00 -0700 https://supabase.com/blog/launch-week Launch week https://supabase.com/blog/launch-week Five days of Supabase. - Thu, 25 Mar 2021 00:00:00 GMT + Thu, 25 Mar 2021 00:00:00 -0700 https://supabase.com/blog/In-The-Loop Developers stay up to date with intheloop.dev https://supabase.com/blog/In-The-Loop Learn why Kevin is building intheloop.dev with Supabase - Mon, 22 Mar 2021 00:00:00 GMT + Mon, 22 Mar 2021 00:00:00 -0700 https://supabase.com/blog/using-supabase-replit Using Supabase in Replit https://supabase.com/blog/using-supabase-replit Free hosted relational database from within your node.js repl - Thu, 11 Mar 2021 00:00:00 GMT + Thu, 11 Mar 2021 00:00:00 -0700 https://supabase.com/blog/toad-a-link-shortener-with-simple-apis-for-low-coders Toad, a link shortener with simple APIs for low-coders https://supabase.com/blog/toad-a-link-shortener-with-simple-apis-for-low-coders An easy-to-use link shortening tool with simple APIs - Mon, 08 Mar 2021 00:00:00 GMT + Mon, 08 Mar 2021 00:00:00 -0700 https://supabase.com/blog/postgres-as-a-cron-server Postgres as a CRON Server https://supabase.com/blog/postgres-as-a-cron-server Running repetitive tasks with your Postgres database. - Fri, 05 Mar 2021 00:00:00 GMT + Fri, 05 Mar 2021 00:00:00 -0700 https://supabase.com/blog/supabase-beta-february-2021 Supabase Beta February 2021 https://supabase.com/blog/supabase-beta-february-2021 One year of building. - Tue, 02 Mar 2021 00:00:00 GMT + Tue, 02 Mar 2021 00:00:00 -0700 https://supabase.com/blog/cracking-postgres-interview Cracking PostgreSQL Interview Questions https://supabase.com/blog/cracking-postgres-interview Understand the top PostgreSQL Interview Questions - Sat, 27 Feb 2021 00:00:00 GMT + Sat, 27 Feb 2021 00:00:00 -0700 https://supabase.com/blog/case-study-roboflow Roboflow.com choose Supabase to power Paint.wtf leaderboard https://supabase.com/blog/case-study-roboflow Learn how Roboflow.com used Supabase to build their Paint.wtf leaderboard - Tue, 09 Feb 2021 00:00:00 GMT + Tue, 09 Feb 2021 00:00:00 -0700 https://supabase.com/blog/supabase-beta-january-2021 Supabase Beta January 2021 https://supabase.com/blog/supabase-beta-january-2021 Eleven months of building. - Tue, 02 Feb 2021 00:00:00 GMT + Tue, 02 Feb 2021 00:00:00 -0700 https://supabase.com/blog/supabase-beta-december-2020 Supabase Beta December 2020 https://supabase.com/blog/supabase-beta-december-2020 Ten months of building. - Sat, 02 Jan 2021 00:00:00 GMT + Sat, 02 Jan 2021 00:00:00 -0700 https://supabase.com/blog/supabase-dashboard-performance Making the Supabase Dashboard Supa-fast https://supabase.com/blog/supabase-dashboard-performance Improving the performance of the Supabase dashboard - Sun, 13 Dec 2020 00:00:00 GMT + Sun, 13 Dec 2020 00:00:00 -0700 https://supabase.com/blog/case-study-monitoro Monitoro Built a Web Crawler Handling Millions of API Requests https://supabase.com/blog/case-study-monitoro See how Monitoro built an automated scraping platform using Supabase. - Wed, 02 Dec 2020 00:00:00 GMT + Wed, 02 Dec 2020 00:00:00 -0700 https://supabase.com/blog/case-study-tayfa TAYFA Built a No-Code Website Builder in Seven Days https://supabase.com/blog/case-study-tayfa See how Tayfa went from idea to paying customer in less than 30 days. - Wed, 02 Dec 2020 00:00:00 GMT + Wed, 02 Dec 2020 00:00:00 -0700 https://supabase.com/blog/case-study-xendit Xendit Built a Counter-Fraud Watchlist for the Fintech Industry https://supabase.com/blog/case-study-xendit See how Xendit use Supabase to build a full-text search engine. - Wed, 02 Dec 2020 00:00:00 GMT + Wed, 02 Dec 2020 00:00:00 -0700 https://supabase.com/blog/supabase-striveschool Supabase Partners With Strive School To Help Teach Open Source https://supabase.com/blog/supabase-striveschool Supabase Partners With Strive School To Help Teach Open Source To The Next Generation Of Developers - Wed, 02 Dec 2020 00:00:00 GMT + Wed, 02 Dec 2020 00:00:00 -0700 https://supabase.com/blog/supabase-alpha-november-2020 Supabase Alpha November 2020 https://supabase.com/blog/supabase-alpha-november-2020 Nine months of building. - Tue, 01 Dec 2020 00:00:00 GMT + Tue, 01 Dec 2020 00:00:00 -0700 https://supabase.com/blog/postgresql-views Postgres Views https://supabase.com/blog/postgresql-views Creating and using a view in PostgreSQL. - Wed, 18 Nov 2020 00:00:00 GMT + Wed, 18 Nov 2020 00:00:00 -0700 https://supabase.com/blog/supabase-alpha-october-2020 Supabase Alpha October 2020 https://supabase.com/blog/supabase-alpha-october-2020 Eight months of building. - Mon, 02 Nov 2020 00:00:00 GMT + Mon, 02 Nov 2020 00:00:00 -0700 https://supabase.com/blog/improved-dx Supabase.js 1.0 https://supabase.com/blog/improved-dx We're releasing a new version of our Supabase client with some awesome new improvements. - Fri, 30 Oct 2020 00:00:00 GMT + Fri, 30 Oct 2020 00:00:00 -0700 https://supabase.com/blog/supabase-alpha-september-2020 Supabase Alpha September 2020 https://supabase.com/blog/supabase-alpha-september-2020 Seven months of building. - Sat, 03 Oct 2020 00:00:00 GMT + Sat, 03 Oct 2020 00:00:00 -0700 https://supabase.com/blog/supabase-hacktoberfest-2020 Supabase Hacktoberfest 2020 https://supabase.com/blog/supabase-hacktoberfest-2020 Join us for a celebration of open source software and learn how to contribute to Supabase. - Fri, 11 Sep 2020 00:00:00 GMT + Fri, 11 Sep 2020 00:00:00 -0700 https://supabase.com/blog/supabase-alpha-august-2020 Supabase Alpha August 2020 https://supabase.com/blog/supabase-alpha-august-2020 Six months of building - Thu, 03 Sep 2020 00:00:00 GMT + Thu, 03 Sep 2020 00:00:00 -0700 https://supabase.com/blog/supabase-auth Supabase Auth https://supabase.com/blog/supabase-auth Authenticate and authorize your users with Supabase Auth - Wed, 05 Aug 2020 00:00:00 GMT + Wed, 05 Aug 2020 00:00:00 -0700 https://supabase.com/blog/continuous-postgresql-backup-walg Continuous PostgreSQL Backups using WAL-G https://supabase.com/blog/continuous-postgresql-backup-walg Have you ever wanted to restore your database's state to a particular moment in time? This post explains how, using WAL-G. - Sun, 02 Aug 2020 00:00:00 GMT + Sun, 02 Aug 2020 00:00:00 -0700 https://supabase.com/blog/supabase-alpha-july-2020 Supabase Alpha July 2020 https://supabase.com/blog/supabase-alpha-july-2020 Five months of building - Sun, 02 Aug 2020 00:00:00 GMT + Sun, 02 Aug 2020 00:00:00 -0700 https://supabase.com/blog/alpha-launch-postmortem Alpha Launch Postmortem https://supabase.com/blog/alpha-launch-postmortem Everything that went wrong with Supabase's launch - Fri, 10 Jul 2020 00:00:00 GMT + Fri, 10 Jul 2020 00:00:00 -0700 https://supabase.com/blog/postgresql-templates What are PostgreSQL Templates? https://supabase.com/blog/postgresql-templates What are PostgreSQL templates and what are they used for? - Thu, 09 Jul 2020 00:00:00 GMT + Thu, 09 Jul 2020 00:00:00 -0700 https://supabase.com/blog/postgresql-physical-logical-backups Physical vs Logical Backups in PostgreSQL https://supabase.com/blog/postgresql-physical-logical-backups What are physical and logical backups in Postgres? - Tue, 07 Jul 2020 00:00:00 GMT + Tue, 07 Jul 2020 00:00:00 -0700 https://supabase.com/blog/supabase-alpha-june-2020 Supabase Alpha June 2020 https://supabase.com/blog/supabase-alpha-june-2020 Four months of building - Wed, 01 Jul 2020 00:00:00 GMT + Wed, 01 Jul 2020 00:00:00 -0700 https://supabase.com/blog/supabase-steve-chavez Steve Chavez has joined Supabase https://supabase.com/blog/supabase-steve-chavez Steve joins Supabase to help build Auth. - Mon, 15 Jun 2020 00:00:00 GMT + Mon, 15 Jun 2020 00:00:00 -0700 https://supabase.com/blog/supabase-alpha-april-2020 Supabase Alpha April 2020 https://supabase.com/blog/supabase-alpha-april-2020 Two months of building - Mon, 01 Jun 2020 00:00:00 GMT + Mon, 01 Jun 2020 00:00:00 -0700 https://supabase.com/blog/supabase-alpha-may-2020 Supabase Alpha May 2020 https://supabase.com/blog/supabase-alpha-may-2020 Three months of building - Mon, 01 Jun 2020 00:00:00 GMT + Mon, 01 Jun 2020 00:00:00 -0700 From 959e28abfb9ef98189eb75a31b2a6f8d28385f81 Mon Sep 17 00:00:00 2001 From: Chris Gwilliams <517923+encima@users.noreply.github.com> Date: Tue, 12 Nov 2024 02:21:42 +0200 Subject: [PATCH 29/86] update ipv4 doc title and add admonition warning (#30415) --- apps/docs/content/guides/platform/ipv4-address.mdx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/guides/platform/ipv4-address.mdx b/apps/docs/content/guides/platform/ipv4-address.mdx index bae6317423f..7a3988ca1b9 100644 --- a/apps/docs/content/guides/platform/ipv4-address.mdx +++ b/apps/docs/content/guides/platform/ipv4-address.mdx @@ -1,5 +1,5 @@ --- -title: 'Dedicated IPv4 Address' +title: 'Dedicated IPv4 Address for Ingress' description: 'Attach an IPv4 address to your database' subtitle: 'Attach an IPv4 address to your database' --- @@ -15,6 +15,12 @@ The Internet Protocol (IP) addresses devices on the internet. There are two main ## When you need the IPv4 add-on: + + +IPv4 addresses are guaranteed to be static for ingress traffic. If your database is making outbound connections, the outbound IP address is not static and cannot be guaranteed. + + + - When using the direct connection string in an IPv6-incompatible network instead of Supavisor or client libraries. - When you need a dedicated IP address for your direct connection string From 06c53f3472efeb9941e4b86e5b61bd21d2c07e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Tue, 12 Nov 2024 16:34:46 +0800 Subject: [PATCH 30/86] chore: use in-browser sha256 (#30398) --- apps/studio/components/ui/PageTelemetry.tsx | 14 ++++++------ apps/studio/package.json | 1 - package-lock.json | 25 ++++++++++++++++----- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/apps/studio/components/ui/PageTelemetry.tsx b/apps/studio/components/ui/PageTelemetry.tsx index dbad4b6cfe6..7bf41bf7844 100644 --- a/apps/studio/components/ui/PageTelemetry.tsx +++ b/apps/studio/components/ui/PageTelemetry.tsx @@ -1,4 +1,3 @@ -import { Sha256 } from '@aws-crypto/sha256-browser' import * as Sentry from '@sentry/nextjs' import { useRouter } from 'next/router' import { PropsWithChildren, useEffect } from 'react' @@ -15,12 +14,13 @@ import { useAppStateSnapshot } from 'state/app-state' import { useConsent } from 'ui-patterns/ConsentToast' const getAnonId = async (id: string) => { - const hash = new Sha256() - hash.update(id) - const u8Array = await hash.digest() - const binString = Array.from(u8Array, (byte) => String.fromCodePoint(byte)).join('') - const b64encoded = btoa(binString) - return b64encoded + const encoder = new TextEncoder() + const data = encoder.encode(id) + const hashBuffer = await crypto.subtle.digest('SHA-256', data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const base64String = btoa(hashArray.map((byte) => String.fromCharCode(byte)).join('')) + + return base64String } const PageTelemetry = ({ children }: PropsWithChildren<{}>) => { diff --git a/apps/studio/package.json b/apps/studio/package.json index 025b1e0a62b..e0b4b046619 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -22,7 +22,6 @@ "prettier:write": "prettier --write ." }, "dependencies": { - "@aws-crypto/sha256-browser": "^3.0.0", "@dagrejs/dagre": "^1.0.4", "@graphiql/react": "^0.19.4", "@graphiql/toolkit": "^0.9.1", diff --git a/package-lock.json b/package-lock.json index 9d304abccc6..9350354435e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1782,7 +1782,6 @@ "apps/studio": { "version": "0.0.9", "dependencies": { - "@aws-crypto/sha256-browser": "^3.0.0", "@dagrejs/dagre": "^1.0.4", "@graphiql/react": "^0.19.4", "@graphiql/toolkit": "^0.9.1", @@ -3669,6 +3668,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", + "dev": true, "dependencies": { "tslib": "^1.11.1" } @@ -3676,12 +3676,14 @@ "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/@aws-crypto/sha256-browser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", + "dev": true, "dependencies": { "@aws-crypto/ie11-detection": "^3.0.0", "@aws-crypto/sha256-js": "^3.0.0", @@ -3696,12 +3698,14 @@ "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/@aws-crypto/sha256-js": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", + "dev": true, "dependencies": { "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", @@ -3711,12 +3715,14 @@ "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/@aws-crypto/supports-web-crypto": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", + "dev": true, "dependencies": { "tslib": "^1.11.1" } @@ -3724,12 +3730,14 @@ "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/@aws-crypto/util": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "dev": true, "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", @@ -3739,7 +3747,8 @@ "node_modules/@aws-crypto/util/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/@aws-sdk/client-secrets-manager": { "version": "3.468.0", @@ -4179,6 +4188,7 @@ "version": "3.468.0", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.468.0.tgz", "integrity": "sha512-rx/9uHI4inRbp2tw3Y4Ih4PNZkVj32h7WneSg3MVgVjAoVD5Zti9KhS5hkvsBxfgmQmg0AQbE+b1sy5WGAgntA==", + "dev": true, "dependencies": { "@smithy/types": "^2.7.0", "tslib": "^2.5.0" @@ -4205,6 +4215,7 @@ "version": "3.465.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.465.0.tgz", "integrity": "sha512-f+QNcWGswredzC1ExNAB/QzODlxwaTdXkNT5cvke2RLX8SFU5pYk6h4uCtWC0vWPELzOfMfloBrJefBzlarhsw==", + "dev": true, "dependencies": { "tslib": "^2.5.0" }, @@ -4251,6 +4262,7 @@ "version": "3.259.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "dev": true, "dependencies": { "tslib": "^2.3.1" } @@ -14529,6 +14541,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.7.0.tgz", "integrity": "sha512-1OIFyhK+vOkMbu4aN2HZz/MomREkrAC/HqY5mlJMUJfGrPRwijJDTeiN8Rnj9zUaB8ogXAfIOtZrrgqZ4w7Wnw==", + "dev": true, "dependencies": { "tslib": "^2.5.0" }, From 1c65e8cb2cab96c8d6bbc56a767c03f50cd25bcd Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 12 Nov 2024 17:42:08 +0800 Subject: [PATCH 31/86] Fix linkedin enabled state in user overview (#30373) --- .../interfaces/Auth/Users/UserOverview.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx index 1c61bf21c79..81d08ea2a2c 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx @@ -221,13 +221,19 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {
{providers.map((provider) => { const providerMeta = PROVIDERS_SCHEMAS.find( - (x) => x.title.toLowerCase() === provider.name + (x) => + x.title.toLowerCase() === + (provider.name === 'linkedin' ? 'linkedin (oidc)' : provider.name) ) const enabledProperty = Object.keys(providerMeta?.properties ?? {}).find((x) => x.toLowerCase().endsWith('_enabled') ) const providerName = - provider.name === 'email' ? 'email' : providerMeta?.title ?? provider.name + provider.name === 'email' + ? 'email' + : provider.name === 'linkedin' + ? 'LinkedIn' + : providerMeta?.title ?? provider.name const isActive = data?.[enabledProperty as keyof typeof data] ?? false return ( @@ -241,7 +247,7 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => { /> )}
-

{provider.name}

+

{providerName}

Signed in with a {providerName} account via{' '} {providerName === 'SAML' ? 'SSO' : 'OAuth'} From b37d37791725c5df72d2999ce038b11b5d376a61 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 12 Nov 2024 17:47:55 +0800 Subject: [PATCH 32/86] Add check for project active healthy status in some RQs that are unnecessary when project is paused (#30377) --- .../Database/Backups/BackupItem.tsx | 10 ++++-- .../Database/Backups/BackupsList.tsx | 32 ++++++++----------- .../Database/Backups/DatabaseBackupsNav.tsx | 11 ++++--- apps/studio/data/auth/users-count-query.ts | 7 ++-- apps/studio/data/auth/users-infinite-query.ts | 12 +++++-- .../database-extensions-query.ts | 12 +++++-- .../database-policies-query.ts | 12 +++++-- apps/studio/data/lint/lint-query.ts | 12 +++++-- apps/studio/data/sql/execute-sql-query.ts | 13 +++++--- apps/studio/data/storage/buckets-query.ts | 18 +++++++---- .../project/[ref]/database/backups/pitr.tsx | 19 +++-------- .../[ref]/database/backups/scheduled.tsx | 15 +++------ 12 files changed, 97 insertions(+), 76 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Backups/BackupItem.tsx b/apps/studio/components/interfaces/Database/Backups/BackupItem.tsx index 61a7e427a44..96d2c57b55f 100644 --- a/apps/studio/components/interfaces/Database/Backups/BackupItem.tsx +++ b/apps/studio/components/interfaces/Database/Backups/BackupItem.tsx @@ -7,16 +7,17 @@ import { useBackupDownloadMutation } from 'data/database/backup-download-mutatio import type { DatabaseBackup } from 'data/database/backups-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Badge } from 'ui' +import { useParams } from 'common' interface BackupItemProps { index: number isHealthy: boolean - projectRef: string backup: DatabaseBackup onSelectBackup: () => void } -const BackupItem = ({ index, isHealthy, backup, projectRef, onSelectBackup }: BackupItemProps) => { +const BackupItem = ({ index, isHealthy, backup, onSelectBackup }: BackupItemProps) => { + const { ref: projectRef } = useParams() const canTriggerScheduledBackups = useCheckPermissions( PermissionAction.INFRA_EXECUTE, 'queue_job.restore.prepare' @@ -62,7 +63,10 @@ const BackupItem = ({ index, isHealthy, backup, projectRef, onSelectBackup }: Ba icon={} loading={isDownloading} disabled={!canTriggerScheduledBackups || isDownloading} - onClick={() => downloadBackup({ ref: projectRef, backup })} + onClick={() => { + if (!projectRef) return console.error('Project ref is required') + downloadBackup({ ref: projectRef, backup }) + }} tooltip={{ content: { side: 'bottom', diff --git a/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx b/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx index 1c25952c1b1..48a962d8840 100644 --- a/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx +++ b/apps/studio/components/interfaces/Database/Backups/BackupsList.tsx @@ -1,4 +1,3 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' import { Clock } from 'lucide-react' @@ -12,28 +11,23 @@ import UpgradeToPro from 'components/ui/UpgradeToPro' import { useBackupRestoreMutation } from 'data/database/backup-restore-mutation' import { DatabaseBackup, useBackupsQuery } from 'data/database/backups-query' import { setProjectStatus } from 'data/projects/projects-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { PROJECT_STATUS } from 'lib/constants' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import BackupItem from './BackupItem' import BackupsEmpty from './BackupsEmpty' import BackupsStorageAlert from './BackupsStorageAlert' +import { useParams } from 'common' const BackupsList = () => { const router = useRouter() const queryClient = useQueryClient() + const { ref: projectRef } = useParams() const { project: selectedProject } = useProjectContext() - const projectRef = selectedProject?.ref || 'default' const isHealthy = selectedProject?.status === PROJECT_STATUS.ACTIVE_HEALTHY const [selectedBackup, setSelectedBackup] = useState() - const canTriggerScheduledBackups = useCheckPermissions( - PermissionAction.INFRA_EXECUTE, - 'queue_job.restore.prepare' - ) - const { data: backups } = useBackupsQuery({ projectRef }) const { mutate: restoreFromBackup, @@ -41,15 +35,17 @@ const BackupsList = () => { isSuccess: isSuccessBackup, } = useBackupRestoreMutation({ onSuccess: () => { - setTimeout(() => { - setProjectStatus(queryClient, projectRef, PROJECT_STATUS.RESTORING) - toast.success( - `Restoring database back to ${dayjs(selectedBackup?.inserted_at).format( - 'DD MMM YYYY HH:mm:ss' - )}` - ) - router.push(`/project/${projectRef}`) - }, 3000) + if (projectRef) { + setTimeout(() => { + setProjectStatus(queryClient, projectRef, PROJECT_STATUS.RESTORING) + toast.success( + `Restoring database back to ${dayjs(selectedBackup?.inserted_at).format( + 'DD MMM YYYY HH:mm:ss' + )}` + ) + router.push(`/project/${projectRef}`) + }, 3000) + } }, }) @@ -86,7 +82,6 @@ const BackupsList = () => { setSelectedBackup(x)} @@ -106,6 +101,7 @@ const BackupsList = () => { loading={isRestoring || isSuccessBackup} onCancel={() => setSelectedBackup(undefined)} onConfirm={() => { + if (projectRef === undefined) return console.error('Project ref required') if (selectedBackup === undefined) return console.error('Backup required') restoreFromBackup({ ref: projectRef, backup: selectedBackup }) }} diff --git a/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx b/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx index 657ff8c0cfd..f0fe0b1d5e4 100644 --- a/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx +++ b/apps/studio/components/interfaces/Database/Backups/DatabaseBackupsNav.tsx @@ -1,23 +1,26 @@ import Link from 'next/link' import React from 'react' + +import { useParams } from 'common' import { NavMenu, NavMenuItem } from 'ui' type Props = { - projRef: string active: 'scheduled' | 'pitr' } -function DatabaseBackupsNav({ projRef, active }: Props) { +function DatabaseBackupsNav({ active }: Props) { + const { ref } = useParams() + const navMenuItems = [ { id: 'scheduled', label: 'Scheduled backups', - href: `/project/${projRef}/database/backups/scheduled`, + href: `/project/${ref}/database/backups/scheduled`, }, { id: 'pitr', label: 'Point in time', - href: `/project/${projRef}/database/backups/pitr`, + href: `/project/${ref}/database/backups/pitr`, }, ] diff --git a/apps/studio/data/auth/users-count-query.ts b/apps/studio/data/auth/users-count-query.ts index 53e89da8d3a..73b475ad066 100644 --- a/apps/studio/data/auth/users-count-query.ts +++ b/apps/studio/data/auth/users-count-query.ts @@ -1,8 +1,8 @@ import { UseQueryOptions } from '@tanstack/react-query' import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from 'data/sql/execute-sql-query' -import { Filter } from './users-infinite-query' import { authKeys } from './keys' +import { Filter } from './users-infinite-query' type UsersCountVariables = { projectRef?: string @@ -65,8 +65,8 @@ export type UsersCountError = ExecuteSqlError export const useUsersCountQuery = ( { projectRef, connectionString, keywords, filter, providers }: UsersCountVariables, options: UseQueryOptions = {} -) => - useExecuteSqlQuery( +) => { + return useExecuteSqlQuery( { projectRef, connectionString, @@ -75,3 +75,4 @@ export const useUsersCountQuery = ( { projectRef, connectionString, keywords, filter, providers, sort, order }: UsersVariables, { enabled = true, ...options }: UseInfiniteQueryOptions = {} -) => - useInfiniteQuery( +) => { + const project = useSelectedProject() + const isActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY + + return useInfiniteQuery( authKeys.usersInfinite(projectRef, { keywords, filter, providers, sort, order }), ({ signal, pageParam }) => { return executeSql( @@ -105,7 +110,7 @@ export const useUsersInfiniteQuery = ( }, { staleTime: 0, - enabled: enabled && typeof projectRef !== 'undefined', + enabled: enabled && typeof projectRef !== 'undefined' && isActive, getNextPageParam(lastPage, pages) { const page = pages.length @@ -116,3 +121,4 @@ export const useUsersInfiniteQuery = ( ...options, } ) +} diff --git a/apps/studio/data/database-extensions/database-extensions-query.ts b/apps/studio/data/database-extensions/database-extensions-query.ts index 0860a3bd019..cff897876ae 100644 --- a/apps/studio/data/database-extensions/database-extensions-query.ts +++ b/apps/studio/data/database-extensions/database-extensions-query.ts @@ -3,6 +3,8 @@ import { get, handleError } from 'data/fetchers' import type { ResponseError } from 'types' import { databaseExtensionsKeys } from './keys' import { components } from 'api-types' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { PROJECT_STATUS } from 'lib/constants' export type DatabaseExtension = components['schemas']['PostgresExtension'] @@ -46,12 +48,16 @@ export const useDatabaseExtensionsQuery = ( enabled = true, ...options }: UseQueryOptions = {} -) => - useQuery( +) => { + const project = useSelectedProject() + const isActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY + + return useQuery( databaseExtensionsKeys.list(projectRef), ({ signal }) => getDatabaseExtensions({ projectRef, connectionString }, signal), { - enabled: enabled && typeof projectRef !== 'undefined', + enabled: enabled && typeof projectRef !== 'undefined' && isActive, ...options, } ) +} diff --git a/apps/studio/data/database-policies/database-policies-query.ts b/apps/studio/data/database-policies/database-policies-query.ts index 3213dac2061..e0246c3d943 100644 --- a/apps/studio/data/database-policies/database-policies-query.ts +++ b/apps/studio/data/database-policies/database-policies-query.ts @@ -1,6 +1,8 @@ import { UseQueryOptions, useQuery } from '@tanstack/react-query' import { get, handleError } from 'data/fetchers' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { PROJECT_STATUS } from 'lib/constants' import type { ResponseError } from 'types' import { databasePoliciesKeys } from './keys' @@ -45,12 +47,16 @@ export const useDatabasePoliciesQuery = ( enabled = true, ...options }: UseQueryOptions = {} -) => - useQuery( +) => { + const project = useSelectedProject() + const isActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY + + return useQuery( databasePoliciesKeys.list(projectRef, schema), ({ signal }) => getDatabasePolicies({ projectRef, connectionString, schema }, signal), { - enabled: enabled && typeof projectRef !== 'undefined', + enabled: enabled && typeof projectRef !== 'undefined' && isActive, ...options, } ) +} diff --git a/apps/studio/data/lint/lint-query.ts b/apps/studio/data/lint/lint-query.ts index ea15206f8c7..c1ccafb0da5 100644 --- a/apps/studio/data/lint/lint-query.ts +++ b/apps/studio/data/lint/lint-query.ts @@ -2,6 +2,8 @@ import { UseQueryOptions, useQuery } from '@tanstack/react-query' import { components } from 'api-types' import { get, handleError } from 'data/fetchers' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { PROJECT_STATUS } from 'lib/constants' import { ResponseError } from 'types' import { lintKeys } from './keys' @@ -31,12 +33,16 @@ export type ProjectLintsError = ResponseError export const useProjectLintsQuery = ( { projectRef }: ProjectLintsVariables, { enabled = true, ...options }: UseQueryOptions = {} -) => - useQuery( +) => { + const project = useSelectedProject() + const isActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY + + return useQuery( lintKeys.lint(projectRef), ({ signal }) => getProjectLints({ projectRef }, signal), { - enabled: enabled && typeof projectRef !== 'undefined', + enabled: enabled && typeof projectRef !== 'undefined' && isActive, ...options, } ) +} diff --git a/apps/studio/data/sql/execute-sql-query.ts b/apps/studio/data/sql/execute-sql-query.ts index 6048a90d97c..3bbc0881cc3 100644 --- a/apps/studio/data/sql/execute-sql-query.ts +++ b/apps/studio/data/sql/execute-sql-query.ts @@ -7,7 +7,8 @@ import { } from 'lib/role-impersonation' import type { ResponseError } from 'types' import { sqlKeys } from './keys' -import { MB } from 'lib/constants' +import { MB, PROJECT_STATUS } from 'lib/constants' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' export type ExecuteSqlVariables = { projectRef?: string @@ -123,13 +124,17 @@ export const useExecuteSqlQuery = ( isRoleImpersonationEnabled, }: ExecuteSqlVariables, { enabled = true, ...options }: UseQueryOptions = {} -) => - useQuery( +) => { + const project = useSelectedProject() + const isActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY + + return useQuery( sqlKeys.query(projectRef, queryKey ?? [btoa(sql)]), ({ signal }) => executeSql( { projectRef, connectionString, sql, queryKey, handleError, isRoleImpersonationEnabled }, signal ), - { enabled: enabled && typeof projectRef !== 'undefined', staleTime: 0, ...options } + { enabled: enabled && typeof projectRef !== 'undefined' && isActive, staleTime: 0, ...options } ) +} diff --git a/apps/studio/data/storage/buckets-query.ts b/apps/studio/data/storage/buckets-query.ts index 2cc53817ead..e4efcfad08c 100644 --- a/apps/studio/data/storage/buckets-query.ts +++ b/apps/studio/data/storage/buckets-query.ts @@ -1,9 +1,9 @@ -import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' import { get, handleError } from 'data/fetchers' -import { API_URL } from 'lib/constants' -import { useCallback } from 'react' -import { storageKeys } from './keys' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { PROJECT_STATUS } from 'lib/constants' import type { ResponseError } from 'types' +import { storageKeys } from './keys' export type BucketsVariables = { projectRef?: string } @@ -36,12 +36,15 @@ export type BucketsError = ResponseError export const useBucketsQuery = ( { projectRef }: BucketsVariables, { enabled = true, ...options }: UseQueryOptions = {} -) => - useQuery( +) => { + const project = useSelectedProject() + const isActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY + + return useQuery( storageKeys.buckets(projectRef), ({ signal }) => getBuckets({ projectRef }, signal), { - enabled: enabled && typeof projectRef !== 'undefined', + enabled: enabled && typeof projectRef !== 'undefined' && isActive, ...options, retry: (failureCount, error) => { if ( @@ -61,3 +64,4 @@ export const useBucketsQuery = ( }, } ) +} diff --git a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx index a97219e66f9..913ad9d7f74 100644 --- a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -1,5 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' +import { AlertCircle } from 'lucide-react' +import { useParams } from 'common' import DatabaseBackupsNav from 'components/interfaces/Database/Backups/DatabaseBackupsNav' import { PITRNotice, PITRSelection } from 'components/interfaces/Database/Backups/PITR' import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' @@ -15,21 +17,17 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { PROJECT_STATUS } from 'lib/constants' -import { AlertCircle } from 'lucide-react' import type { NextPageWithLayout } from 'types' import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_ } from 'ui' const DatabasePhysicalBackups: NextPageWithLayout = () => { - const { project } = useProjectContext() - const ref = project?.ref ?? 'default' - return (

- +
@@ -45,17 +43,10 @@ DatabasePhysicalBackups.getLayout = (page) => ( ) const PITR = () => { + const { ref: projectRef } = useParams() const { project } = useProjectContext() const organization = useSelectedOrganization() - const { - data: backups, - error, - isLoading, - isError, - isSuccess, - } = useBackupsQuery({ - projectRef: project?.ref, - }) + const { data: backups, error, isLoading, isError, isSuccess } = useBackupsQuery({ projectRef }) const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) diff --git a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx index 9d6df075c76..ee52cf9c265 100644 --- a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx @@ -1,10 +1,10 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { Info } from 'lucide-react' +import { useParams } from 'common' import { BackupsList } from 'components/interfaces/Database' import DatabaseBackupsNav from 'components/interfaces/Database/Backups/DatabaseBackupsNav' import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import { FormHeader } from 'components/ui/Forms/FormHeader' @@ -16,16 +16,9 @@ import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPe import type { NextPageWithLayout } from 'types' const DatabaseScheduledBackups: NextPageWithLayout = () => { - const { project } = useProjectContext() - const ref = project?.ref || 'default' + const { ref: projectRef } = useParams() - const { - data: backups, - error, - isLoading, - isError, - isSuccess, - } = useBackupsQuery({ projectRef: ref }) + const { data: backups, error, isLoading, isError, isSuccess } = useBackupsQuery({ projectRef }) const isPitrEnabled = backups?.pitr_enabled const isPermissionsLoaded = usePermissionsLoaded() @@ -39,7 +32,7 @@ const DatabaseScheduledBackups: NextPageWithLayout = () => {
- +
{isLoading && } From eb7b0818d1ef05b50779bf57a88b1493613fa41b Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Tue, 12 Nov 2024 17:50:05 +0800 Subject: [PATCH 33/86] fix: use nulls last when sorting users (#30430) --- apps/studio/data/auth/users-infinite-query.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/data/auth/users-infinite-query.ts b/apps/studio/data/auth/users-infinite-query.ts index 8557554bdd2..97ec9d82287 100644 --- a/apps/studio/data/auth/users-infinite-query.ts +++ b/apps/studio/data/auth/users-infinite-query.ts @@ -35,7 +35,7 @@ export const getUsersSQL = ({ keywords?: string providers?: string[] sort: string - order: string + order: 'asc' | 'desc' }) => { const offset = page * USERS_PAGE_LIMIT const hasValidKeywords = keywords && keywords !== '' @@ -75,7 +75,7 @@ export const getUsersSQL = ({ const sortOn = sort ?? 'created_at' const sortOrder = order ?? 'desc' - return `${baseQueryUsers}${conditions.length > 0 ? ` where ${combinedConditions}` : ''} order by "${sortOn}" ${sortOrder} limit ${USERS_PAGE_LIMIT} offset ${offset};` + return `${baseQueryUsers}${conditions.length > 0 ? ` where ${combinedConditions}` : ''} order by "${sortOn}" ${sortOrder} nulls last limit ${USERS_PAGE_LIMIT} offset ${offset};` } export type UsersData = { result: User[] } From a2907880475c5eb7579974771d69ae16c695335e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Tue, 12 Nov 2024 20:55:50 +0800 Subject: [PATCH 34/86] fix: allow downgrade to Free Plan with no active projects (#30433) The member exceeding Free Plan limits modal should not be displayed if the org to be downgraded has no active projects. --- .../BillingSettings/Subscription/PlanUpdateSidePanel.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx index b40b2de2829..6929f728c9e 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx @@ -102,7 +102,9 @@ const PlanUpdateSidePanel = () => { } = useOrganizationBillingSubscriptionPreview({ tier: selectedTier, organizationSlug: slug }) const availablePlans: OrgPlan[] = plans?.plans ?? [] - const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 + const hasMembersExceedingFreeTierLimit = + (membersExceededLimit || []).length > 0 && + orgProjects.filter((it) => it.status !== 'INACTIVE' && it.status !== 'GOING_DOWN').length > 0 const subscriptionPlanMeta = subscriptionsPlans.find((tier) => tier.id === selectedTier) const expandUsageFee = (fee: string) => { From 95219d3e2ab6a31a6b80e00bc63d29ba89a356e9 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 12 Nov 2024 11:55:01 -0330 Subject: [PATCH 35/86] Chore/cron nl (#30345) * Add NL input * Add chart to sidebar * Better loading animation * Redo api route to not use edge * Remove edge stuff * Allow sheet to close * Cleanup * Adjust loading state * Add timezone * CR comments * Replace query with useCompletion * Set the form value * Tiny clean up * Tinyyyyy tweaks * Fix missing syntax * Smol refactor * Fixy * Remove console log --------- Co-authored-by: Joshen Lim --- .../Database/Extensions/ExtensionCard.tsx | 17 +- .../CronJobs/CreateCronJobSheet.tsx | 3 +- .../CronJobs/CronJobScheduleSection.tsx | 263 ++++++++++++++---- .../Integrations/CronJobs/CronJobsListing.tsx | 81 +----- .../Integrations/CronJobs/CronSyntaxChart.tsx | 18 ++ .../database-cron-timezone-query.ts | 38 +++ apps/studio/data/database-cron-jobs/keys.ts | 1 + apps/studio/middleware.ts | 1 + apps/studio/pages/api/ai/sql/cron.ts | 59 ++++ .../project/[ref]/integrations/cron-jobs.tsx | 91 +++++- .../[ref]/integrations/wrappers/index.tsx | 4 +- packages/ai-commands/edge.ts | 1 + packages/ai-commands/src/sql/cron.ts | 97 +++++++ 13 files changed, 523 insertions(+), 151 deletions(-) create mode 100644 apps/studio/components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx create mode 100644 apps/studio/data/database-cron-jobs/database-cron-timezone-query.ts create mode 100644 apps/studio/pages/api/ai/sql/cron.ts create mode 100644 packages/ai-commands/src/sql/cron.ts diff --git a/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx b/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx index a393c9eb2d5..f86edb50bc4 100644 --- a/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx @@ -1,5 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Book, Github, Loader2, Settings, AlertCircle } from 'lucide-react' +import { Book, Github, Loader2, Settings } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' import { toast } from 'sonner' @@ -9,11 +9,11 @@ import { useDatabaseExtensionDisableMutation } from 'data/database-extensions/da import { DatabaseExtension } from 'data/database-extensions/database-extensions-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { extensions } from 'shared-data' -import { Button, cn, Switch, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_ } from 'ui' +import { Button, cn, Switch } from 'ui' +import { Admonition } from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import EnableExtensionModal from './EnableExtensionModal' import { EXTENSION_DISABLE_WARNINGS } from './Extensions.constants' -import { Alert } from '@ui/components/shadcn/ui/alert' interface ExtensionCardProps { extension: DatabaseExtension @@ -168,17 +168,14 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => { onCancel={() => setIsDisableModalOpen(false)} onConfirm={() => onConfirmDisable()} > -
+

Are you sure you want to turn OFF the "{extension.name}" extension?

{EXTENSION_DISABLE_WARNINGS[extension.name] && ( - - - - {EXTENSION_DISABLE_WARNINGS[extension.name]} - - + + {EXTENSION_DISABLE_WARNINGS[extension.name]} + )}
diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx index 32365416b10..5b4e7a5b70f 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx @@ -42,6 +42,7 @@ import { HttpRequestSection } from './HttpRequestSection' import { SqlFunctionSection } from './SqlFunctionSection' import { SqlSnippetSection } from './SqlSnippetSection' import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' +import { checkDomainOfScale } from 'recharts/types/util/ChartUtils' export interface CreateCronJobSheetProps { selectedCronJob?: Pick @@ -131,7 +132,7 @@ export const CreateCronJobSheet = ({ resolver: zodResolver(FormSchema), defaultValues: { name: selectedCronJob?.jobname || '', - schedule: selectedCronJob?.schedule || '', + schedule: selectedCronJob?.schedule || '* * * * * *', values: cronJobValues, }, }) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx index 1d71314b921..5adbd02dd10 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx @@ -1,108 +1,229 @@ import { toString as CronToString } from 'cronstrue' +import { motion } from 'framer-motion' import { useEffect, useState } from 'react' import { UseFormReturn } from 'react-hook-form' +import { useDebounce } from 'use-debounce' +import { useCompletion } from 'ai/react' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useCronTimezoneQuery } from 'data/database-cron-jobs/database-cron-timezone-query' +import { BASE_PATH } from 'lib/constants' import { + Accordion_Shadcn_, + AccordionContent_Shadcn_, + AccordionItem_Shadcn_, + AccordionTrigger_Shadcn_, + Button, + cn, FormControl_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, FormLabel_Shadcn_, - FormMessage_Shadcn_, Input_Shadcn_, - Select_Shadcn_, - SelectContent_Shadcn_, - SelectItem_Shadcn_, - SelectTrigger_Shadcn_, - SelectValue_Shadcn_, SheetSection, + Switch, } from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' import { CreateCronJobForm } from './CreateCronJobSheet' +import CronSyntaxChart from './CronSyntaxChart' interface CronJobScheduleSectionProps { form: UseFormReturn } -const schedules = [ - { name: 'Every minute', expression: '* * * * *' }, +const presets = [ + { name: 'Every minute', expression: '* * * * * *' }, { name: 'Every 5 minutes', expression: '*/5 * * * *' }, - { name: 'Every hour, at 30 minutes', expression: '30 * * * *' }, { name: 'Every first of the month, at 00:00', expression: '0 0 1 * *' }, - { name: 'Custom', expression: 'custom' }, + { name: 'Every Monday at midnight', expression: '0 0 * * 1' }, + { name: 'Every night at midnight', expression: '0 0 0 * * *' }, ] as const export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => { - let initialValue = schedules[0].expression as string - const scheduleValue = form.getValues('schedule') - if (scheduleValue && scheduleValue.length > 0) { - initialValue = 'custom' - } + const { project } = useProjectContext() + const initialValue = form.getValues('schedule') + const { schedule } = form.watch() const [presetValue, setPresetValue] = useState(initialValue) + const [inputValue, setInputValue] = useState(initialValue) + const [debouncedValue] = useDebounce(inputValue, 500) + const [useNaturalLanguage, setUseNaturalLanguage] = useState(false) + const [scheduleString, setScheduleString] = useState('') - const onChangeSelectValue = (v: string) => { - setPresetValue(v) - } + const { complete: generateCronSyntax, isLoading: isGeneratingCron } = useCompletion({ + api: `${BASE_PATH}/api/ai/sql/cron`, + onResponse: async (response) => { + if (response.ok) { + // remove quotes from the cron expression + const expression = (await response.text()).trim().replace(/^"|"$/g, '') + form.setValue('schedule', expression) + setPresetValue(expression) + setScheduleString(CronToString(expression)) + } + }, + onError: (error) => { + console.error('Error generating cron:', error) + }, + }) + + const { data: timezone } = useCronTimezoneQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) useEffect(() => { - if (presetValue !== 'custom') { - form.setValue('schedule', presetValue) + if (!useNaturalLanguage || !debouncedValue) return + generateCronSyntax(debouncedValue) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debouncedValue, useNaturalLanguage]) + + useEffect(() => { + if (!inputValue) return + + // update the cronstrue string when the input value changes + try { + setScheduleString(CronToString(inputValue)) + form.setValue('schedule', inputValue) + } catch {} + }, [form, inputValue]) + + useEffect(() => { + if (!useNaturalLanguage) { + setPresetValue(schedule) + setScheduleString(CronToString(schedule)) } - }, [presetValue, form]) + }, [schedule]) return ( { - let scheduleString = '' - try { - scheduleString = CronToString(field.value) - } catch {} - + render={({ field }) => { return ( Schedule + + {useNaturalLanguage ? 'Describe your schedule in words' : 'Enter a cron expression'} + -
- - - - - - - - {schedules.map((schedule) => { - return ( - - {schedule.name} - - ) - })} - - - +
+ {useNaturalLanguage ? ( + setInputValue(e.target.value)} + /> + ) : ( + { + if (e.key === 'Enter') { + e.preventDefault() + } + }} + /> + )} + +
+ { + setUseNaturalLanguage(!useNaturalLanguage) + setInputValue('') + setPresetValue('') + setScheduleString('') + }} + /> +

Use natural language

+
+ +
+
    + {presets.map((preset) => ( +
  • + +
  • + ))} +
+
+ + + + View syntax chart + + + + + +
- + +
+

+ Schedule {timezone ? `(${timezone})` : ''} +

{scheduleString ? ( - - The cron will be run {/* lowercase the first letter */} - {scheduleString - .split(' ') - .map((s, i) => (i === 0 ? s.toLocaleLowerCase() : s)) - .join(' ')} - . + + {isGeneratingCron ? : presetValue || '* * * * * *'} ) : ( - !fieldState.error && + + {isGeneratingCron ? : presetValue || '* * * * * *'} + )} - + {!inputValue && !isGeneratingCron && !scheduleString ? ( + + Describe your schedule above + + ) : scheduleString ? ( + + The cron will be run {/* lowercase the first letter */} + {isGeneratingCron ? ( + + {[0, 1, 2].map((i) => ( + + . + + ))} + + ) : ( + scheduleString + .split(' ') + .map((s, i) => (i === 0 ? s.toLocaleLowerCase() : s)) + .join(' ') + '.' + )} + + ) : null} +
) }} @@ -110,3 +231,25 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => ) } + +const CronSyntaxLoader = () => { + return ( +
+ {['*', '*', '*', '*', '*', '*'].map((char, i) => ( + + {char} + + ))} +
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsListing.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsListing.tsx index edfe0766c69..2ae618a7a8c 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsListing.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsListing.tsx @@ -1,24 +1,17 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import AlertError from 'components/ui/AlertError' -import { FormHeader } from 'components/ui/Forms/FormHeader' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { CronJob, useCronJobsQuery } from 'data/database-cron-jobs/database-cron-jobs-query' -import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' -import { Button, Sheet, SheetContent } from 'ui' -import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' -import { DocsButton } from 'components/ui/DocsButton' -import { CreateCronJobSheet } from './CreateCronJobSheet' import { CronJobCard } from './CronJobCard' -import { CronJobsDisabledState } from './CronJobsDisabledState' import DeleteCronJob from './DeleteCronJob' +import { Sheet, SheetContent } from 'ui' +import { CreateCronJobSheet } from 'components/interfaces/Integrations/CronJobs/CreateCronJobSheet' export const CronJobsListing = () => { const { project } = useProjectContext() - const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) // used for confirmation prompt in the Create Cron Job Sheet const [isClosingCreateCronJobSheet, setIsClosingCreateCronJobSheet] = useState(false) const [createCronJobSheetShown, setCreateCronJobSheetShown] = useState< @@ -26,64 +19,16 @@ export const CronJobsListing = () => { >() const [cronJobForDeletion, setCronJobForDeletion] = useState() - const { - data: cronJobs, - error, - isLoading, - isError, - refetch, - } = useCronJobsQuery({ + const { data: cronJobs, isLoading } = useCronJobsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) - const { data: extensions, isLoading: isLoadingExtensions } = useDatabaseExtensionsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - const pgCronExtension = (extensions ?? []).find((ext) => ext.name === 'pg_cron') - const pgCronExtensionInstalled = pgCronExtension?.installed_version - - useEffect(() => { - // refetch the cron jobs afte the pgcron extension has been installed - if (pgCronExtensionInstalled && isError) { - refetch() - } - }, [isError, pgCronExtensionInstalled, refetch]) - - // this avoid showing loading screen when the extension is not installed. Otherwise, we'll have to wait for three - // retries (which are sure to fail because the extension is not installed) - if (isLoadingExtensions) return - if (!pgCronExtensionInstalled) return if (isLoading) return - if (isError) return return ( <>
- - - - - } - /> {(cronJobs ?? []).length == 0 ? (

No cron jobs created yet

@@ -93,7 +38,7 @@ export const CronJobsListing = () => {
) : (
- {cronJobs.map((job) => ( + {(cronJobs ?? []).map((job) => ( setCreateCronJobSheetShown(job)} @@ -104,6 +49,12 @@ export const CronJobsListing = () => { )}
+ setCronJobForDeletion(undefined)} + cronJob={cronJobForDeletion!} + /> + setIsClosingCreateCronJobSheet(true)} @@ -120,16 +71,6 @@ export const CronJobsListing = () => { /> - setCronJobForDeletion(undefined)} - cronJob={cronJobForDeletion!} - /> - setShowEnableExtensionModal(false)} - /> ) } diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx new file mode 100644 index 00000000000..6bd57df71d3 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx @@ -0,0 +1,18 @@ +export default function CronSyntaxChart() { + return ( +
+
+        {/* prettier-ignore */}
+        {`
+┌───────────── second (0 - 59)
+│  ┌───────────── minute (0 - 59)
+│  │  ┌───────────── hour (0 - 23)
+│  │  │  ┌───────────── day of month (1 - 31)
+│  │  │  │  ┌───────────── month (1 - 12)
+│  │  │  │  │  ┌───────────── day of week (0 - 7)
+│  │  │  │  │  │
+*  *  *  *  *  * `}
+      
+
+ ) +} diff --git a/apps/studio/data/database-cron-jobs/database-cron-timezone-query.ts b/apps/studio/data/database-cron-jobs/database-cron-timezone-query.ts new file mode 100644 index 00000000000..ec9423fcba5 --- /dev/null +++ b/apps/studio/data/database-cron-jobs/database-cron-timezone-query.ts @@ -0,0 +1,38 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { executeSql } from 'data/sql/execute-sql-query' +import { ResponseError } from 'types' +import { databaseCronJobsKeys } from './keys' + +export type DatabaseCronJobsVariables = { + projectRef?: string + connectionString?: string +} + +export async function getDatabaseCronTimezone({ + projectRef, + connectionString, +}: DatabaseCronJobsVariables) { + if (!projectRef) throw new Error('Project ref is required') + + const { result } = await executeSql({ + projectRef, + connectionString, + sql: `select setting from pg_settings where name = 'cron.timezone';`, + }) + return result[0].setting +} + +export type DatabaseCronJobError = ResponseError + +export const useCronTimezoneQuery = ( + { projectRef, connectionString }: DatabaseCronJobsVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + databaseCronJobsKeys.timezone(projectRef), + () => getDatabaseCronTimezone({ projectRef, connectionString }), + { + enabled: enabled && typeof projectRef !== 'undefined', + ...options, + } + ) diff --git a/apps/studio/data/database-cron-jobs/keys.ts b/apps/studio/data/database-cron-jobs/keys.ts index 8bd4b049051..5df1b7ec631 100644 --- a/apps/studio/data/database-cron-jobs/keys.ts +++ b/apps/studio/data/database-cron-jobs/keys.ts @@ -3,4 +3,5 @@ export const databaseCronJobsKeys = { delete: () => ['cron-jobs', 'delete'] as const, alter: () => ['cronjobs', 'alter'] as const, list: (projectRef: string | undefined) => ['projects', projectRef, 'cron-jobs'] as const, + timezone: (projectRef: string | undefined) => ['database-cron-timezone', projectRef] as const, } diff --git a/apps/studio/middleware.ts b/apps/studio/middleware.ts index 935169b1b42..717606e7347 100644 --- a/apps/studio/middleware.ts +++ b/apps/studio/middleware.ts @@ -11,6 +11,7 @@ const HOSTED_SUPPORTED_API_URLS = [ '/ai/sql/generate-v2', '/ai/sql/title', '/ai/sql/debug', + '/ai/sql/cron', '/get-ip-address', ] diff --git a/apps/studio/pages/api/ai/sql/cron.ts b/apps/studio/pages/api/ai/sql/cron.ts new file mode 100644 index 00000000000..23d807d399e --- /dev/null +++ b/apps/studio/pages/api/ai/sql/cron.ts @@ -0,0 +1,59 @@ +import { ContextLengthError } from 'ai-commands' +import { generateCron } from 'ai-commands/edge' +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' +import OpenAI from 'openai' + +const openAiKey = process.env.OPENAI_API_KEY +const openai = new OpenAI({ apiKey: openAiKey }) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!openAiKey) { + return res.status(500).json({ + error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.', + }) + } + + const { method } = req + + switch (method) { + case 'POST': + return handlePost(req, res) + default: + res.setHeader('Allow', ['POST']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +export async function handlePost(req: NextApiRequest, res: NextApiResponse) { + const { + body: { prompt }, + } = req + + try { + const result = await generateCron(openai, prompt) + + return res.json(result) + } catch (error) { + if (error instanceof Error) { + console.error(`AI cron generation failed: ${error.message}`) + + if (error instanceof ContextLengthError) { + return res.status(400).json({ + error: + 'Your cron prompt is too large for Supabase AI to ingest. Try splitting it into smaller prompts.', + }) + } + } else { + console.log(`Unknown error: ${error}`) + } + + return res.status(500).json({ + error: 'There was an unknown error generating the cron syntax. Please try again.', + }) + } +} + +const wrapper = (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +export default wrapper diff --git a/apps/studio/pages/project/[ref]/integrations/cron-jobs.tsx b/apps/studio/pages/project/[ref]/integrations/cron-jobs.tsx index 329da3177e4..1a59917e814 100644 --- a/apps/studio/pages/project/[ref]/integrations/cron-jobs.tsx +++ b/apps/studio/pages/project/[ref]/integrations/cron-jobs.tsx @@ -1,24 +1,101 @@ import { CronJobsListing } from 'components/interfaces/Integrations/CronJobs/CronJobsListing' import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' +import { DocsButton } from 'components/ui/DocsButton' +import { FormHeader } from 'components/ui/Forms/FormHeader' +import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' +import { CronJob, useCronJobsQuery } from 'data/database-cron-jobs/database-cron-jobs-query' +import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' +import { useEffect, useState } from 'react' import type { NextPageWithLayout } from 'types' +import { Button } from 'ui' +import { CronJobsDisabledState } from '../../../../components/interfaces/Integrations/CronJobs/CronJobsDisabledState' + +import { CreateCronJobSheet } from 'components/interfaces/Integrations/CronJobs/CreateCronJobSheet' +import { Sheet, SheetContent } from 'ui' const CronJobsPage: NextPageWithLayout = () => { - // TODO: Change this to the correct permissions - // const canReadFunctions = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'functions') - // const isPermissionsLoaded = usePermissionsLoaded() + const { project } = useProjectContext() + const [isClosingCreateCronJobSheet, setIsClosingCreateCronJobSheet] = useState(false) + const [createCronJobSheetShown, setCreateCronJobSheetShown] = useState< + Pick | undefined + >() - // if (isPermissionsLoaded && !canReadFunctions) { - // return - // } + const { isError, refetch } = useCronJobsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const { data: extensions, isLoading: isLoadingExtensions } = useDatabaseExtensionsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const pgCronExtension = (extensions ?? []).find((ext) => ext.name === 'pg_cron') + const pgCronExtensionInstalled = pgCronExtension?.installed_version + + useEffect(() => { + // refetch the cron jobs after the pgcron extension has been installed + if (pgCronExtensionInstalled && isError) { + refetch() + } + }, [isError, pgCronExtensionInstalled, refetch]) return (
- + {pgCronExtensionInstalled && ( + + + + + } + /> + )} + {isLoadingExtensions ? ( + + ) : !pgCronExtensionInstalled ? ( + + ) : ( + + )}
+ + setIsClosingCreateCronJobSheet(true)} + > + + { + setIsClosingCreateCronJobSheet(false) + setCreateCronJobSheetShown(undefined) + }} + isClosing={isClosingCreateCronJobSheet} + setIsClosing={setIsClosingCreateCronJobSheet} + /> + +
) } diff --git a/apps/studio/pages/project/[ref]/integrations/wrappers/index.tsx b/apps/studio/pages/project/[ref]/integrations/wrappers/index.tsx index aae646b283d..fa21318f6c4 100644 --- a/apps/studio/pages/project/[ref]/integrations/wrappers/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/wrappers/index.tsx @@ -1,5 +1,4 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { ExternalLink } from 'lucide-react' import { Wrappers } from 'components/interfaces/Database' import WrappersDropdown from 'components/interfaces/Database/Wrappers/WrappersDropdown' @@ -11,15 +10,14 @@ import { ScaffoldSectionContent, ScaffoldSectionDetail, } from 'components/layouts/Scaffold' +import { DocsButton } from 'components/ui/DocsButton' import { FormHeader } from 'components/ui/Forms/FormHeader' import NoPermission from 'components/ui/NoPermission' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useFDWsQuery } from 'data/fdw/fdws-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import type { NextPageWithLayout } from 'types' -import { Button } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' -import { DocsButton } from 'components/ui/DocsButton' const DatabaseWrappers: NextPageWithLayout = () => { const { project } = useProjectContext() diff --git a/packages/ai-commands/edge.ts b/packages/ai-commands/edge.ts index d73e04b769f..51b414c885b 100644 --- a/packages/ai-commands/edge.ts +++ b/packages/ai-commands/edge.ts @@ -3,3 +3,4 @@ export * from './src/errors' export * from './src/docs' export * from './src/sql/rls' export * from './src/sql/chat' +export * from './src/sql/cron' diff --git a/packages/ai-commands/src/sql/cron.ts b/packages/ai-commands/src/sql/cron.ts new file mode 100644 index 00000000000..c9f0ce5fdb1 --- /dev/null +++ b/packages/ai-commands/src/sql/cron.ts @@ -0,0 +1,97 @@ +import type OpenAI from 'openai' +import { ContextLengthError } from '../errors' +import { jsonrepair } from 'jsonrepair' +import { codeBlock } from 'common-tags' + +// Responds to a natural language request for cron syntax. +export async function generateCron(openai: OpenAI, prompt: string) { + const initMessages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [ + { + role: 'system', + content: codeBlock` + You are a cron syntax expert. Your purpose is to convert natural language time descriptions into valid cron expressions for pg_cron. + + Rules for responses: + - Output cron expressions in the 6-field format supported by pg_cron (seconds minute hour day month weekday) + - The seconds field is optional - if not specified, it defaults to 0 + - Provide a brief explanation of what the cron expression does + - Format output as markdown with the cron expression in a code block + - If the request is unclear or impossible, ask for clarification + - If the request isn't related to cron scheduling, explain that you can only help with cron expressions + + Example input: "Every Monday at 3am" + Example output: + This cron expression runs every Monday at 3:00:00 AM: + \`\`\` + 0 0 3 * * 1 + \`\`\` + + Common patterns to remember: + - * means "every" for that field + - */n means "every n intervals" + - n means "exactly at n" + - n,m means "at n and m" + - n-m means "every value from n to m" + + Field order: seconds (0-59), minute (0-59), hour (0-23), day (1-31), month (1-12), weekday (0-6, Sunday=0) + + Here is the user's prompt: + ${prompt} + `, + }, + ] + + const completionFunction = { + name: 'generateCronSyntax', + description: 'Generates a cron expression for a given natural language description.', + parameters: { + type: 'object', + properties: { + cron_expression: { + type: 'string', + description: 'The generated cron expression.', + }, + }, + required: ['cron_expression'], + }, + } + + try { + const response = await openai.chat.completions.create({ + model: 'gpt-4o-mini-2024-07-18', + messages: initMessages, + max_tokens: 1024, + temperature: 0, + stream: false, + tool_choice: { + type: 'function', + function: { + name: completionFunction.name, + }, + }, + tools: [ + { + type: 'function', + function: completionFunction, + }, + ], + }) + + // Parse the JSON string in arguments using jsonrepair + const functionArgs = response?.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments + if (!functionArgs) { + throw new Error('No cron expression generated') + } + + // Use jsonrepair before parsing + const repairedJsonString = jsonrepair(functionArgs) + const args = JSON.parse(repairedJsonString) + + return args.cron_expression + } catch (error) { + if (error instanceof Error && 'code' in error && error.code === 'context_length_exceeded') { + throw new ContextLengthError() + } + throw error + } +} From 6c0d20740fb81894118ad7e201cae368f0208048 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Tue, 12 Nov 2024 13:27:26 -0400 Subject: [PATCH 36/86] fix: careers copywriting (#30442) remote opportunities --- apps/www/pages/careers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/pages/careers.tsx b/apps/www/pages/careers.tsx index 9ec0d3327ca..f1631cd88f6 100644 --- a/apps/www/pages/careers.tsx +++ b/apps/www/pages/careers.tsx @@ -119,7 +119,7 @@ const CareerPage: NextPage = ({ jobs, placeholderJob, contribu We're on a mission to build the best developer platform

- Explore remote possibilities and join our team to help us achieve it. + Explore remote opportunities and join our team to help us achieve it.

@@ -145,9 +122,9 @@ const CreateUserModal = ({ visible, setVisible }: CreateUserModalProps) => { type="password" name="password" placeholder="••••••••" - disabled={isCreatingUser || isLoading} + disabled={isCreatingUser} className="pl-8" - >
+ />
@@ -182,7 +159,7 @@ const CreateUserModal = ({ visible, setVisible }: CreateUserModalProps) => { size="small" htmlType="submit" loading={isCreatingUser} - disabled={!canCreateUsers || isCreatingUser || isLoading} + disabled={!canCreateUsers || isCreatingUser} > Create user diff --git a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx index 81d08ea2a2c..af15700c840 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx @@ -16,7 +16,6 @@ import { useUserSendMagicLinkMutation } from 'data/auth/user-send-magic-link-mut import { useUserSendOTPMutation } from 'data/auth/user-send-otp-mutation' import { useUserUpdateMutation } from 'data/auth/user-update-mutation' import { User } from 'data/auth/users-infinite-query' -import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { timeout } from 'lib/helpers' @@ -58,6 +57,7 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => { } }) + const canUpdateUser = useCheckPermissions(PermissionAction.AUTH_EXECUTE, '*') const canSendMagicLink = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'send_magic_link') const canSendRecovery = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'send_recovery') const canSendOtp = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'send_otp') @@ -76,7 +76,6 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => { const [isDeleteFactorsModalOpen, setIsDeleteFactorsModalOpen] = useState(false) const { data } = useAuthConfigQuery({ projectRef }) - const { data: settings } = useProjectSettingsV2Query({ projectRef }) const mailerOtpExpiry = data?.MAILER_OTP_EXP ?? 0 const minutes = Math.floor(mailerOtpExpiry / 60) @@ -143,24 +142,13 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => { } const handleUnban = () => { - if (!settings) { - return toast.error(`Failed to ban user: Error loading project config`) - } else if (user.id === undefined) { + if (projectRef === undefined) return console.error('Proejct ref is required') + if (user.id === undefined) { return toast.error(`Failed to ban user: User ID not found`) } - const protocol = settings?.app_config?.protocol ?? 'https' - const endpoint = settings?.app_config?.endpoint - const { serviceKey } = getAPIKeys(settings) - - if (!endpoint) return toast.error(`Failed to unban user: Unable to retrieve API endpoint`) - if (!serviceKey?.api_key) return toast.error(`Failed to unban user: Unable to retrieve API key`) - updateUser({ projectRef, - protocol, - endpoint, - serviceApiKey: serviceKey.api_key, userId: user.id, banDuration: 'none', }) @@ -386,7 +374,7 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => { button={{ icon: , text: isBanned ? 'Unban user' : 'Ban user', - disabled: !canRemoveMFAFactors, + disabled: !canUpdateUser, onClick: () => { if (isBanned) { setIsUnbanModalOpen(true) diff --git a/apps/studio/data/auth/user-create-mutation.ts b/apps/studio/data/auth/user-create-mutation.ts index 4484f199cc7..83adf73a3ff 100644 --- a/apps/studio/data/auth/user-create-mutation.ts +++ b/apps/studio/data/auth/user-create-mutation.ts @@ -1,59 +1,31 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' +import { handleError, post } from 'data/fetchers' import { sqlKeys } from 'data/sql/keys' -import { post } from 'lib/common/fetch' -import { IS_PLATFORM } from 'lib/constants' -import { PROJECT_ENDPOINT_PROTOCOL } from 'pages/api/constants' import type { ResponseError } from 'types' import { authKeys } from './keys' export type UserCreateVariables = { - projectRef?: string - protocol: string - endpoint: string - serviceApiKey: string + projectRef: string user: { email: string password: string - autoConfirmUser: string + autoConfirmUser: boolean } } -export type UserCreateResponse = { - id: string - phone: string - role: string - updated_at: string - app_metadata: { - provider: string - providers: string[] - } - aud: string - created_at: string - email: string - email_confirmed_at: string - identities: any[] - user_metadata: any -} - -export async function createUser({ protocol, endpoint, serviceApiKey, user }: UserCreateVariables) { - const response = await post( - `${protocol}://${endpoint}/auth/v1/admin/users`, - { +export async function createUser({ projectRef, user }: UserCreateVariables) { + const { data, error } = await post('/platform/auth/{ref}/users', { + params: { path: { ref: projectRef } }, + body: { email: user.email, password: user.password, email_confirm: user.autoConfirmUser, }, - { - headers: { - apikey: serviceApiKey, - Authorization: `Bearer ${serviceApiKey}`, - }, - } - ) - if (response.error) throw response.error - return response + }) + if (error) handleError(error) + return data } type UserCreateData = Awaited> diff --git a/apps/studio/data/auth/user-update-mutation.ts b/apps/studio/data/auth/user-update-mutation.ts index d6f2e5a7623..19cf3ae3b9f 100644 --- a/apps/studio/data/auth/user-update-mutation.ts +++ b/apps/studio/data/auth/user-update-mutation.ts @@ -1,46 +1,25 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { put } from 'lib/common/fetch' -import { IS_PLATFORM } from 'lib/constants' -import { PROJECT_ENDPOINT_PROTOCOL } from 'pages/api/constants' +import { handleError, patch } from 'data/fetchers' import type { ResponseError } from 'types' import { authKeys } from './keys' export type UserUpdateVariables = { - projectRef?: string - protocol: string - endpoint: string - serviceApiKey: string - + projectRef: string userId: string // For now just support updating banning the user banDuration: number | 'none' // In hours, "none" to unban, otherwise a string in hours e.g "24h" } -export async function updateUser({ - protocol, - endpoint, - serviceApiKey, - userId, - banDuration, -}: UserUpdateVariables) { - // [Joshen] This is probably the only endpoint that needs the put method from lib/common/fetch - // as it's not our internal API. - const response = await put( - `${protocol}://${endpoint}/auth/v1/admin/users/${userId}`, - { ban_duration: typeof banDuration === 'number' ? `${banDuration}h` : banDuration }, - { - headers: { - apikey: serviceApiKey, - Authorization: `Bearer ${serviceApiKey}`, - }, - credentials: undefined, - } - ) +export async function updateUser({ projectRef, userId, banDuration }: UserUpdateVariables) { + const { data, error } = await patch('/platform/auth/{ref}/users/{id}', { + params: { path: { ref: projectRef, id: userId } }, + body: { ban_duration: typeof banDuration === 'number' ? `${banDuration}h` : banDuration }, + }) - if (response.error) throw response.error - return response + if (error) handleError(error) + return data } type UserUpdateData = Awaited> diff --git a/apps/studio/pages/api/auth/[ref]/users.ts b/apps/studio/pages/api/auth/[ref]/users.ts index 5acfcfd044c..0fbcf299d36 100644 --- a/apps/studio/pages/api/auth/[ref]/users.ts +++ b/apps/studio/pages/api/auth/[ref]/users.ts @@ -1,5 +1,6 @@ import { NextApiRequest, NextApiResponse } from 'next' import SqlString from 'sqlstring' +import { createClient } from '@supabase/supabase-js' import { post } from 'lib/common/fetch' import { tryParseInt } from 'lib/helpers' @@ -7,6 +8,8 @@ import { PG_META_URL } from 'lib/constants' import apiWrapper from 'lib/api/apiWrapper' import { constructHeaders } from 'lib/api/apiHelpers' +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -15,10 +18,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { switch (method) { case 'GET': return handleGetAll(req, res) + case 'POST': + return handlePost(req, res) case 'DELETE': return handleDelete(req, res) default: - res.setHeader('Allow', ['GET', 'DELETE']) + res.setHeader('Allow', ['GET', 'POST', 'DELETE']) res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) } } @@ -110,18 +115,18 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(200).json({ total, users: getUsers }) } -const handleDelete = async (req: NextApiRequest, res: NextApiResponse) => { - const headers = constructHeaders(req.headers) +const handlePost = async (req: NextApiRequest, res: NextApiResponse) => { + const { email, password, email_confirm } = req.body + const { data, error } = await supabase.auth.admin.createUser({ email, password, email_confirm }) - const payload = req.body - const query = { query: `DELETE from auth.users where id='${payload.id}';` } + if (error) return res.status(400).json({ error: { message: error.message } }) + return res.status(200).json(data.user) +} - const response = await post(`${PG_META_URL}/query`, query, { headers }) +const handleDelete = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.body + const { data, error } = await supabase.auth.admin.deleteUser(id) - if (response.error) { - console.error('Delete Users POST:', response.error) - return res.status(400).json({ error: response.error }) - } - - return res.status(200).json(response) + if (error) return res.status(400).json({ error: { message: error.message } }) + return res.status(200).json(data.user) } diff --git a/apps/studio/pages/api/auth/[ref]/users/[id]/index.ts b/apps/studio/pages/api/auth/[ref]/users/[id]/index.ts new file mode 100644 index 00000000000..531cf3ba6f7 --- /dev/null +++ b/apps/studio/pages/api/auth/[ref]/users/[id]/index.ts @@ -0,0 +1,29 @@ +import { createClient } from '@supabase/supabase-js' +import { NextApiRequest, NextApiResponse } from 'next' + +import apiWrapper from 'lib/api/apiWrapper' + +const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!) + +export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'PATCH': + return handlePatch(req, res) + default: + res.setHeader('Allow', ['PATCH']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +const handlePatch = async (req: NextApiRequest, res: NextApiResponse) => { + const { id } = req.query + const { ban_duration } = req.body + const { data, error } = await supabase.auth.admin.updateUserById(id as string, { ban_duration }) + + if (error) return res.status(400).json({ error: { message: error.message } }) + return res.status(200).json(data.user) +} From efd536e599599863c3229ff4be5f0c27202aa9e0 Mon Sep 17 00:00:00 2001 From: Kyle Malloy <35405191+kmalloy24@users.noreply.github.com> Date: Tue, 12 Nov 2024 21:17:26 -0500 Subject: [PATCH 38/86] docs: subdomain sso (#30424) * docs:subdomain sso * docs:subdomain sso - lint & format --- .../auth/enterprise-sso/auth-sso-saml.mdx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/apps/docs/content/guides/auth/enterprise-sso/auth-sso-saml.mdx b/apps/docs/content/guides/auth/enterprise-sso/auth-sso-saml.mdx index 59a5c388ae4..7e41fc0adb1 100644 --- a/apps/docs/content/guides/auth/enterprise-sso/auth-sso-saml.mdx +++ b/apps/docs/content/guides/auth/enterprise-sso/auth-sso-saml.mdx @@ -421,3 +421,47 @@ If you really need access to the key, please [open a support ticket](https://sup Yes, Supabase supports multi-tenant Single Sign-On (SSO) using SAML 2.0. While the dashboard displays only one SAML field, you can set up multiple SAML connections using the Supabase CLI. Each connection is assigned a unique `sso_provider_id`, which is included in the user's JWT and can be used in Row Level Security (RLS) policies. You can configure custom attribute mappings for each connection to include tenant-specific information, such as roles. This setup allows you to implement multi-tenant SSO for multiple clients or organizations within a single application. For example, if you have an app with multiple clients using different Azure Active Directories, you can create separate SAML connections for each and use the `sso_provider_id` to manage access and apply appropriate security policies. + +### Is multi-subdomain SSO with SAML supported? + +Yes, also referred to as [cross-origin authentication within the same site](https://web.dev/articles/same-site-same-origin). To redirect to a URL other than the [Site URL](https://supabase.com/docs/guides/auth/redirect-urls), following the SAML response from the IdP, the `redirectTo` option can be added to [`signInWithSSO`](https://supabase.com/docs/reference/javascript/auth-signinwithsso). + +```ts + const { data, error } = await supabase.auth.signInWithSSO({ + domain: 'company.com' + options: { + redirectTo: `https://app.company.com/callback`, + }, + }) +``` + +When redirecting to a URL other than the Site URL, a `/callback` endpoint is necessary to process the auth code from the IdP and exchange it for a session. This assumes the [Supabase SSR client](https://supabase.com/docs/guides/auth/server-side/creating-a-client) has already been configured. + + + + + +```ts +import { error, redirect } from '@sveltejs/kit' +import type { RequestHandler } from './$types' + +export const GET: RequestHandler = async ({ url, locals }) => { + const code = url.searchParams.get('code') + + if (!code) { + error(400, 'No authorization code provided') + } + + const { error: tokenExchangeError } = await locals.supabase.auth.exchangeCodeForSession(code) + + if (tokenExchangeError) { + error(400, 'Failed to exchange authorization code for session') + } + + redirect(303, '/') +} +``` + + + + From aad6c48d037352e4b2d102b7a6d5bc6c1cdaa6c4 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Wed, 13 Nov 2024 04:32:07 +0100 Subject: [PATCH 39/86] remove debugger (#30440) rm debugger --- apps/studio/components/grid/components/editor/SelectEditor.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/studio/components/grid/components/editor/SelectEditor.tsx b/apps/studio/components/grid/components/editor/SelectEditor.tsx index 0aa6a08e6c2..3ea7cbe16b1 100644 --- a/apps/studio/components/grid/components/editor/SelectEditor.tsx +++ b/apps/studio/components/grid/components/editor/SelectEditor.tsx @@ -23,7 +23,6 @@ export function SelectEditor({ const value = row[column.key as keyof TRow] as unknown as string function onChange(event: any) { - debugger if (!event.target.value || event.target.value == '') { onRowChange({ ...row, [column.key]: null }, true) } else { From 297fc76bfd0fee4e3f0ceb109048cdaf4906fdc0 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Wed, 13 Nov 2024 14:03:48 +0800 Subject: [PATCH 40/86] Chore/update sql side panel (#30050) * init * Update inner-side-menu.mdx * chore: update SQL sidebar to use ui pattern components * mor * Update * Update index.tsx --------- Co-authored-by: Joshen Lim --- .../layouts/SQLEditorLayout/SQLEditorMenu.tsx | 137 ++++++++------- .../SQLEditorNavV2/SQLEditorNav.tsx | 162 +++++++++--------- packages/config/tailwind.config.js | 3 + packages/ui-patterns/InnerSideMenu/index.tsx | 15 +- 4 files changed, 164 insertions(+), 153 deletions(-) diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx index 16c4cc691d1..dce32b418ac 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx @@ -36,11 +36,10 @@ export const SQLEditorMenu = ({ onViewOngoingQueries }: SQLEditorMenuProps) => { const router = useRouter() const { profile } = useProfile() const project = useSelectedProject() - const { ref, id: activeId } = useParams() + const { ref } = useParams() const snapV2 = useSqlEditorV2StateSnapshot() const [searchText, setSearchText] = useState('') - const [selectedQueries, setSelectedQueries] = useState([]) const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', { resource: { type: 'sql', owner_id: profile?.id }, @@ -80,78 +79,76 @@ export const SQLEditorMenu = ({ onViewOngoingQueries }: SQLEditorMenuProps) => { } return ( - <> -
-
-
- - setSearchText(e.target.value)} +
+
+
+ + setSearchText(e.target.value)} + > + snapV2.setOrder(value)} > - snapV2.setOrder(value)} - > - - Alphabetical - - - Created At - - - - - - -
- -
- - Templates - - - Quickstarts - -
- - + + Alphabetical + + + Created At + + + + + + +
-
- +
+ + Templates + + + Quickstarts +
+ +
- + +
+ +
+
) } diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx index 16d3174d5ac..9562476804d 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx @@ -1,4 +1,4 @@ -import { ChevronRight, Eye, EyeOffIcon, Heart, Unlock } from 'lucide-react' +import { Eye, EyeOffIcon, Heart, Unlock } from 'lucide-react' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { toast } from 'sonner' @@ -31,18 +31,15 @@ import { useSnippets, useSqlEditorV2StateSnapshot, } from 'state/sql-editor-v2' +import { Separator, Skeleton, TreeView } from 'ui' import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, - CollapsibleContent_Shadcn_, - CollapsibleTrigger_Shadcn_, - Collapsible_Shadcn_, - Separator, - TreeView, -} from 'ui' + InnerSideBarEmptyPanel, + InnerSideMenuCollapsible, + InnerSideMenuCollapsibleContent, + InnerSideMenuCollapsibleTrigger, + InnerSideMenuSeparator, +} from 'ui-patterns' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { ROOT_NODE, formatFolderResponseForTreeView } from './SQLEditorNav.utils' import { SQLEditorTreeViewItem } from './SQLEditorTreeViewItem' @@ -75,11 +72,6 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) => const [selectedSnippetToDownload, setSelectedSnippetToDownload] = useState() const [selectedFolderToDelete, setSelectedFolderToDelete] = useState() - const COLLAPSIBLE_TRIGGER_CLASS_NAMES = - 'flex items-center gap-x-2 px-4 [&[data-state=open]>svg]:!rotate-90' - const COLLAPSIBLE_ICON_CLASS_NAMES = 'text-foreground-light transition-transform duration-200' - const COLLASIBLE_HEADER_CLASS_NAMES = 'text-foreground-light font-mono text-sm uppercase' - // ======================================================= // [Joshen] Set up favorites, shared, and private snippets // ======================================================= @@ -341,27 +333,24 @@ export const SQLEditorNav = ({ searchText: _searchText }: SQLEditorNavProps) => return ( <> - - + {((numProjectSnippets === 0 && searchText.length === 0) || numProjectSnippets > 0) && ( <> - - - - - Shared{numProjectSnippets > 0 && ` (${numProjectSnippets})`} - - - + + 0 ? ` (${numProjectSnippets})` : ''}`} + /> + {numProjectSnippets === 0 ? ( -
- - No shared queries - - Share queries with your team by right-clicking on the query. - - -
+ ) : ( )} /> )} -
-
- + + + )} {((numFavoriteSnippets === 0 && searchText.length === 0) || numFavoriteSnippets > 0) && ( <> - - - - - Favorites{numFavoriteSnippets > 0 && ` (${numFavoriteSnippets})`} - - - + + 0 ? ` (${numFavoriteSnippets})` : ''}`} + /> + {numFavoriteSnippets === 0 ? ( -
- - No favorite queries - + Save a query to favorites for easy accessbility by clicking the{' '} {' '} icon. - - -
+ + } + /> ) : ( )} /> )} -
-
- + + + )} - - - - - PRIVATE - {numPrivateSnippets > 0 && ` (${numPrivateSnippets})`} - - - + + 0 ? ` (${numPrivateSnippets})` : ''}`} + /> + {!snapV2.loaded[projectRef as string] ? ( -
- -
+ <> +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ ) : folders.length === 0 && numPrivateSnippets === 0 ? ( -
- - No queries created yet - - Queries will be automatically saved once you start writing in the editor. - - -
+ ) : ( )} /> )} -
-
+ + diff --git a/packages/config/tailwind.config.js b/packages/config/tailwind.config.js index 9254c114124..d0d5df21861 100644 --- a/packages/config/tailwind.config.js +++ b/packages/config/tailwind.config.js @@ -420,6 +420,9 @@ const uiConfig = ui({ require('@tailwindcss/typography'), require('tailwindcss-animate'), plugin(motionSafeTransition), + function ({ addVariant }) { + addVariant('not-disabled', '&:not(:disabled)') + }, ], }) diff --git a/packages/ui-patterns/InnerSideMenu/index.tsx b/packages/ui-patterns/InnerSideMenu/index.tsx index 89de8af534e..d478cc2ef61 100644 --- a/packages/ui-patterns/InnerSideMenu/index.tsx +++ b/packages/ui-patterns/InnerSideMenu/index.tsx @@ -39,7 +39,13 @@ const InnerSideMenuCollapsible = forwardRef< ElementRef, React.ComponentPropsWithoutRef >(({ ...props }, ref) => { - return + return ( + + ) }) const InnerSideMenuCollapsibleTrigger = forwardRef< @@ -73,10 +79,7 @@ const InnerSideMenuCollapsibleContent = forwardRef< ) }) @@ -220,7 +223,7 @@ const InnerSideBarEmptyPanel = forwardRef< HTMLDivElement, React.ComponentPropsWithoutRef<'div'> & { title: string - description?: string + description?: string | React.ReactNode illustration?: React.ReactNode actions?: React.ReactNode } From 91523a78ea68eb12c173a5db9eeabf83087ec70b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Wed, 13 Nov 2024 16:53:28 +0800 Subject: [PATCH 41/86] fix: org billing link (#30456) --- .../components/interfaces/Billing/NoProjectsOnPaidOrgInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Billing/NoProjectsOnPaidOrgInfo.tsx b/apps/studio/components/interfaces/Billing/NoProjectsOnPaidOrgInfo.tsx index 34e1d52cb90..870af2ef2d4 100644 --- a/apps/studio/components/interfaces/Billing/NoProjectsOnPaidOrgInfo.tsx +++ b/apps/studio/components/interfaces/Billing/NoProjectsOnPaidOrgInfo.tsx @@ -31,7 +31,7 @@ export const NoProjectsOnPaidOrgInfo = ({ organization }: NoProjectsOnPaidOrgInf } /> From f7518c28961fc7848e0d6d6b85f8db42bbcc437b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thor=20=E9=9B=B7=E7=A5=9E=20Schaeff?= <5748289+thorwebdev@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:57:25 +0800 Subject: [PATCH 42/86] chore: background tasks updates (#30454) * chore: bacground task docs and example. * chore: add video. --- .../guides/functions/background-tasks.mdx | 11 +-- .../background-upload-storage/index.ts | 76 +++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 examples/edge-functions/supabase/functions/background-upload-storage/index.ts diff --git a/apps/docs/content/guides/functions/background-tasks.mdx b/apps/docs/content/guides/functions/background-tasks.mdx index 2ad76d7028d..7eb79415be5 100644 --- a/apps/docs/content/guides/functions/background-tasks.mdx +++ b/apps/docs/content/guides/functions/background-tasks.mdx @@ -3,6 +3,7 @@ id: 'function-background-tasks' title: 'Background Tasks' description: 'How to run background tasks in an Edge Function outside of the request handler' subtitle: 'How to run background tasks in an Edge Function outside of the request handler' +tocVideo: 'rSKBTJxG9VA' --- Edge Function instances can process background tasks outside of the request handler. Background tasks are useful for asynchronous operations like uploading a file to Storage, updating a database, or sending events to a logging service. You can respond to the request immediately and leave the task running in the background. @@ -12,18 +13,18 @@ A background task can run until the Edge Function instance hits its [wall-clock Here's an example of defining a background task using a custom event. ```ts -// define a custom event +// Define a custom event type for the background task. class MyBackgroundTaskEvent extends Event { - readonly taskPromise: Promise + readonly taskPromise: Promise - constructor(taskPromise) { + constructor(taskPromise: Promise) { super('myBackgroundTask') this.taskPromise = taskPromise } } -globalThis.addEventListener('myBackgroundTask', async (event: MyBackgroundTaskEvent) => { - const res = await event.taskPromise +globalThis.addEventListener('myBackgroundTask', async (event) => { + const res = await (event as MyBackgroundTaskEvent).taskPromise console.log(await res.json()) }) diff --git a/examples/edge-functions/supabase/functions/background-upload-storage/index.ts b/examples/edge-functions/supabase/functions/background-upload-storage/index.ts new file mode 100644 index 00000000000..6ab56876341 --- /dev/null +++ b/examples/edge-functions/supabase/functions/background-upload-storage/index.ts @@ -0,0 +1,76 @@ +import { createClient } from 'jsr:@supabase/supabase-js@2.45.6' +import OpenAI from 'https://deno.land/x/openai@v4.68.2/mod.ts' + +const client = new OpenAI({ apiKey: Deno.env.get('OPENAI_API_KEY')! }) + +const supabase = createClient( + Deno.env.get('SUPABASE_URL')!, + Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! +) + +type StorageFileApi = ReturnType +type StorageUploadPromise = ReturnType + +class MyBackgroundTaskEvent extends Event { + readonly taskPromise: StorageUploadPromise + + constructor(taskPromise: StorageUploadPromise) { + super('myBackgroundTask') + this.taskPromise = taskPromise + } +} + +globalThis.addEventListener('myBackgroundTask', async (event) => { + const { data, error } = await (event as MyBackgroundTaskEvent).taskPromise + console.log({ data, error }) +}) + +Deno.serve(async (req) => { + const url = new URL(req.url) + const params = new URLSearchParams(url.search) + const text = params.get('text') + + if (!text) { + return new Response(JSON.stringify({ error: 'Text parameter is required' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }) + } + + try { + const response = await client.audio.speech.create({ + model: 'tts-1', + voice: 'nova', + input: text, + }) + + const stream = response.body + if (!stream) { + throw new Error('No stream') + } + + // Branch stream to Supabase Storage + const [browserStream, storageStream] = stream.tee() + + // Upload to Supabase Storage + const storageUploadPromise = supabase.storage + .from('videos') + .upload(`audio-stream_${Date.now()}.mp3`, storageStream, { + contentType: 'audio/mp3', + }) + const event = new MyBackgroundTaskEvent(storageUploadPromise) + globalThis.dispatchEvent(event) + + return new Response(browserStream, { + headers: { + 'Content-Type': 'audio/mp3', + }, + }) + } catch (error) { + console.log('error', { error }) + return new Response(JSON.stringify({ error: error.message }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }) + } +}) From dd936695fa07f3691d8b58e4f392b53f54afcb5d Mon Sep 17 00:00:00 2001 From: Rodrigo Mansueli Date: Wed, 13 Nov 2024 08:42:35 -0300 Subject: [PATCH 43/86] feat: dynamic function blog post (#30422) * feat blog post * Update 2024-11-13-supabase-dynamic-functions.mdx * Update 2024-11-13-supabase-dynamic-functions.mdx --- .../2024-11-13-supabase-dynamic-functions.mdx | 473 ++++++++++++++++++ .../dynamic-og.png | Bin 0 -> 21971 bytes 2 files changed, 473 insertions(+) create mode 100644 apps/www/_blog/2024-11-13-supabase-dynamic-functions.mdx create mode 100644 apps/www/public/images/blog/2024-11-13-supabase-dynamic-functions/dynamic-og.png diff --git a/apps/www/_blog/2024-11-13-supabase-dynamic-functions.mdx b/apps/www/_blog/2024-11-13-supabase-dynamic-functions.mdx new file mode 100644 index 00000000000..71b5e7448a1 --- /dev/null +++ b/apps/www/_blog/2024-11-13-supabase-dynamic-functions.mdx @@ -0,0 +1,473 @@ +--- +title: 'Executing Dynamic JavaScript Code on Supabase with Edge Functions' +description: 'Learn how to execute dynamic JavaScript code on Supabase using Edge Functions.' +author: rodrigo_mansueli +image: 2024-11-13-supabase-dynamic-functions/dynamic-og.png +thumb: 2024-11-13-supabase-dynamic-functions/dynamic-og.png +categories: + - engineering +tags: + - edge-funcion + - funcions + - AI +date: '2024-11-13' +toc_depth: 3 +--- + +We're always looking for ways to improve the developer experience and reduce complexity across your application development pipeline. One way you can use Supabase to do that is with dynamic JavaScript in Edge Functions. This greatly increases the versatility of your edge functions and reduces the need for you to redeploy your functions if you need to change business logic. + +### Introduction to Edge Functions + +Edge Functions in Supabase are serverless functions that execute in response to HTTP requests. These functions are deployed at the edge, meaning they run close to the user's location, resulting in faster response times. + +### Why Use Dynamic Code Execution? + +Dynamic code execution allows you to modify and run JavaScript code on the fly without having to redeploy your function each time the code changes. This is particularly useful when you need the flexibility to execute different logic depending on the incoming request, without incurring the overhead of redeployment. + +### Prerequisites + +To follow along, you will need: + +- A Supabase project +- Supabase CLI installed on your local machine +- Orb Stack or Docker Desktop installed on your local machine +- Environment variables set up in Vault, ensuring it passes validation in the function (e.g., `service_role`) + +> Edge Functions defaults to the verification of the JWT, so it could be called with the ANON API Key. Make sure to implement proper security measures. + +## Install the SQL script from the repo + +We have a repo with the SQL script to create helper functions to support the dynamic execution of JavaScript code. You can find the repo here: [supa-dynamic](https://github.com/mansueli/supa-dynamic) + +Install the SQL script `supa-dynamic--0.1.sql` from the repo in your Supabase project. (You can copy and paste the code from the repo into the SQL editor in your Supabase project.) +These are the functions we'll use to execute the JavaScript code: + +- `edge.http_request(url text, method text, headers jsonb, params jsonb, payload jsonb, timeout_ms integer) RETURNS jsonb`: Makes an HTTP request with the specified parameters. +- `edge_wrapper(code text) RETURNS text`: Executes the provided JavaScript code. +- `edge.get_secret(secret_name text) RETURNS text`: Retrieves a secret from Vault. + +### Deep Dive into the helper functions (optional) + +You can skip this section if you are only interested in using the dynamic execution of JavaScript code. However, if you want to understand how the helper functions work, keep reading. + +### `edge.http_request` Function + +This function handles the actual HTTP request and processes the response. It ensures consistency in response format. + +```sql +CREATE OR REPLACE FUNCTION edge.http_request( + url TEXT, + method TEXT DEFAULT 'POST', + headers JSONB DEFAULT '{"Content-Type": "application/json"}'::jsonb, + params JSONB DEFAULT '{}'::jsonb, + payload JSONB DEFAULT '{}'::jsonb, + timeout_ms INTEGER DEFAULT 5000 +) RETURNS jsonb AS $$ +DECLARE + http_response extensions.http_response; + status_code integer := 0; + response_json jsonb; + response_text text; + header_array extensions.http_header[]; + request extensions.http_request; +BEGIN + -- Set the timeout option + IF timeout_ms > 0 THEN + PERFORM http_set_curlopt('CURLOPT_TIMEOUT_MS', timeout_ms::text); + END IF; + + -- Convert headers JSONB to http_header array + SELECT array_agg(extensions.http_header(key, value::text)) + FROM jsonb_each_text(headers) + INTO header_array; + + -- Construct the http_request composite type + request := ROW(method, url, header_array, 'application/json', payload::text)::extensions.http_request; + + -- Make the HTTP request + http_response := http(request); + status_code := http_response.status; + + -- Attempt to extract JSONB response content + BEGIN + response_json := http_response.content::jsonb; + EXCEPTION + WHEN others THEN + response_text := http_response.content; + response_json := jsonb_build_object('status_code', status_code, 'response', response_text); + END; + + RETURN jsonb_build_object('status_code', status_code, 'response', response_json); +END; +$$ LANGUAGE plpgsql; + +``` + +### `edge_wrapper` Function + +The `edge_wrapper` function manages HTTP requests with features like retries, custom headers, and region selection. Below are the parameters it accepts: + +- **`url`**: The endpoint to call. +- **`method`**: HTTP method, defaulting to `POST`. +- **`headers`**: Custom headers to include, including region information. +- **`timeout_ms`**: Timeout duration in milliseconds. +- **`max_retries`**: Maximum retry attempts for the request. + +```sql +CREATE OR REPLACE FUNCTION edge.edge_wrapper( + url TEXT, + method TEXT DEFAULT 'POST', + headers JSONB DEFAULT '{"Content-Type": "application/json"}'::jsonb, + params JSONB DEFAULT '{}'::jsonb, + payload JSONB DEFAULT '{}'::jsonb, -- Payload as JSONB + timeout_ms INTEGER DEFAULT 5000, + max_retries INTEGER DEFAULT 0, + allowed_regions TEXT[] DEFAULT NULL +) RETURNS jsonb AS $$ +DECLARE + retry_count INTEGER := 0; + retry_delays DOUBLE PRECISION[] := ARRAY[0, 0.250, 0.500, 1.000, 2.500, 5.000]; + succeeded BOOLEAN := FALSE; + current_region_index INTEGER := 1; -- Start index at 1 for PostgreSQL array + combined_headers JSONB; + response_json JSONB; +BEGIN + -- Validate headers, params, and payload are JSON objects + IF headers IS NULL OR NOT jsonb_typeof(headers) = 'object' THEN + RAISE EXCEPTION 'Invalid headers parameter: %', headers; + END IF; + + IF params IS NULL OR NOT jsonb_typeof(params) = 'object' THEN + RAISE EXCEPTION 'Invalid params parameter: %', params; + END IF; + + IF payload IS NULL OR NOT jsonb_typeof(payload) = 'object' THEN + RAISE EXCEPTION 'Invalid payload parameter: %', payload; + END IF; + + -- Validate allowed_regions if provided + IF allowed_regions IS NOT NULL AND cardinality(allowed_regions) = 0 THEN + RAISE EXCEPTION 'allowed_regions parameter cannot be an empty array'; + END IF; + + -- Check if retry_delays has enough elements + IF cardinality(retry_delays) < max_retries + 1 THEN + RAISE EXCEPTION 'retry_delays array must have at least % elements', max_retries + 1; + END IF; + + -- Retry loop + WHILE NOT succeeded AND retry_count <= max_retries LOOP + combined_headers := headers; + + -- Set x-region header if allowed_regions is provided + IF allowed_regions IS NOT NULL AND cardinality(allowed_regions) > 0 THEN + combined_headers := combined_headers || jsonb_build_object('x-region', allowed_regions[current_region_index]); + END IF; + + -- Sleep if not the first attempt + IF retry_count > 0 THEN + PERFORM pg_sleep(retry_delays[retry_count]); + END IF; + + retry_count := retry_count + 1; + + -- Increment region index, wrapping around if necessary + IF allowed_regions IS NOT NULL AND cardinality(allowed_regions) > 0 THEN + current_region_index := current_region_index + 1; + IF current_region_index > cardinality(allowed_regions) THEN + current_region_index := 1; + END IF; + END IF; + + BEGIN + RAISE WARNING 'headers:%s', combined_headers; + + -- Call the simplified HTTP request function + response_json := edge.http_request(url, method, combined_headers, params, payload, timeout_ms); + + -- Check the status code + IF (response_json->>'status_code')::INTEGER < 500 THEN + succeeded := TRUE; + END IF; + EXCEPTION + WHEN OTHERS THEN + IF retry_count > max_retries THEN + RAISE EXCEPTION 'HTTP request failed after % retries. SQL Error: { %, % }', + max_retries, SQLERRM, SQLSTATE; + END IF; + END; + END LOOP; + + RETURN response_json; +END; +$$ LANGUAGE plpgsql; + +``` + +To securely manage secrets, you will need to set your `service_role_key` in Vault. Here’s how you can create a function to retrieve secrets: + +```sql +CREATE OR REPLACE FUNCTION edge.get_secret(secret_name text) RETURNS text + LANGUAGE "plpgsql" + AS $$ +DECLARE + decrypted text; +BEGIN + IF current_setting('request.jwt.claims', true)::jsonb->>'role' = 'service_role' OR current_user = 'postgres' THEN + SELECT decrypted_secret + INTO decrypted + FROM vault.decrypted_secrets + WHERE name = secret_name; + RETURN decrypted; + ELSE + RAISE EXCEPTION 'Access denied: only service_role or postgres user can execute this function.'; + END IF; +END; +$$; +``` + +This function can retrieve the `service_role` secret from [Vault](https://supabase.com/dashboard/project/_/settings/vault/secrets), it also ensures that only authorized roles can access sensitive environment variables. + +## Setting Up the Edge Function + +Let's dive into the code and set up our dynamic JavaScript executor Edge Function using Deno. Below is an overview of how to accomplish this. + +### Code Walkthrough + +We'll create a function named `multi-purpose`: + +```jsx +supabase functions new multi-purpose +``` + +Now, we'll edit the code adding verification and the eval function, including the supabase client so we have it ready without the need to import. + +```tsx +import "jsr:@supabase/functions-js/edge-runtime.d.ts"; + +// Import the supabase client +import { createClient } from ""; + +console.log("===\\n\\tBooted Edge Worker!\\n===\\n"); +const supabase_url = Deno.env.get("SUPABASE_URL") ?? ""; +const service_role = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); +// Set the permission to service_role key: +const supabase = createClient(supabase_url, service_role); +// This allows us to use Supabase.ai in the function +const session = new Supabase.ai.Session('gte-small'); + +Deno.serve(async (req: Request) => + const authorization = req.headers.get("Authorization"); + if (!authorization) throw new Error("Authorization header is missing."); + // Ensures that the function is called with service_role to prevent missuse + if (!authorization.includes(service_role)) { + throw new Error("Authorization header is invalid."); + } + + const { code } = await req.json(); + try { + // Wrap the provided code in an async function context + const asyncFunction = new Function('supabase', ` + return (async () => { + ${code.replace(/\\\\/g, '')} + })(); + `); + // Pass the Supabase client as the scope for the function to use: + const data = await asyncFunction(supabase); + console.log(data); + return new Response( + JSON.stringify({ data }), + { headers: { 'Content-Type': 'application/json', 'Connection': 'keep-alive' } }, + ); + } catch (error) { + console.error("Error executing user code:", error); + return new Response( + JSON.stringify({ error: "An error occurred -> " + error.message }), + { status: 500, headers: { "Content-Type": "application/json" } } + ); + } +}); + +``` + +> Note: If you need more details, check the full guide to [create an edge function](https://supabase.com/docs/guides/functions/quickstart#create-an-edge-function). + +### Step-by-Step Walkthrough + +1. **Validate Authorization**: First, we ensure the request contains a valid authorization header. (this prevents calls from anon users) + +```jsx +const authorization = req.headers.get('Authorization') +if (!authorization) throw new Error('Authorization header is missing.') +// Ensures that the function is called with service_role to prevent missuse +if (!authorization.includes(service_role)) { + throw new Error('Authorization header is invalid.') +} +``` + +1. **Receive JavaScript Code Payload**: Extract the `code` from the request body. + +```jsx +const { code } = await req.json() +``` + +1. **Wrap Code in Async Context**: Use `new Function()` to create an async function that executes the incoming JavaScript code. This allows async calls in the code to be executed: + +```jsx +try { + // Wrap the provided code in an async function context + const asyncFunction = new Function('supabase', ` + return (async () => { + ${code.replace(/\\\\/g, '')} + })(); + `); +} +``` + +1. **Execute and Return Results**: Run the JavaScript code, which can interact with Supabase via the provided client, and return the results. + +```jsx +// Pass the Supabase client as the scope for the function to use: +const data = await asyncFunction(supabase) +console.log(data) +return new Response(JSON.stringify({ data }), { + headers: { 'Content-Type': 'application/json', Connection: 'keep-alive' }, +}) +``` + +### Deploying the Edge Function + +To deploy this Edge Function, you'll need to use the Supabase CLI. Ensure you have Docker installed and running on your local machine. Follow these steps to deploy: + +1. **Install the Supabase CLI**: If you haven't already, install the Supabase CLI by following the instructions in the [Supabase CLI Documentation](https://supabase.com/docs/guides/cli). +2. **Log In to Supabase**: Use the command `supabase login` to authenticate your account. +3. **Deploy the Function**: Run the command `supabase functions deploy ` to deploy your Edge Function. Replace `` with the desired name for your function. + +### Setting Environment Variables in Vault + +## Creating the main function to interact with the edge function + +We are using the helper functions defined earlier to create a function that interacts with the edge function. This function will execute the dynamic JavaScript code and return the results. +This is the main function that will be used to execute the dynamic JavaScript code and return the results. + +### `edge.exec` Function + +The `edge.exec` is a simple function leverages `edge_wrapper` to execute dynamic JavaScript code. Here's an example of how it is structured: + +```sql +CREATE OR REPLACE FUNCTION edge.exec(data text) RETURNS JSONB LANGUAGE plpgsql +AS $function$ +DECLARE + custom_headers JSONB; +-- Example restricting regions available to Europe + allowed_regions TEXT[] := ARRAY['eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-north-1', 'eu-central-1']; +BEGIN + -- Set headers with anon key and Content-Type + custom_headers := jsonb_build_object( + 'Authorization', 'Bearer ' || edge.get_secret('service_role_key'), + 'Content-Type', 'application/json', + 'x-region', allowed_regions + ); + -- Call edge_wrapper function with default values + RETURN edge.edge_wrapper( + url := ('https://.supabase.co/functions/v1/multi-purpose'), + headers := custom_headers, + payload := jsonb_build_object('code', data), + max_retries := 5, + allowed_regions := allowed_regions + ); +END; +$function$; + +``` + +### Executing Dynamic JavaScript Code + +The key to executing the dynamic JavaScript code is wrapping it in an `async` function context using `new Function()`. This approach lets you evaluate the code in isolation while retaining access to the `supabase` client for interacting with your database. You can check the examples of how to use this calling the [supabase client](https://www.notion.so/Executing-Dynamic-JavaScript-Code-on-Supabase-with-Edge-Functions-1125004b775f80ca97d4c367a2cfcd7d?pvs=21) or even [generating embeddings](https://www.notion.so/Executing-Dynamic-JavaScript-Code-on-Supabase-with-Edge-Functions-1125004b775f80ca97d4c367a2cfcd7d?pvs=21). + +### Example of Using Supabase Client Libraries + +To demonstrate the execution of dynamic JavaScript, you can use the Supabase client libraries within the SQL context. Here’s an example query: + +```sql +SELECT edge.exec( + $js$ + const { data, error } = await supabase.rpc('postgres_function', {'foo': 'bar'}); + if (error) { + return new Response(JSON.stringify({ error: "An error occurred ->" + error.message }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); + } + return data; + $js$ +); + +``` + +## Using the Edge Function in Practice + +### Example: Generating Embeddings + +The `edge.exec` function allows for dynamic JavaScript execution, such as interacting with an AI session to generate embeddings. When executed, the JavaScript code within the SQL context runs through the edge function, returning results to the database. + +```sql +select edge.exec( +$js$ + +const session = new Supabase.ai.Session('gte-small'); +return await session.run('hello world'); + +$js$); +``` + +You can also create a Postgres function to generate embeddings: + +``` +CREATE OR REPLACE FUNCTION edge.generate_embedding(input_text TEXT) RETURNS JSONB AS $$ +DECLARE + response JSONB; +BEGIN + -- Call the edge function to generate the embedding for the provided text + response := edge.exec( + format( + $js$ + const session = new Supabase.ai.Session('gte-small'); + return await session.run(%L); + $js$, + input_text + ) + ); + RETURN response->'response'->'data'; +END; +$$ LANGUAGE plpgsql; + + select edge.generate_embedding('The quick brown fox jumps over the lazy dog'); + +-- response: +-- [-0.07254139333963394,-0.02173878252506256,0.042930446565151215,0.04853367060422897,0.015609614551067352,0.02912059798836708,0.0371023565530777,0.05054798722267151,0.0035842431243509054,0.0015563230263069272,0.0009484672918915749,-0.09247169643640518,0.04190639406442642,0.05874202027916908,-0.012341015040874481,0.01661474071443081,-0.013452880084514618,0.003742767730727792,-0.07664268463850021,0.03231268376111984,0.0006968052475713193,-0.06508929282426834,-0.04956015944480896,-0.014327225275337696,0.03270547464489937,0.01635774038732052,-0.022707758471369743,-0.007586371619254351,-0.03548099845647812,-0.17844657599925995,0.03325255215167999,-0.07009242475032806,0.02982083335518837,-0.05649203434586525,-0.006693259347230196,-0.02781110256910324,-0.01687553897500038,0.04976152256131172,-0.015715090557932854,0.038247860968112946,0.040495794266462326,-0.007263457402586937,-0.019288228824734688,-0.0527581050992012,-0.0065462407656013966,-0.022786622866988182,-0.04975651577115059,-0.04053974151611328,0.03047902137041092,-0.05064946785569191,-0.023929744958877563,-0.03891737014055252,0.03785012289881706,-0.0133274607360363,0.03001898154616356,-0.007281183265149593,0.060004156082868576,0.017414024099707603,0.025516854599118233,0.029599720612168312,0.02893918938934803,0.03455337509512901,-0.14698833227157593,0.09387505799531937,0.05768263339996338,0.019130567088723183,-0.0380706787109375,-0.04105521738529205,0.008963614702224731,0.012743324972689152,0.009223062545061111,0.060711149126291275,0.007398003712296486,0.04229794815182686,0.046996768563985825,-0.003397924592718482,0.00808036606758833,0.022617157548666,-0.01847437582910061,0.0026343590579926968,-0.010598739609122276,-0.037673674523830414,-0.04375630244612694,-0.0007789010996930301,-0.007935777306556702,-0.03272915259003639,0.021433845162391663,-0.07967976480722427,0.06888656318187714,0.07489841431379318,-0.02783842757344246,-0.006374717690050602,-0.035476282238960266,0.006344574969261885,-0.03357071802020073,-0.036727335304021835,0.012309364043176174,-0.00006389369809767231,-0.053050097078084946,0.19709722697734833,-0.05575009435415268,0.05757850036025047,0.0951322615146637,-0.04633559286594391,0.03476420044898987,0.012983368709683418,0.0004390157700981945,0.010212302207946777,-0.012741461396217346,0.014706282876431942,0.03321540355682373,-0.006495281588286161,0.041682176291942596,0.003406582633033395,0.02581774815917015,-0.0007246752502396703,0.011133069172501564,0.08353550732135773,0.006477882619947195,0.00224463758058846,0.020395604893565178,-0.013416256755590439,0.05663946643471718,-0.028388522565364838,0.019082417711615562,-0.08387858420610428,0.054498571902513504,0.10694538056850433,0.06286843866109848,0.03180928900837898,0.037740662693977356,-0.07479764521121979,0.010231229476630688,-0.04866624251008034,0.004061027429997921,0.0362103171646595,-0.009540606290102005,0.00915283989161253,0.031154874712228775,-0.04876647889614105,-0.015956921502947807,-0.1429857611656189,-0.01470054779201746,-0.09399641305208206,-0.019157350063323975,0.02896934375166893,-0.018669532611966133,0.014991801232099533,-0.06764508783817291,0.027312103658914566,-0.003859955817461014,0.025718173012137413,-0.018675100058317184,-0.016409857198596,-0.021459592506289482,0.004702075384557247,-0.0323822982609272,0.10394860059022903,-0.020106177777051926,-0.008876764215528965,-0.027185838669538498,0.0003392586368136108,-0.009877108968794346,-0.0004303457390051335,0.04185814782977104,-0.05188998952507973,-0.021185973659157753,0.00026368125691078603,-0.02180171199142933,-0.03400561958551407,0.020068379119038582,0.034275852143764496,-0.10943055897951126,0.031987469643354416,0.054017845541238785,-0.009243185631930828,-0.07103140652179718,0.00785127654671669,-0.0040434580296278,-0.05036382004618645,0.07858535647392273,-0.08356015384197235,-0.06914680451154709,0.06180981919169426,0.043073058128356934,-0.020246226340532303,-0.015496478416025639,-0.005946696270257235,0.006562687456607819,0.04845070466399193,-0.029123008251190186,0.02194702997803688,0.002446065191179514,-0.06825454533100128,-0.07056894898414612,0.01598423719406128,-0.04185032472014427,-0.01633128523826599,0.014294272288680077,-0.01768324337899685,0.05590462312102318,-0.044063832610845566,0.02461099997162819,0.0006756667862646282,0.07429251074790955,0.011551265604794025,0.014212443493306637,-0.02237367257475853,0.039057254791259766,0.000325449975207448,-0.004185846075415611,-0.003040974261239171,0.01800958439707756,-0.02479490265250206,-0.019247515127062798,0.04366869106888771,-0.027130864560604095,0.018955133855342865,0.03239727392792702,0.03226468712091446,0.06487660109996796,-0.06456360220909119,0.0006639647181145847,-0.20788206160068512,0.05066373199224472,-0.012870946899056435,-0.034873317927122116,0.023824242874979973,-0.02305314689874649,0.030056791380047798,-0.06937119364738464,0.0642433762550354,0.05418730527162552,0.06050065532326698,-0.04655877873301506,-0.026898164302110672,-0.003803820814937353,0.002598312683403492,0.1081414744257927,0.014850604347884655,0.013619652017951012,0.013523285277187824,-0.0016119466163218021,-0.00329813570715487,0.002907108049839735,0.014589778147637844,-0.048919934779405594,0.056754376739263535,-0.03171522915363312,0.2308642566204071,0.08356188982725143,0.05350973457098007,-0.03191335126757622,0.003732810029760003,0.031172126531600952,-0.08899383991956711,-0.09938952326774597,0.08256369829177856,0.08178982138633728,0.07785400003194809,-0.04618730768561363,-0.02995850332081318,-0.022348755970597267,-0.05898110195994377,0.05294518917798996,0.0038859194610267878,-0.0923057422041893,-0.01576364040374756,-0.0035308743827044964,-0.04901731014251709,-0.012596397660672665,-0.036502618342638016,0.00886201299726963,0.059619251638650894,-0.017561428248882294,0.05459151417016983,0.04560315981507301,-0.0019153780303895473,0.009595169685781002,-0.057729125022888184,0.026341130957007408,-0.023892194032669067,0.016832968220114708,-0.026450062170624733,-0.07305766642093658,0.03468620404601097,-0.02054707705974579,0.041034333407878876,0.00404499564319849,-0.017474710941314697,-0.043891143053770065,0.02514275535941124,0.02372695878148079,0.010677577927708626,0.06225359067320824,0.040919024497270584,0.005154050886631012,0.030111495405435562,0.0054080006666481495,0.03592434898018837,0.0001651789789320901,0.017304912209510803,-0.01922907680273056,0.04822206869721413,-0.0688890889286995,0.019858958199620247,-0.0008752745925448835,0.03513675928115845,-0.07729781419038773,0.08145932108163834,-0.0327017717063427,0.03425054997205734,-0.08482713997364044,0.006879036780446768,0.059308722615242004,-0.03618019446730614,-0.056978799402713776,-0.021730659529566765,-0.0007874490693211555,-0.30017349123954773,0.011467894539237022,0.0029629627242684364,-0.00585860526189208,-0.010300826281309128,0.023507587611675262,0.009586751461029053,0.01615791581571102,-0.05407087132334709,-0.0025957857724279165,-0.005770532879978418,0.03627054765820503,0.03723520413041115,0.0002953026269096881,-0.01028500497341156,0.003999052103608847,-0.005846572108566761,0.033623822033405304,-0.0072589460760355,-0.07468357682228088,0.03272583335638046,-0.00448765279725194,0.21248994767665863,-0.057705674320459366,0.044046953320503235,0.03008623979985714,-0.018218697980046272,0.04393533617258072,0.07603447884321213,-0.04150347039103508,0.06695082038640976,-0.010416779667139053,0.08510852605104446,-0.07743050903081894,-0.005964982323348522,0.03540671616792679,-0.036865249276161194,0.058287233114242554,0.005791360046714544,-0.03530560061335564,-0.010620728135108948,0.03216135874390602,0.012065712362527847,-0.05922657623887062,0.08696120232343674,-0.051534030586481094,-0.08612160384654999,-0.04676511138677597,-0.005788259673863649,0.06060168892145157,-0.02552523836493492,-0.02923434041440487,-0.05256013199687004,0.0033684736117720604,0.023232899606227875,0.023369308561086655,-0.02598796784877777,-0.02167469449341297,-0.05872185155749321,-0.0459195151925087,0.008857548236846924,-0.07634632289409637,0.016223475337028503,0.03924580290913582,0.11316763609647751] +``` + +### Example: Creating Users via Admin API + +You can also leverage the admin API to create users: + +```jsx +select edge.exec( +$js$ + +const { data, error } = await supabase.auth.admin.createUser({ + email: 'user@email.com', + password: 'password', + user_metadata: { name: 'Yoda' } +}); + +$js$)); + +``` + +## Conclusion + +As you can see, combining dynamic Javascript in Edge Functions with a few SQL support functions gets you a powerful new set of tools. By leveraging the edge_wrapper, edge.http_request, and `edge.exec` functions, developers can create robust and flexible serverless applications that can dynamically execute JavaScript code while interacting with PostgreSQL databases. + +As we continue to build and innovate with Supabase, combining edge functions and SQL support functions opens up new avenues for building scalable, efficient, and secure applications. Whether developing a simple project or a complex application, these tools provide the flexibility and power to bring your ideas to life. diff --git a/apps/www/public/images/blog/2024-11-13-supabase-dynamic-functions/dynamic-og.png b/apps/www/public/images/blog/2024-11-13-supabase-dynamic-functions/dynamic-og.png new file mode 100644 index 0000000000000000000000000000000000000000..cfd4b9e92b492e9b27b5a8807d3e60a193256fce GIT binary patch literal 21971 zcmeIac{o&U{6F4?L1b4XlBLX8vW5_a5Gu_$cBewfntiRzq^zligj7PA!HLGcw|9-maAKmBN_j&K{*ZXyQblJ?1gH4ES&z?OTMi(z! z-LvNav}ezLG1PwKCkM3M7541eYjye3HGKgA0iDhSet!OA$BuDxbMx`>NlHqJh=}m; z@bL2Ta&mHVad90wbm-{OquQMbCr+H;;NXyukPsIaKYaMGkdTnDu<(&1M`UDVL`6lV zq@*wyjF_0%$&)9grKOJ_KQ1UJD0)&t%2-3GBvhp`Qn@TlF3(q?FyKsGlv^jpx*G{gpQI%)5GIRXsz~mbe4zA>)Y~VKh^4cask+rX}2&rUYZs6WE?MQ zHa}8MaaSx3)^2#H^*I{(qg);#`_4zT>Mq9J{B%*!nfxHd_?su6JIm&JD}D@-BHlQO z)s=h#o^6RyxNmprrK?1O{qYA@XIo zv_z#&h{>NiaaQ5Ng$sn*y)}reS-me>-PyB;otN=%@17UwhxhCe+hcS=_gYZ;T=iC@ zaBFaGFelTfc+7p>NBd6k@HvIv7H_`w>vNK9ym+tu#FsxOlZqD&G^Af<#aC-KYow$O z(&^DVRpI&51#T;`5kAKk&*~iIl|pehcxkF0KZ;?LP?^kl$;U@zQLP8z+`S;{O#^mVG3vML zIbh#^OaGI=e-ijl0{=+cm(?&k5$tv<508_tx#Uu{e?lx6K ze#jsH9jm<(`Da*Uw{D9z5dM2>H~!}J&ho7vixQi^d}f7**bjfJP1@ELD&5s7E8p&A zJcH*>{#~pUU1vY{Euv9^emycWQvOqfjRE^t`UT+!0Y@tjHP-*i`unio=QkdhT?1|b ziEI>BB1~PU-%q}mjf-|2)GwlZESTNu(0f3;O@yv$s6AYv?gJ?;a$CCrW;i))`bK!Pry3+nS+;564XSqV@-jrEfk3p&SQ7i zv1aneQzT1@rar0Nu#Q?x8zL9G!E6I#i_+?1n7tsDfNgFAm2Gk=taAkY7bt3XjwSc@ zTYgY<3eLPw{e7IwhH4eW&7GfD&UwU)&-IepVzgww*b8}Q}Srez@n0pPK zFg<0*E8NP{adG+PSX|1Ru0xjUfud)R8H#Epgx81OoA_`miQ_6>#$~<%9e-W&Z6(JE z%<-MaceH6OiJx>=I9GhLNCh`YoLP@vA5Rkn%l$lsI9hk}Z)A~oM{WLbT~{46DcOgY zyy)C_i7=JcXPi%34VVy?AU{ip|L5fdee^i;Ri?h@fFmfAUkj8Mw`U9Ae4Tb1EU{i{ z@+^ROG*ob*$-Bkfsd(HW9D!1s2=Qa1H*^}rGrp-&*?1kCb&`U|(C&@p*1bpih;Nil_-$;)fIqUsWAmpB~aeXE*LN>a2 zE)mibBj1JI=o6ShrmI3N6dF#8e^b;dC$ptf>mcx67g+T=JG)dkGxTy=`dO_-TJ8Ei zC(}2Q7I-WRNX27^&*vxSGgZVGhE#4k&(v5)=EXzFyOS!I-e>u)8@Y)xq=VSy`6QId zIe;Ie2EONTM(2=n6lVM=ecHV_m8x2<{P5LXHlbwqZ`seNT=(?hynemg)xy`{`v;WE$;)&4@RA^IUHTQnHp*c)ioP#k z$*xx=WRu!5{R);MWN+H>VY+Hs&!BgawQRSO;-I5kxKrf5_!k&m4%p2F9bbT>uJHxj z8heCgb59*MtmA2~!WWIE0utm;5-fP^UC0-tUVYN|)(Dtmv8!DXJEeD-X6j6J?jcB_AyYYn-c~~_p zM()nNZs(gxiE^A8KJp3IFBU)HP97;7;B=}lJ&{c4If+()TMVss^=PJRPxAMG1U&zx z$7%Til=rkFr3(&y3ALa0&Tu8qc?}3SEtg)`#mKb8ph&9q%pfZYEk*|JFWshmr+&A# zW}OR(h}wsfCG&5$%$%RWm|I-|vCfFcAU!I(YWNC)pY%TDxQEBPKN*WdPX;*N2!4_w zWP2q`4r9C0Jm_tzyD`9tqN54UEgiVuL27y0xA_`lTn0#wj#^bl;5kkC*n*h!ggO?P;SOQ6H|KRz* zVI9l3+Qa%i0|R%0YMtNa#&{*yO03|qu7dt2QfoI#I^9nwtxYSuAv3`;&j`=~JoRI4 zCH5Uts$i79kq6U1GhJ7F#u3(iz)jV3i6DsB?}W!3S~p z&t~j_qE5Ye>oSS2Y~ozad@JXn^!$Hz%4wA|TGbl2AbynTl+ zQ)ofld-BC~E*vM!Q`O~tKh;R*U=w8oYijAEmfwd95%6FB6m)zc(g0?2&?;T0;u1Qn zR_RUd>gHBd-DG^;Wt?A7B6O7tH_OGga0Tai*z$oT)((4#vhu@tuv+7wwh)2qT5<+- zy&|TTj7CzOxN(lyAT|GIVdqkSYzz#&2P4r_Ky2cdlKoA%$BVI zKOCbAR_DKn&0MhrwPFqyx_PzkYL(>f3}ey-%e6r29t*2O!_F34iAN%X3`SbF9BSmqn7yejddTyT zV6KI^RmYcCE2cwX5jPr-jXR_?lH#u2u@f;h?_@5GZVrq1?nw4A1qel4I1W5NZ*u0V zw}z51;tVFEz1!OX2MdhNjGeHXR>AryDw)I2Kd|(6PpCM3-8OGb+$z1{jrH}>)*`jk zDkP&w3bcX0fq>olVp8!8MOq5_)e~l)}zDd;rFLJv};e_IP|@+ zSoCb%`t(hW^5Utr>V0y~lTE~&ws;~6t%g&7WeDhU;VuIa{K^YgvwXf|)+2XGyx)k@TR8@BaoAZxs`B4&Xzt$VR5y9ql2di2HvgxfS!XemY0ZZsraToe-r(Q5rnUW2@+-_` z!@Uj}re0&zSM_9vgo7=y#On^~8BmNUTE40^X?p6mH%Yw}ZT0d)Li*hecqu1IoO}R} z<;STgxu$<-c?4~n(P}3$ZYvy)%Z;*D(;QfkA{Si+lqY`B?17Xvv+xZ;xJ3!aAqwZ+ zg=FA7mz))>IHvHaR?j^%DI)pB2V-!~6_4G%eB~78#Zt$Htmu2GOC6%u(iC}dFN&&Izmg^F{RZUG6^!2@y zpry)3jG`a`x}WM}{-@$gsFlXn^tNip;TQ$^M!?9X4DNGBV_fc1B4^*zue*e)98PcR zN)<2Pk`L)fb*M8p!DH`3Y#>2r{Cm>5@8u!&?19&Q|5|iRd2UxWINQ@N*5K9Fv)}8^ z%NyO6hMMD~x*r{5Ec3Tk-%K7xuLjA+pwKMf1k9GuvTTy_+44Q)>!9^~zHQa9WkV!4 zP3w}*Y6=f-DeF*1PF1l*%@dMdH?X98`KWy+@sHb;3x8`imY*dqqPKeu?Y|fMf|AQe zewKR7{m}av?W7iZW?G8;t^T`Z)Rbe>nKY?lVPkN0N%)(hFNYyFh$N-gqQcQzl;jbs zV1A{3!p1@VoMyB61ZYVl<4+$7JzVHN%#FLsHlXgn!8~Ra93GC#=XgNkMWOeC`UIlD zXti~#)oGc-nVbQMvCG#Te=B%oIuRrrTch(zJ=0=U(-UI9!(t(k!F$^5i;e6$36F1D#BOM1Zj z4++r)9~f~4D~K!S331@Lb`atW3H)A|Qwd0QJeTVF7@hc_gUjSNIc!!T?Agch5l`Aa zUb&y|bLMY0>AAo{8jI%v6)s#9^mZu1mJ_-FG*`{qRh@r+=UG58OT+xhB6M)gQ-{9i z_N~rzbmy1A&*t-$+81B=N-uEzZ2PWBD2I4%h?q=>fDO6;79{qcm0DwA2St#vjRf2` zSQM)?vQzi<$aRV!?(`=isXNu?0Ut-_1rG3n{O2)`Y+Nyc%QlS68wPGPg;DX8<^ANa zheS5_k8zMeHWc7RIp-Mcd=8jD?SN|C4+g_67FXxx0&VTNj~%_3>es{qMAg|^kLdP( zPy`8ySpUR7G6a=N1WG^SH7aG3w!F@;Z{mg?Actz@$4N?V7~0R1E&zt5k>=yh3C=ap zpMG}tVL_UiC4}!nRI|Z_KJwL)QO53veeqb4XiL1mJU1;&Fks2!LV|tdXxsrmxJBqs z^L)n<+43{y$p1=Uay9Nj#*BKp$onxz*%OtM6a1h!S+i1;r8Q$|UBYJjCkh?pT^%X_ z9>;7P4EjEkF+YD46SzO_Do`!q*}%HbUY#Gz7DMWgRU4%Qh|?Eb4fKB!+T8q)AT7cM zoZEu?RW&%WvEG9KlbKYyy?lx0bbX27oH=3WO@KHq=*#@^FM!f_$vChm7 zo;L=GTgkgSh1a=tehmAd|+tNwVRE%#7$=-Oi* z9Nb)03H^Kq;eF`8=LahrRKSZs>IbgmyO1#$FGP_1pfRvWtzhbkEr2yfM>R%9ZSnr4 zR}lYVEO{fX6AC|Jhbz}OQ)iz0MvSLZ3b=4r?kvOyM%bCPG%Ir_+wwIMh=W$G6yOn5)6qO7+zYn6eF7=L9n4i@@O(cH7aAc zs^W%A(xA^%g{^ zAess};U~OR=VUO+2M^%=??M8|L##8O_ysi25GxOgVR)q`5p(3C?L+>8TD}B-v+SNr zz$GrD)HXX%OAlxlBRhkuXTj5O%anb6-->3BK%1u`)N51Zb|&;0NG#%g#f?My3<>fv*o_CLeP`vQIz9LvbW<$3#C<6X zg_a_J2L8T_E7=c&=RW}En*keN_(7&~fDb4s!VWU&0%$%QtpJ7IgJ3!6N1U*m0PQlG zye;N|r-~u3jkyP1hmD^`L7th=%kb^>#j!2(OzrRxw1bu~a)=Pd+mTEt?0B4PBSB7F z&CmmXUs7TPvra?Uik%ZwEU227SxO?YG$*)(;>Y4u9{`$OAl+{5R&>3 zvqpmGWgxgz50C-p2zW}VD`*$7C8JDcGnt|G&rEkZ)Q0=}uS$tLLf%{^1sb)ABLy#7 zf{fr=uJ5>L@w~K&(!!c+OQUJI6P>m6?Kgz33icUKQ{X(Fe-FrnCz|>i0%~@ENsr7O z+|RdOC(ajp%|26kIT(v_e+W&Bk^jII(U9jesG!!UU@Uo@y7jIA-ZQZfD`Z`?3`IDGlL8L$S!LX6Xpq{uh${>y~bIXLwG;6~%k$H?pB z*=7!dSVnN?I007#dOcMk(>KhAYr^TZ%r^A_>@!nBev)b&h4C@-t~LAM7W1rgfI5mK zP6jUkMmefd93=)&4jOX{Ft%y%GkhSUslthILeT%HLJ{;uwbJaS`wmaTu)CLH4~0)J;=~e`Ge7D21fyFj6iNq?u%8s~dgQgN@?}2;PMg z20eUT&8~PG0_|tn+ns`PGlPE*gx<5fXHt{$^1HpQP`=~^K=vGv4LtzCRaF^G?vDiM zMUZu2No_<8-lU*9CFodKN3mdRW^o)80)p9%zo(+=|CbBE2*)yb#4bqP{o|u9rO2~F zL9SUyt%yx)t#%KLTm9MRR{B7~bMIdx-v{$Z*`P6uWusvwWrxWekoJYlKeH4n7d!a= z98k_UgqCpuN=*_|i!2hijj*6nvGvbL216C{-+x!Fbu8Bi0dvvRP(FcVWhF)iQSb?) zn8JB{1Khaht#0KztNqyyt<_9H9!nRHw8h9-))Hh6&mVzSr^-%{18O!yHkg!B^5swj zmky=j7jtJ~Ac4CO{?QoP9H@_XSA$_cghay*K(7tha+>nm23AUoEHfGTZ6wm(R!hI}=f zw88h_4fqN26)2QDn!qntb^?>>;?=h~ZRH^oh8LBQ7=PBuMXNZ+sE9?}Prxw@v0Kz> z&6Czvc`bun!*8F&Dm)ti_>WOHz-0BX{zh3sSI*E?^@?Ze6(?g3m4!3lpgciaok_XOp_T z{d>|~{m;e*n*KaJKzO+vw*7dif*YT`55Wgj`qH6-7vTiRdZFqVxg>Juxbp1~A@$Xb zv;Yg-aKQ!O1S9kk2r2`QAtCm86#CjZw(b{hl{cGt9U3Q1>}n5ED|hqTpHaJ0&|J7D zx_In*_RfSQbGOh#f;{q9f$MoL;GMMPdz}EZ5}~X?}pTPTh=;p z?rgqNq_y(1{+pg;xoJ*a0vsr`FU{5Q?u2}deH97&IA-O=l;2e?`1?I553v{nps;U) z4GfOG{!+f$B)MW`=u;PwwN?FxAfp4TKn~7DTTZUg1LpI_y4eNv1D|(I)taSvh$IW* zw0jHzNyO^$)B;2ZZ)x2?=8`;WxcBILp>e~GZjs$1`0V{47STE$vKBzhK3i;;Y^9W8 zpxxdWvh&AjQqM~GZ-L6nNQ7e)c9I8T-WyM$8qWcj>Y4YeDGp8ktJ}T`K^`|6r9dZ( zecU)!xNjdwK>`lO!N^P4P=8SM3fB$7T64(aNVxyIZYTATpg$sb>M=Ye7^E1RvUy5zj&a^fx+<$d?qkrb>Khd0_w-{`)7LyCOFi>5EaK+p7*co0Y@#-Ok#zl*o--o$Zmx-A?*M?RK}Q z@$2Y3*&1A+Z->%tMo9F{(nvbBI8C11*kzK>S;Q*XP~#@_RaWQa zc_aiUOOwA<5LW*nuQLu`4I~ssoT%Acz8SVUCc6E@{K;P0+Goqi&3YXZ{3`OAEYa%V zfHA^q0XSLn^Hg>Qy?XQEynC!TO&pv+>NZ5%;^Yx)!s<$dAh=^XcmKCwGZUy@v)L-T z`@3@&BkZ{I%W!SjcGwTJQ?Y$ht@5H}YIKN=yb~pgalWp?PC9!1ID$}7j6l0utE<4o zy38t8v1W6sFk&@UG*=K`yF0%Yxw||b0v&dI1kDHb1CH$Y)tnKTS?rj2y=3s9!rM^S zD~gQoqA?S)Wy=KNJi&qNyQ`#+cUPl6TMpCht-S-80iB9SdPHe;nyMVRHAkbg_f7g2 zm65cZw1+12{U{SAxbGG8ktxs+C@g+dqn;Ly2h!p zNkl=!LaG$YOv}yz4M^R9cyAiYUX^@am?(u*)KV|c4}Za3rINtBE?iI-@qLZ+hjQr z_wA-Z{aVJ}WC!2cNcwK{Dr!b_7nFo6tQS|U?r@Vz&T@nKB6!MiP}ag*o-mP6i0lx& zcc!~}4?1nkUbei@3r`6FRj{9AEgz~;H_{9oJaidqamKGM?JmK-EWlOolp`Feg*P8m zTGVZV9(O6iu%d0$KYtn}VCzFDB%g+qkPGMOpB?m`3k*cQ;2)^oi+eX7LEo(%#(}Aq zV%WZk>ES5>Tr?@X|AuogNVFJO7RFEU=q+M3KDnJNA1-~duOvaA<&lRgw%y>-7~`$6QLsr@tD$!z!ul>0|1 zP}T+s4CT|LH+PAaPz`-cZdeG+A6>f5n^n>9KLsXjlR)vSvs+IIGfzbv zQ>}Jma3N+NE<(TOOm3Z9->*je>h=96$ZGk!?b(GC#uk(SP}afQ9FMiqdyWY<%XQrG zhsNhSTp=6YOC0XuDj594U5F+}F05c{m3PdpkujOqNlY1-nOq+f2h01{I50l;r6IcU z5RDVIE`>DgxSZi9M>zENlDKGZ)HyN^O(O|!ihgQ)ZCs~0vWuk)QGNAw3BTe=1VCZI zQ#`@yUs7ZpoU^0kesaJaAxx#uWgu#pjeeZI(H*(tvFHfB%vKlXN^ancV0eagF;q~w zF}*7qb3e3*CHayo0Qj0m7H!qNNvoB(ZCy1v3wcsZL49KfFK1Uy7@0@A3(&SYVB;oB z;$EDUuX(~tRGTY=F7tX1dCqxqpj!8W`Bs2D2c9T@{I*QF<)m_0cyKG8dh<~0BfufQ z2buLEyDnc^>wsHa9Gz-f2oy)NsNT45?QA4hxAM?i2^krKcH_gUU&#FlS#5q^>~KXA z9K!*KL7wSQ(Oz7b*?C9zn_c&}mW4Ca0>|zsGTC`)F%&;)JXbI<Z>P8=l=Rh1;M}9L`iS%%<$x^`HtAw*=v5wu&;dm^ujDwaw3Kj{d`PU~_EW?sUfS_dVq8dbW7%j(4v3km`z3CuLM3XK_s7k{I? zGoGSMx6B?NI-6VP+kan{J9+ZajMw#&>4+=8?6{a!b^O|TozRwy8g)<^q@;E|P8qPY z@E#^Z1F6h)Cc2aTc5(hw)j@;X^IEW++I7YUV7Y0(9-v6==?~0A0&EBqgKW0vQj!uU zNrJ1%VUuB7Ax^N28&bhCAsDLA&>nC@a_xaShZpXe_b>$6T3|*KP3Y)d%FnJwRxpFP zC>D>E;6=c!nFnm72HF>rOqmw|%nz0m!kk7)huZsQl_&o8dvgT;KodL_LI?$pJ9EQ@ zogLYc2NLYTbC(boNM^`P?)AjrSE_CkNlCTicapdh*b~DKrD7 z^&~?=Kb0{$JHMn*t)hTP@(1y6v&!a=&oijw?WWct7JXSa9N0?^RICrYi2Yxuq>biPoFq z-sr^!wS^lpwPE!3*b>cuf+UM(_}S3(*$YTvJd?xfzKEz&j-g7?K3RdPgXR$@7o?NV zh6=xo6{@L3Y%d@)BbHGk-h~YM<;acd9B6sQYI*AfuQ$sk!?t$eyDL*WO^HROQk$THEt=;l;4q zS=FXo?a6A{d*ycbG@mgG;yGZM6pA>R2bq1)yh#LVRh+j7(^@_^&16D6t)wcU2GZde zx=DY@3{D6~iy`Y6on#t;dfAs=T9+@Fd?L;zr(c8bY^`lb3g$eho;J*Q89x`z8(E~B zJ8~#0gNODeSI**dh)6fK!O}8{pPh8Zeh;Je7M3Qj)4oWl5QY8ZMqp{pPL9r&yrog6 z^SqliMQ*qM?Jp%=d!3QAGVhMK&o!XKX()|g9x(A|wqD$&di`t9-I~HSvv=_*wCgEG z=ZN@vL#ctiW!+ArEe|epYj%#*lcZVbz~uZX^jb;A)W^iiziHq;(rOh$;;pD|oOkVM zI7FZ)aA$L3IdG`)ZDHY%R%E%i{wp1NOWuzm39@($s+9>GybMx*Dw7L;)L4Plo;sb| z;pYk4OYgsyaITU^Ngbie&lm+D8biJ=PW!`(KASrdI)gZ8`&shjT#8X(Wwzzu#Nby; z5X2&&02>$X8c?4vfb%~xLf7o!2CLm7@!jlMCS~Rh1s#^UAoXA5fP-#GXSiiR{Kc4Up-l z@^;m-cD0%jQ1Ec*th?n&hXC5Oooj5x8_);EDQ&7*KRPqMBT&N}VI%X@S>6XnRa@=&T)Z%Ii#?c+1wOaz1o#J4J>pj)2Yw zU^gzDmoNc4D70~CCgXGkhqO+ml5oxTQfuG)juhe}i_MJg{f2xI8xzsQkA8N*M7$ou zNLZg<0f1@`KBUH_&(now)1mFBdr2(c94yfj-!v@oE}7>if018>@e3p?@v<|(1Ekrw zwS}K(ckMY>7rqSVWvZ`JClf^rSvs{Nwr#CeQ&4l%()@)i?X9Ego!;2}Rt`WS7fuOf z^878!#6JXTzcwRTnDPy%$&>4fdsI(fsoAUw!u#!b{92#98AQ3(8*vI;Mc^Jr7bJP< z`%6APoYxF~;vl4Y)q8j!C<|haNQOsjomRYDvAh0QGy*LpEp5S?dFB@hv=rOG{oiWU#UA&>{W znc{n7q)-h>jclN-TBG&-?pW0ir}=475G0Yw7_I9?jvq$4yC_*+nhwztPgm{%K@Pe7 zO4+l)rPI1(X)?0%Nsu@)EG{2^0#GqdpfURsPi*7Gi;M{+T)*}Y+CSAFZu8*e69;~^WGRT@Uxq6El5 zO%EATZbgQ!%`d+wJv?X(s+^peo^tfwC>eCc>( zd@4b?D+Fy8*oQ|h$~nX@*OX*p(E>-Wl(B)5r`cK2u~Li;JdhEj2-1NfAK*LmO0%P4 z$^39t*v5PVtuK#{re$;lwnia@1{K7Us?8rlB^CgC=iE{}?Iel7l2>2=7=6AEw>aP02f3z@Z>6;?QhNFeOhbPTcwVz$p2@F{|yu(6wn6ClqbhzG&sBLlG_ z9@RA=tJ@P@9A$Z2H~|Pjw`>`t4eC{(Ia&;^7=iQbb*BSc6kX1mAaG#cU#LePFZoCh zC^Z0V&Wn>Pxk2?07Z|O$6uJH(G|z~`P7TQ10ztLTL!^P})?Cj6H_UM5aID@RBZY9E_jtLN1xHz(p_5So7ip^zjq-AS7xYCp&Y|@(9=q z+z43%8Rr|I+*!fw_$kV)U^z19LzqQ;jJdD17#U%Z;U}UX+zIma*qeBMW>6omq6Z-R zP}y2z#Tj5D0-v7$cOZ2Q=sg(7a7~1l5P-o`Pm+^xKyalvGVPpjf62|D014v07jxkd zqO%^zpVlJAn6&5u-zk58qyZ8u|NIa4sXh;COiNfR`5o#a0Y#3R)yJ^9BOvp244!ye zyAajd+a$vHY9z`afG19;lN;C4{LlOHQo&>k|IC-SH??kOAcmS)!unde;JI@;pO8WzGH&D7z12-Pth! zSAD_upU4*^E2R4K3ru{9v+`+6QD$dwxOp~>ESCr>zIfuobcDvBUsWkSqLu`ionpyp zTjr+SJ3%()dx~RlUU@dR51hFy3+S06)LMRJCi1wBTZFfGg*3sOj)rg z_u4A}3lHf$?dRJ|ok$55SLX%?C8K6uT>*^FG75TxA!JA(9{Rx9^wg9k41Fd|XG(@F z-R6GrmpNy~UK{|pYA}`^1?SY*W`?ROqxZ|*#QW4%LYrj{XLQutCLJ5_L;|BCZRMv$ zoCBJ*@^3S(yjolD$l^TDo}*2w2V!oSdOrQmPWwvyblRzvIVUCrfJ;uy(1W zMtAH%%aFNYV_l$ObW#s!;QRgp;QJGiK@y zff4feBPOrH2lo}SlTz4c_Kw}}O_e8Cm%x3?gobdqZcMry3h@UeuV6gwPat7r;Ji(FcXu?9J7!S1+B&);5asGxi&*#>;Oyq9Zf^!r>+Dmydi)Msh$NIG;zov5WOQSyz{aL>G zm!a0{^AGyyxM=W8C;(xw*fu_d3~Hbj#?iJM9DuBAK&AHQ7p)Jg1DlRGPQZCY3vL`U zoadQjJM^M=4NuiP^J~zkW_n?z?ZHOYRbZ>zX%Br4rb50_*p zCvRbDzKzz3S6RVRx}F-EJ4us=e&192bQOi5ky;mDEZs^`YtGbO!4nV6^{ismCAq)} zG4iM5d`{PG!Bx%>=_$*xE|Fm%M=fmpAoNG7pb&;P#Y3%6e-1$?LQh3r#!^$#1gl4W ze?H=9G?3njFwORZ1haM@MP44-)4{H{rfJc;e8hfd)xJ*G=Dx z;_oj28wb{|0hE*Cm-GWhgI$#Hok`24e!z z-eF;&rfahKvP%DNy&AcITU;O^>mg$vj4)x8rJEY)nXDuQT0(mxf-q%Qx9I`dA94gM zOT(&q_#E*34{az2De2$UvsY4IT?I66k+U|Utn;0`)WJEddsH=xrW4s}gk%d$Ox}Cpi-46f!;qIcB@eQ?Uz2E2xAMxBSs42`G`~`G!{u(aAkBVhpS_8;`fz+GVbT!6 zkac9V4@Hf%{w-B-+hWwE^PT`VuD4Sz;Ps}WX8xud{p%vJ@Ld_S*^rbM8rm@Am;GT= zQ;c3b^{D$v0rX&GL*tqqmppm;hCU_e!+UXOI_tX1%-bOi8vsQm=LLF zr_^@c&u50+>gh)Pi$X=viEg->V9v@Yw`2*#@%LxmwTK=Wm@ejPTpp_^jKgD3(?(Ws z4gxRRHg_+oXiBuR9)_(CAUAnhGb&P~PqqWWare~}gTg|$dK<#71}FxW`ZQSzf&7h2 zfgPO_I^_+PJx^UV>^_c?s(oG-|;J$_)w@EIp)Js zL_kEu)-!)!4RbG7tY&Y`dd>>b47j7>qqS!bABypN0a8D37AKQiY6F!QG7owR3rt-a7WBd#;|>CNu?=Nr68 zL(}ER$6yv9q$4LGS2=Q!&P)bENbg<$Nh_YWU71XD5#0D8)9+gKlk{25>0fUAy!HzR z3z~J&trix+;-=-@hIS&YVckz-ZZeAWd_y1jz zmFIi*tv#&;HM-~?s z&yRe|GE6*%GG{Yu{Jt}}JhidntdN=6X(nyxGGTLZD5;7vuBH7fFs)|cEo%bv{@i~> zUt?zfYUs&pgugxA>AbuV7NT?}CFiV`NTT_5Q!Cfzy5L>tbXYVIH-zQFg@Pi-IwWpK ztod$PZXUU8i{US>*zvLA&3}0_YO8$SE}E~_XxAX!KE+RS1hWjlZ5tI!w*)pezgg`X zAIjr6>`G#)U5L50Wx4$R!0R`%lPAtSryLZ8c{tD47i45jJ-NL5H1O%|tG%OiOHrz5JkpBZR^9-%B@_~!y2E7Fe>Yme2oxAYPL=bwOl%d=>)9yb;<8j07p>gggc?NAOSu8o9r-tc`&#s{2ZhGqQ8|!Nq|SfQNl%rq zFbe-i)DCNCrQ#xcy2V(paN+u&`lQDN-#c7RAUo*S=l`t!b&DS8_SyA=v15yPn6;nR zK?h$dqZB2wlP=>hO8O<+y0~*z?Ag>J^slX6UlFa}JXDA5Q8XGG3KFv3k#9 zX+bh@sz+nT8Z+)zF`I_-CrWngOe-Y}@;U zgBCn8yKm<)RkY+fm|j;Zj4}M*;x)U)?OmvVh4VJ-dgSwqA|c@SUb~z0Aw|FGLTZQL zuh_(aK;LJHl?b+Rc24Z~zHgCjEAb!CF9*6t-5})^Ahq1nTLa$>|F>YBljTajbKmTO z)eq)>i7B_V+X$7uPA9w6{v-ZmaGX1^BsM0NfBy9+sGYRK$Q|1W$V20B&7 z8xpW)KJUgm3v)_DT#u~1-P5`@eU45dzWQGJo(uPbNzas05lCI}?AN*TAumjqDy4kJ z;)oDo0^t!OO`{CCTDKTtbHXYCB6l{9-hM2(RhUQ*Ajixv{ye>r-EFwKFqqC6D0Myx zT3R(hkr-4sN+}Y|iP$^1XMgP8oKKvlEs$!9C=l%BAzSc$hspYb*x}vVJ7o}ZH-_;5 z6MjOj3B^XzwHl&#YmCIyP+iF-H){*&@EDl-orHBhT9EV?yO9B$+l(UvZt{CJE?&D{A1tZ#V*-n{`yocQf(du z(p$)_)EWKNA8Its`ri4Z1f!Kw7RTH(fv+zXhZ+GLC*o~+LIfYI`qb7^KHY!PsM!5j zYp{3>6MW=jj0x;no3t<7r1VCegH}=3g&$884(rPyTTaZ{n& z_&eNo4b=~(Y361yBG3ze{od!>S0NSvmeH8A?m*8v8DQ6MShp0pz!gd%uH9R{{N%R>Kzjb2 zv%l@ug6E!5I9-nLby4Iga$x?rtY;r?DB+;hI~gC7Ov1nXC_Us>s+wO(o0UP}9-Eq> z7y=S~C{87LLm{5eEuxUnjnIMDPZUaFgq>2=TYW3A%dG|HfSDjs8I4`;{;530@_{F7)h z-P*0Kq7`)7O}=pDN8w}M9JS*`vxS|w{E~|X$Q%c`BS$sjbS=T%xsg+t?b?MzyCHNBdHty+Zd+tcPSAbiiveLDS)a z=!bg4Sm&vxW6B?we|xwJV@CSpp~T**i9=KEouFL45RC+3l^yU+o} zEG95j_rIn8N#H*T{3n6`B=DaE{*%Ce68KL7|F05=b~p-;Jlguv0j?yiRb}M7wlceZ qNXR-k<#?D3m%yH*LwviD!TqyNbBcTqkY6CW$4KAoLV=#+{r>|9M|aTx literal 0 HcmV?d00001 From a882994565306a02b9f9a0e7d99d8d5b27beea6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Wed, 13 Nov 2024 15:49:42 +0100 Subject: [PATCH 44/86] docs: Update next.js and sveltekit quick start guides (#30457) --- .../getting-started/quickstarts/nextjs.mdx | 62 +++---------------- .../getting-started/quickstarts/sveltekit.mdx | 10 +-- 2 files changed, 13 insertions(+), 59 deletions(-) diff --git a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx index 1b11196eaeb..88b705e3f90 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx @@ -9,53 +9,7 @@ hideToc: true - - - Go to [database.new](https://database.new) and create a new Supabase project. - - When your project is up and running, go to the [Table Editor](https://supabase.com/dashboard/project/_/editor), create a new table and insert some data. - - Alternatively, you can run the following snippet in your project's [SQL Editor](https://supabase.com/dashboard/project/_/sql/new). This will create a `notes` table with some sample data. - - - - - - ```sql SQL_EDITOR - -- Create the table - create table notes ( - id bigint primary key generated always as identity, - title text not null - ); - - -- Insert some sample data into the table - insert into notes (title) - values - ('Today I created a Supabase project.'), - ('I added some data and queried it from Next.js.'), - ('It was awesome!'); - - alter table notes enable row level security; - ``` - - - - - - Make the data in your table publicly readable by adding an RLS policy: - - - - - - ```sql SQL_EDITOR - create policy "public can read notes" - on public.notes - for select to anon - using (true); - ``` - - + @@ -104,9 +58,9 @@ hideToc: true - Create a new file at `app/notes/page.tsx` and populate with the following. + Create a new file at `app/countries/page.tsx` and populate with the following. - This will select all the rows from the `notes` table in Supabase and render them on the page. + This will select all the rows from the `countries` table in Supabase and render them on the page. @@ -114,14 +68,14 @@ hideToc: true - ```ts app/notes/page.tsx + ```ts app/countries/page.tsx import { createClient } from '@/utils/supabase/server'; - export default async function Notes() { + export default async function Countries() { const supabase = await createClient(); - const { data: notes } = await supabase.from("notes").select(); + const { data: countries } = await supabase.from("countries").select(); - return
{JSON.stringify(notes, null, 2)}
+ return
{JSON.stringify(countries, null, 2)}
} ``` @@ -166,7 +120,7 @@ hideToc: true - Run the development server, go to http://localhost:3000/notes in a browser and you should see the list of notes. + Run the development server, go to http://localhost:3000/countries in a browser and you should see the list of countries. diff --git a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx index bcb65aca897..c78cc43a9b1 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx @@ -24,7 +24,7 @@ hideToc: true ```bash Terminal - npm create svelte@latest myapp + npx sv create my-app ``` @@ -43,7 +43,7 @@ hideToc: true ```bash Terminal - cd myapp && npm install @supabase/supabase-js + cd my-app && npm install @supabase/supabase-js ``` @@ -53,7 +53,7 @@ hideToc: true - Create a `/src/lib` directory in your SvelteKit app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client with your project URL and public API (anon) key: + Create a `src/lib` directory in your SvelteKit app, create a file called `supabaseClient.js` and add the following code to initialize the Supabase client with your project URL and public API (anon) key: @@ -77,7 +77,7 @@ hideToc: true Use `load` method to fetch the data server-side and display the query results as a simple list. - Create `+page.server.js` file in the `routes` directory with the following code. + Create `+page.server.js` file in the `src/routes` directory with the following code. @@ -98,7 +98,7 @@ hideToc: true - Replace the existing content in your `+page.svelte` file in the `routes` directory with the following code. + Replace the existing content in your `+page.svelte` file in the `src/routes` directory with the following code. From 12b6389090ddc5a93d749269635b68ff09963277 Mon Sep 17 00:00:00 2001 From: Pavel Borisov <63344111+pashkinelfe@users.noreply.github.com> Date: Wed, 13 Nov 2024 19:11:16 +0400 Subject: [PATCH 45/86] Update pgvector.mdx (for 0.8.0) (#30414) * Update pgvector.mdx Add usage of iterative search (from version 0.8.0) * Update pgvector.mdx * Update apps/docs/content/guides/database/extensions/pgvector.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/content/guides/database/extensions/pgvector.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/content/guides/database/extensions/pgvector.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/content/guides/database/extensions/pgvector.mdx Co-authored-by: Artur Zakirov * Update pgvector.mdx Remove repeat --------- Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> Co-authored-by: Artur Zakirov --- .../guides/database/extensions/pgvector.mdx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/apps/docs/content/guides/database/extensions/pgvector.mdx b/apps/docs/content/guides/database/extensions/pgvector.mdx index b320a7e23a5..c505abe6991 100644 --- a/apps/docs/content/guides/database/extensions/pgvector.mdx +++ b/apps/docs/content/guides/database/extensions/pgvector.mdx @@ -102,6 +102,20 @@ const { data, error } = await supabase.from('posts').insert({ }) ``` +## Specific usage cases + +### Queries with filtering + +If you use an IVFFlat or HNSW index and naively filter the results based on the value of another column, you may get fewer rows returned than requested. + +For example, the following query may return fewer than 5 rows, even if 5 corresponding rows exist in the database. This is because the embedding index may not return 5 rows matching the filter. + +``` +SELECT * FROM items WHERE category_id = 123 ORDER BY embedding <-> '[3,1,2]' LIMIT 5; +``` + +To get the exact number of requested rows, use [iterative search](https://github.com/pgvector/pgvector/?tab=readme-ov-file#iterative-index-scans) to continue scanning the index until enough results are found. + ## More pgvector and Supabase resources - [Supabase Clippy: ChatGPT for Supabase Docs](https://supabase.com/blog/chatgpt-supabase-docs) From f7292dce8cf360491c0547fcd6fb5cf663c78d00 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Wed, 13 Nov 2024 14:09:24 -0330 Subject: [PATCH 46/86] Chore/spam page (#29854) * Add spam page * Add select * Add experimental flag in next.config to see if it resolves the build issue * Add slack webhook call * updated endpoint * revert * update * fix placeholder styling in Select * clean * Update [ref].tsx * Update select.tsx * revert * protect form and endpoint from bad ref abuse * Delete spam api route * Add api route back * Basic api route * Rename * rename route * Add as route handler * Stub in supabase update * Add the proper supabase project * Rename spam to opt-out * Swap anon for service role key * Rename env var --------- Co-authored-by: Joshen Lim Co-authored-by: Jonathan Summers-Muir --- apps/www/app/api-v2/opt-out/[ref]/route.ts | 56 +++++ apps/www/pages/opt-out/[ref].tsx | 196 ++++++++++++++++++ apps/www/pages/opt-out/index.tsx | 15 ++ .../ui/src/components/shadcn/ui/select.tsx | 2 +- turbo.json | 4 +- 5 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 apps/www/app/api-v2/opt-out/[ref]/route.ts create mode 100644 apps/www/pages/opt-out/[ref].tsx create mode 100644 apps/www/pages/opt-out/index.tsx diff --git a/apps/www/app/api-v2/opt-out/[ref]/route.ts b/apps/www/app/api-v2/opt-out/[ref]/route.ts new file mode 100644 index 00000000000..15456e39ba6 --- /dev/null +++ b/apps/www/app/api-v2/opt-out/[ref]/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createClient } from '@supabase/supabase-js' + +const supabaseUrl = process.env.NEXT_PUBLIC_EMAIL_ABUSE_URL as string +const supabaseServiceKey = process.env.EMAIL_ABUSE_SERVICE_KEY as string + +export async function POST(req: NextRequest, { params }: { params: { ref: string } }) { + const ref = params.ref + const { reason, email } = await req.json() + + const supabase = createClient(supabaseUrl, supabaseServiceKey) + + if (!ref) { + return NextResponse.json( + { error: 'Bad Request: Missing or invalid project reference.' }, + { status: 400 } + ) + } + + const refPattern = /^[a-zA-Z]{20}$/ + const refIsInvalid = !refPattern.test(ref) + + if (refIsInvalid) { + return NextResponse.json( + { error: 'Bad Request: Missing or invalid project reference.' }, + { status: 400 } + ) + } + + try { + const { error: supabaseError } = await supabase + .from('manual_reports') + .insert([{ project_ref: ref, reason, email }]) + + if (supabaseError) throw new Error(`Supabase error: ${supabaseError.message}`) + + const response = await fetch(process.env.EMAIL_REPORT_SLACK_WEBHOOK as string, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: `New report from: ${ref} \n\n ${reason}` }), + }) + + if (!response.ok) throw new Error('Failed to send to Slack') + + return NextResponse.json( + { message: 'Thank you! We have received your report.' }, + { status: 200 } + ) + } catch (error) { + const errorMessage = (error as Error).message + return NextResponse.json( + { error: `Failure: Could not send post to Slack. Error: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/www/pages/opt-out/[ref].tsx b/apps/www/pages/opt-out/[ref].tsx new file mode 100644 index 00000000000..b164d18390b --- /dev/null +++ b/apps/www/pages/opt-out/[ref].tsx @@ -0,0 +1,196 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import Layout from 'components/Layouts/Default' +import { NextSeo } from 'next-seo' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' +import { + Button, + cn, + Form_Shadcn_, + FormField_Shadcn_, + FormMessage_Shadcn_, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectGroup_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, +} from 'ui' +import { Admonition } from 'ui-patterns' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { z } from 'zod' + +const formSchema = z.object({ + ref: z.string(), + reason: z.string().optional(), +}) + +export default function OptOutPage() { + const router = useRouter() + const ref = router.query.ref as string + const email = router.query.email as string | undefined + + const [formMessage, setFormMessage] = useState(null) + const [submissionType, setSubmissionType] = useState<'success' | 'error' | null>(null) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + ref: ref, + reason: undefined, + }, + }) + + const { register } = form + + // Ensure ref is a string + const refString = typeof ref !== 'string' + const refPattern = /^[a-zA-Z]{20}$/ + const refIsInvalid = !refString && !refPattern.test(ref) + const SelectItemContainerClasses = 'flex flex-col text-sm items-start' + + const onSubmit = async (data: z.infer) => { + try { + setSubmissionType(null) + setFormMessage('Submitting...') + + const response = await fetch(`/api-v2/opt-out/${data.ref}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ reason: data.reason, email: email }), + }) + const message = await response.text() + + if (refIsInvalid) { + setFormMessage('Error: Invalid project reference.') + setSubmissionType('error') + } else if (response.ok) { + setFormMessage(message) + setSubmissionType('success') + } else { + console.error('Form submission failed:', message) + setFormMessage('Error: Form submission failed.') + setSubmissionType('error') + } + } catch (error) { + console.error('Error submitting form:', error) + setFormMessage('Error: An unexpected error occurred.') + setSubmissionType('error') + } + } + + function OptOutLayout({ children }: { children: React.ReactNode }) { + return ( + <> + + + {children} + + + ) + } + + useEffect(() => { + form.reset({ ref: ref, reason: '' }) + }, [ref]) + + return ( + <> + +
+ Opt out +

Receive an unexpected email from Supabase?

+

Please report it here so we can investigate.

+
+ + {refIsInvalid && ( + +

The project reference you entered is invalid.

+

Please check the URL and try again.

+
+ )} + + +
+ + ( + + + + + {field.value} + + + + + +
+ Phishing + + Attempt to obtain sensitive information + +
+
+ +
+ Advertisement + + Unwanted promotional content + +
+
+ +
+ Malware + + Contains harmful software + +
+
+ +
+ Scam + + Fraudulent or deceptive content + +
+
+ +
+ Other + + Any other type of spam + +
+
+
+
+
+
+ )} + /> + + + {formMessage} + + +
+
+ + ) +} diff --git a/apps/www/pages/opt-out/index.tsx b/apps/www/pages/opt-out/index.tsx new file mode 100644 index 00000000000..690ff9449b5 --- /dev/null +++ b/apps/www/pages/opt-out/index.tsx @@ -0,0 +1,15 @@ +import { useEffect } from 'react' +import { useRouter } from 'next/router' + +const OptOutIndex = () => { + const router = useRouter() + + // We don't want anyone landing here directly + useEffect(() => { + router.push('/') + }, [router]) + + return null +} + +export default OptOutIndex diff --git a/packages/ui/src/components/shadcn/ui/select.tsx b/packages/ui/src/components/shadcn/ui/select.tsx index 8bc84d8b5f8..8fd409dbcae 100644 --- a/packages/ui/src/components/shadcn/ui/select.tsx +++ b/packages/ui/src/components/shadcn/ui/select.tsx @@ -47,7 +47,7 @@ const SelectTrigger = React.forwardRef< Date: Wed, 13 Nov 2024 21:00:01 -0500 Subject: [PATCH 47/86] Updating copy on YC hackathon -- event is full (#30462) * Updating copy on YC hackathon -- event is full * disable event cta --------- Co-authored-by: Francesco Sansalvadore --- .../2024-11-22-supabase-ai-hackathon-at-y-combinator.mdx | 9 +++++---- apps/www/pages/events/[slug].tsx | 7 +++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/www/_events/2024-11-22-supabase-ai-hackathon-at-y-combinator.mdx b/apps/www/_events/2024-11-22-supabase-ai-hackathon-at-y-combinator.mdx index 8e12fe15aab..52e254cee09 100644 --- a/apps/www/_events/2024-11-22-supabase-ai-hackathon-at-y-combinator.mdx +++ b/apps/www/_events/2024-11-22-supabase-ai-hackathon-at-y-combinator.mdx @@ -8,10 +8,11 @@ og_image: '/images/events/hackathons/supabase-ai-hackathon-at-y-combinator/yc-ha onDemand: false main_cta: { - url: 'https://forms.supabase.com/yai-hackathon', + url: '', target: '_blank', - label: 'Apply now', - disabled_label: 'Registrations closed', + label: 'Applications closed', + disabled_label: 'Applications closed', + disabled: true, } date: '2024-11-22T10:00:00.000-07:00' end_date: '2024-11-23T18:00:00.000-07:00' @@ -29,7 +30,7 @@ We've already seen [several 1M+ user AI applications](https://x.com/kiwicopple/s [Anthropic](https://www.anthropic.com) is also supporting the hackathon and will provide credits to every team. -Because we have limited capacity, we've created [a very short application](https://forms.supabase.com/yai-hackathon). We’ll keep the form open for a few weeks and let you know if you’re in by early November. +**This event is full and invite only.** ### Jury diff --git a/apps/www/pages/events/[slug].tsx b/apps/www/pages/events/[slug].tsx index 5e2bf7a07b2..372d7ccc2d1 100644 --- a/apps/www/pages/events/[slug].tsx +++ b/apps/www/pages/events/[slug].tsx @@ -49,6 +49,7 @@ type CTA = { url: string label?: string disabled_label?: string + disabled?: boolean target?: '_blank' | '_self' } @@ -289,7 +290,9 @@ const EventPage = ({ event }: InferGetStaticPropsType) => type="primary" size="medium" className="mt-2" - disabled={!IS_REGISTRATION_OPEN} + disabled={ + !IS_REGISTRATION_OPEN || event.main_cta?.disabled || event.main_cta?.disabled + } asChild > ) => type="primary" size="medium" className="mt-2" - disabled={!IS_REGISTRATION_OPEN} + disabled={!IS_REGISTRATION_OPEN || event.main_cta?.disabled} asChild > Date: Wed, 13 Nov 2024 21:12:06 -0500 Subject: [PATCH 48/86] updating start time to 6pm + timezone to pst (#30463) --- .../2024-11-22-supabase-ai-hackathon-at-y-combinator.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/www/_events/2024-11-22-supabase-ai-hackathon-at-y-combinator.mdx b/apps/www/_events/2024-11-22-supabase-ai-hackathon-at-y-combinator.mdx index 52e254cee09..bf5781898c7 100644 --- a/apps/www/_events/2024-11-22-supabase-ai-hackathon-at-y-combinator.mdx +++ b/apps/www/_events/2024-11-22-supabase-ai-hackathon-at-y-combinator.mdx @@ -14,8 +14,8 @@ main_cta: disabled_label: 'Applications closed', disabled: true, } -date: '2024-11-22T10:00:00.000-07:00' -end_date: '2024-11-23T18:00:00.000-07:00' +date: '2024-11-22T18:00:00.000-08:00' +end_date: '2024-11-23T18:00:00.000-08:00' timezone: 'America/Los_Angeles' duration: '2 days' categories: From 0e3bc5804b60499fa9d8034c51d76669fb66170d Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Thu, 14 Nov 2024 15:21:29 +0800 Subject: [PATCH 49/86] chore: remove useExecuteSqlQuery (part 1) (#30437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: remove useExecuteSqlQuery part 1 * fix invalidation * fixes * more fixes * I fixed it, but tested on preview instead of local 🤦🏻 * only refetch table rows and not count on row updates * removed unneeded invalidations and prefetched new table after create --- apps/studio/components/grid/SupabaseGrid.tsx | 3 +- .../footer/pagination/Pagination.tsx | 4 +- .../grid/components/header/Header.tsx | 6 +- .../grid/components/header/RefreshButton.tsx | 8 +- .../ColumnEditor/ColumnEditor.tsx | 3 +- .../ForeignRowSelector/ForeignRowSelector.tsx | 3 +- .../SidePanelEditor/SidePanelEditor.tsx | 36 ++--- .../SidePanelEditor/SidePanelEditor.utils.tsx | 48 ++++-- .../TableEditor/TableEditor.tsx | 3 +- .../TableGridEditor/TableDefinition.tsx | 6 +- .../TableGridEditor/TableGridEditor.tsx | 17 +- .../database-column-delete-mutation.ts | 16 +- .../studio/data/database/constraints-query.ts | 76 +++++---- .../data/database/entity-definition-query.ts | 75 ++++++--- .../data/database/entity-definitions-query.ts | 50 +++--- apps/studio/data/database/keys.ts | 8 +- .../data/database/table-definition-query.ts | 76 ++++++--- .../data/database/view-definition-query.ts | 76 ++++++--- .../prefetchers/project.$ref.editor.$id.tsx | 82 ++++++---- .../data/table-editor/table-editor-query.ts | 2 +- .../table-rows/get-cell-value-mutation.ts | 2 - apps/studio/data/table-rows/keys.ts | 12 ++ .../table-rows/table-row-create-mutation.ts | 6 +- .../table-row-delete-all-mutation.ts | 4 +- .../table-rows/table-row-delete-mutation.tsx | 4 +- .../table-rows/table-row-truncate-mutation.ts | 6 +- .../table-rows/table-row-update-mutation.ts | 9 +- .../data/table-rows/table-rows-count-query.ts | 100 +++++++----- .../data/table-rows/table-rows-query.ts | 150 ++++++++++-------- 29 files changed, 536 insertions(+), 355 deletions(-) create mode 100644 apps/studio/data/table-rows/keys.ts diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx index 2ba8e5d0a03..7c996ca9108 100644 --- a/apps/studio/components/grid/SupabaseGrid.tsx +++ b/apps/studio/components/grid/SupabaseGrid.tsx @@ -149,10 +149,9 @@ const SupabaseGridLayout = (props: SupabaseGridProps) => { const { project } = useProjectContext() const { data, error, isSuccess, isError, isLoading, isRefetching } = useTableRowsQuery( { - queryKey: [props.table.schema, props.table.name], projectRef: project?.ref, connectionString: project?.connectionString, - table: props.table, + tableId: props.table.id, sorts, filters, page: snap.page, diff --git a/apps/studio/components/grid/components/footer/pagination/Pagination.tsx b/apps/studio/components/grid/components/footer/pagination/Pagination.tsx index bb32dd1a3e2..9c4384a8f14 100644 --- a/apps/studio/components/grid/components/footer/pagination/Pagination.tsx +++ b/apps/studio/components/grid/components/footer/pagination/Pagination.tsx @@ -44,7 +44,6 @@ const Pagination = () => { const [{ filter }] = useUrlState({ arrayKeys: ['filter'] }) const filters = formatFilterURLParams(filter as string[]) const page = snap.page - const table = state.table ?? undefined const roleImpersonationState = useRoleImpersonationStateSnapshot() const [isConfirmNextModalOpen, setIsConfirmNextModalOpen] = useState(false) @@ -60,10 +59,9 @@ const Pagination = () => { const { data, isLoading, isSuccess, isError, isFetching } = useTableRowsCountQuery( { - queryKey: [table?.schema, table?.name, 'count-estimate'], projectRef: project?.ref, connectionString: project?.connectionString, - table, + tableId: id, filters, enforceExactCount: snap.enforceExactCount, impersonatedRole: roleImpersonationState.role, diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index 9024b480d53..45c69218f99 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -239,10 +239,9 @@ const RowHeader = ({ table, sorts, filters }: RowHeaderProps) => { const [isExporting, setIsExporting] = useState(false) const { data } = useTableRowsQuery({ - queryKey: [table.schema, table.name], projectRef: project?.ref, connectionString: project?.connectionString, - table, + tableId: table.id, sorts, filters, page: snap.page, @@ -252,10 +251,9 @@ const RowHeader = ({ table, sorts, filters }: RowHeaderProps) => { const { data: countData } = useTableRowsCountQuery( { - queryKey: [table?.schema, table?.name, 'count-estimate'], projectRef: project?.ref, connectionString: project?.connectionString, - table, + tableId: table.id, filters, enforceExactCount: snap.enforceExactCount, impersonatedRole: roleImpersonationState.role, diff --git a/apps/studio/components/grid/components/header/RefreshButton.tsx b/apps/studio/components/grid/components/header/RefreshButton.tsx index 0b43a2aa0b2..9bf63bf1b0c 100644 --- a/apps/studio/components/grid/components/header/RefreshButton.tsx +++ b/apps/studio/components/grid/components/header/RefreshButton.tsx @@ -2,20 +2,18 @@ import { useQueryClient } from '@tanstack/react-query' import { RefreshCw } from 'lucide-react' import { useParams } from 'common' -import type { SupaTable } from 'components/grid/types' -import { sqlKeys } from 'data/sql/keys' -import { Entity } from 'data/table-editor/table-editor-types' +import { tableRowKeys } from 'data/table-rows/keys' import { Button } from 'ui' export type RefreshButtonProps = { - table: Entity | SupaTable + table: { id: number } isRefetching?: boolean } const RefreshButton = ({ table, isRefetching }: RefreshButtonProps) => { const { ref } = useParams() const queryClient = useQueryClient() - const queryKey = sqlKeys.query(ref, [table.schema, table.name]) + const queryKey = tableRowKeys.tableRowsAndCount(ref, table.id) async function onClick() { await queryClient.invalidateQueries(queryKey) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx index 423fc6843c0..3d717734a9f 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx @@ -86,8 +86,7 @@ const ColumnEditor = ({ const { data: constraints } = useTableConstraintsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, - schema: selectedTable?.schema, - table: selectedTable?.name, + id: selectedTable?.id, }) const primaryKey = (constraints ?? []).find( (constraint) => constraint.type === CONSTRAINT_TYPE.PRIMARY_KEY_CONSTRAINT diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx index 85de3f09930..1840497c8e4 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx @@ -59,10 +59,9 @@ const ForeignRowSelector = ({ const { data, isLoading, isSuccess, isError, isRefetching } = useTableRowsQuery( { - queryKey: [schemaName, tableName], projectRef: project?.ref, connectionString: project?.connectionString, - table: supaTable, + tableId: table?.id, sorts, filters, page, diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index b1c2dfca30d..94ea0ca15e7 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -10,10 +10,12 @@ import { useDatabasePublicationsQuery } from 'data/database-publications/databas import { useDatabasePublicationUpdateMutation } from 'data/database-publications/database-publications-update-mutation' import type { Constraint } from 'data/database/constraints-query' import type { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query' +import { databaseKeys } from 'data/database/keys' import { entityTypeKeys } from 'data/entity-types/keys' import { sqlKeys } from 'data/sql/keys' import { tableEditorKeys } from 'data/table-editor/keys' import { isTableLike } from 'data/table-editor/table-editor-types' +import { tableRowKeys } from 'data/table-rows/keys' import { useTableRowCreateMutation } from 'data/table-rows/table-row-create-mutation' import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation' import { tableKeys } from 'data/tables/keys' @@ -258,17 +260,15 @@ const SidePanelEditor = ({ queryClient.invalidateQueries(tableEditorKeys.tableEditor(project?.ref, selectedTable?.id)), queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])), queryClient.invalidateQueries( - sqlKeys.query(project?.ref, [ - 'table-definition', - selectedTable!.schema, - selectedTable!.name, - ]) + databaseKeys.tableDefinition(project?.ref, selectedTable?.id) ), queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)), ]) + // We need to invalidate tableRowsAndCount after tableEditor + // to ensure the query sent is correct await queryClient.invalidateQueries( - sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name]) + tableRowKeys.tableRowsAndCount(project?.ref, selectedTable?.id) ) setIsEdited(false) @@ -472,22 +472,6 @@ const SidePanelEditor = ({ if (hasError) { toast(`Table ${table.name} has been updated, but there were some errors`, { id: toastId }) } else { - queryClient.invalidateQueries(sqlKeys.query(project?.ref, ['foreign-key-constraints'])) - await Promise.all([ - queryClient.invalidateQueries(tableEditorKeys.tableEditor(project?.ref, table.id)), - queryClient.invalidateQueries( - sqlKeys.query(project?.ref, [selectedTable.schema, selectedTable.name]) - ), - queryClient.invalidateQueries( - sqlKeys.query(project?.ref, [ - 'table-definition', - selectedTable.schema, - selectedTable.name, - ]) - ), - queryClient.invalidateQueries(entityTypeKeys.list(project?.ref)), - ]) - toast.success(`Successfully updated ${table.name}!`, { id: toastId }) } } @@ -562,11 +546,9 @@ const SidePanelEditor = ({ } } - await Promise.all([ - queryClient.invalidateQueries( - sqlKeys.query(project?.ref, [selectedTable!.schema, selectedTable!.name]) - ), - ]) + await queryClient.invalidateQueries( + tableRowKeys.tableRowsAndCount(project?.ref, selectedTable?.id) + ) toast.success(`Successfully imported ${rowCount} rows of data into ${selectedTable.name}`, { id: toastId, }) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx index 12aa1adf6ca..f13b0cd7c37 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx @@ -11,11 +11,15 @@ import { updateDatabaseColumn } from 'data/database-columns/database-column-upda import type { Constraint } from 'data/database/constraints-query' import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' import { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query' +import { databaseKeys } from 'data/database/keys' import { entityTypeKeys } from 'data/entity-types/keys' +import { prefetchEditorTablePage } from 'data/prefetchers/project.$ref.editor.$id' import { getQueryClient } from 'data/query-client' import { executeSql } from 'data/sql/execute-sql-query' import { sqlKeys } from 'data/sql/keys' -import { getTableEditor } from 'data/table-editor/table-editor-query' +import { tableEditorKeys } from 'data/table-editor/keys' +import { prefetchTableEditor } from 'data/table-editor/table-editor-query' +import { tableRowKeys } from 'data/table-rows/keys' import { tableKeys } from 'data/tables/keys' import { createTable as createTableMutation } from 'data/tables/table-create-mutation' import { deleteTable as deleteTableMutation } from 'data/tables/table-delete-mutation' @@ -211,6 +215,8 @@ export const createColumn = async ({ selectedTable, primaryKey, foreignKeyRelations = [], + skipSuccessMessage = false, + toastId: _toastId, }: { projectRef: string connectionString: string | undefined @@ -218,8 +224,10 @@ export const createColumn = async ({ selectedTable: PostgresTable primaryKey?: Constraint foreignKeyRelations?: ForeignKey[] + skipSuccessMessage?: boolean + toastId?: string | number }) => { - const toastId = toast.loading(`Creating column "${payload.name}"...`) + const toastId = _toastId ?? toast.loading(`Creating column "${payload.name}"...`) try { // Once pg-meta supports composite keys, we can remove this logic const { isPrimaryKey, ...formattedPayload } = payload @@ -265,7 +273,9 @@ export const createColumn = async ({ }) } - toast.success(`Successfully created column "${column.name}"`, { id: toastId }) + if (!skipSuccessMessage) { + toast.success(`Successfully created column "${column.name}"`, { id: toastId }) + } } catch (error: any) { toast.error(`An error occurred while creating the column "${payload.name}"`, { id: toastId }) return { error } @@ -454,6 +464,8 @@ export const createTable = async ({ isRLSEnabled: boolean importContent?: ImportContent }) => { + const queryClient = getQueryClient() + // Create the table first. Error may be thrown. const table = await createTableMutation({ projectRef: projectRef, @@ -598,6 +610,13 @@ export const createTable = async ({ } } + await prefetchEditorTablePage({ + queryClient, + projectRef, + connectionString, + id: table.id, + }) + // Finally, return the created table return table } catch (error) { @@ -689,6 +708,8 @@ export const updateTable = async ({ connectionString: connectionString, payload: columnPayload, selectedTable: updatedTable, + skipSuccessMessage: true, + toastId, }) } else { const originalColumn = find(originalColumns, { id: column.id }) @@ -696,16 +717,15 @@ export const updateTable = async ({ const columnPayload = generateUpdateColumnPayload(originalColumn, updatedTable, column) if (!isEmpty(columnPayload)) { toast.loading(`Updating column ${column.name} from ${updatedTable.name}`, { id: toastId }) - const skipPKCreation = true - const skipSuccessMessage = true + const res = await updateColumn({ projectRef: projectRef, connectionString: connectionString, id: column.id, payload: columnPayload, selectedTable: updatedTable, - skipPKCreation, - skipSuccessMessage, + skipPKCreation: true, + skipSuccessMessage: true, }) if (res?.error) { hasError = true @@ -739,18 +759,18 @@ export const updateTable = async ({ const queryClient = getQueryClient() await Promise.all([ + queryClient.invalidateQueries(tableEditorKeys.tableEditor(projectRef, table.id)), queryClient.invalidateQueries(sqlKeys.query(projectRef, ['foreign-key-constraints'])), - // invalidate list of tables, as well as visible individual tables - queryClient.invalidateQueries(['projects', projectRef, 'tables']), - queryClient.invalidateQueries(sqlKeys.query(projectRef, [table.schema, table.name])), - queryClient.invalidateQueries( - sqlKeys.query(projectRef, ['table-definition', table.schema, table.name]) - ), + queryClient.invalidateQueries(databaseKeys.tableDefinition(projectRef, table.id)), queryClient.invalidateQueries(entityTypeKeys.list(projectRef)), ]) + // We need to invalidate tableRowsAndCount after tableEditor + // to ensure the query sent is correct + await queryClient.invalidateQueries(tableRowKeys.tableRowsAndCount(projectRef, table.id)) + return { - table: await getTableEditor({ + table: await prefetchTableEditor(queryClient, { projectRef, connectionString, id: table.id, diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index 4b471a8014d..cc792afd031 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -124,8 +124,7 @@ const TableEditor = ({ const { data: constraints } = useTableConstraintsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, - schema: table?.schema, - table: table?.name, + id: table?.id, }) const primaryKey = (constraints ?? []).find( (constraint) => constraint.type === CONSTRAINT_TYPE.PRIMARY_KEY_CONSTRAINT diff --git a/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx b/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx index db15bafe083..8250fc7528a 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableDefinition.tsx @@ -33,8 +33,7 @@ const TableDefinition = ({ entity }: TableDefinitionProps) => { const viewResult = useViewDefinitionQuery( { - schema: entity?.schema, - name: entity?.name, + id: entity?.id, projectRef: project?.ref, connectionString: project?.connectionString, }, @@ -45,8 +44,7 @@ const TableDefinition = ({ entity }: TableDefinitionProps) => { const tableResult = useTableDefinitionQuery( { - schema: entity?.schema, - name: entity?.name, + id: entity?.id, projectRef: project?.ref, connectionString: project?.connectionString, }, diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index 9b1887d9081..ff4ef5d7127 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -9,14 +9,15 @@ import { SupabaseGrid } from 'components/grid/SupabaseGrid' import { parseSupaTable } from 'components/grid/SupabaseGrid.utils' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { DocsButton } from 'components/ui/DocsButton' -import { sqlKeys } from 'data/sql/keys' import { Entity, isMaterializedView, isTableLike, isView, } from 'data/table-editor/table-editor-types' +import { tableRowKeys } from 'data/table-rows/keys' import { useTableRowUpdateMutation } from 'data/table-rows/table-row-update-mutation' +import { TableRowsData } from 'data/table-rows/table-rows-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import useLatest from 'hooks/misc/useLatest' import { useUrlState } from 'hooks/ui/useUrlState' @@ -61,20 +62,16 @@ const TableGridEditor = ({ async onMutate({ projectRef, table, configuration, payload }) { const primaryKeyColumns = new Set(Object.keys(configuration.identifiers)) - const queryKey = sqlKeys.query(projectRef, [ - table.schema, - table.name, - { table: { name: table.name, schema: table.schema } }, - ]) + const queryKey = tableRowKeys.tableRows(projectRef, { table: { id: table.id } }) await queryClient.cancelQueries(queryKey) - const previousRowsQueries = queryClient.getQueriesData<{ result: any[] }>(queryKey) + const previousRowsQueries = queryClient.getQueriesData(queryKey) - queryClient.setQueriesData<{ result: any[] }>(queryKey, (old) => { + queryClient.setQueriesData(queryKey, (old) => { return { - result: - old?.result.map((row) => { + rows: + old?.rows.map((row) => { // match primary keys if ( Object.entries(row) diff --git a/apps/studio/data/database-columns/database-column-delete-mutation.ts b/apps/studio/data/database-columns/database-column-delete-mutation.ts index 2572f5fa673..16350f8961f 100644 --- a/apps/studio/data/database-columns/database-column-delete-mutation.ts +++ b/apps/studio/data/database-columns/database-column-delete-mutation.ts @@ -1,10 +1,12 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' +import { databaseKeys } from 'data/database/keys' import { entityTypeKeys } from 'data/entity-types/keys' import { del, handleError } from 'data/fetchers' import { sqlKeys } from 'data/sql/keys' import { tableEditorKeys } from 'data/table-editor/keys' +import { tableRowKeys } from 'data/table-rows/keys' import { viewKeys } from 'data/views/keys' import type { ResponseError } from 'types' @@ -62,18 +64,20 @@ export const useDatabaseColumnDeleteMutation = ({ ...(table !== undefined ? [ queryClient.invalidateQueries(tableEditorKeys.tableEditor(projectRef, table.id)), - queryClient.invalidateQueries( - sqlKeys.query(projectRef, [table.schema, table.name]) - ), - queryClient.invalidateQueries( - sqlKeys.query(projectRef, ['table-definition', table.schema, table.name]) - ), + queryClient.invalidateQueries(databaseKeys.tableDefinition(projectRef, table.id)), // invalidate all views from this schema, not sure if this is needed since you can't actually delete a column // which has a view dependent on it queryClient.invalidateQueries(viewKeys.listBySchema(projectRef, table.schema)), ] : []), ]) + + if (table !== undefined) { + // We need to invalidate tableRowsAndCount after tableEditor + // to ensure the query sent is correct + await queryClient.invalidateQueries(tableRowKeys.tableRowsAndCount(projectRef, table.id)) + } + await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/data/database/constraints-query.ts b/apps/studio/data/database/constraints-query.ts index ccf0ae456da..bfefce69f65 100644 --- a/apps/studio/data/database/constraints-query.ts +++ b/apps/studio/data/database/constraints-query.ts @@ -1,9 +1,9 @@ -import { UseQueryOptions } from '@tanstack/react-query' -import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query' +import { databaseKeys } from './keys' type GetTableConstraintsVariables = { - schema?: string - table?: string + id?: number } export type Constraint = { @@ -21,19 +21,29 @@ export enum CONSTRAINT_TYPE { EXCLUSION_CONSTRAINT = 'x', } -export const getTableConstraints = ({ schema, table }: GetTableConstraintsVariables) => { +export const getTableConstraintsSql = ({ id }: GetTableConstraintsVariables) => { const sql = /* SQL */ ` - SELECT + with table_info as ( + select + n.nspname::text as schema, + c.relname::text as name, + to_regclass(concat('"', n.nspname, '"."', c.relname, '"')) as regclass + from pg_class c + join pg_namespace n on n.oid = c.relnamespace + where c.oid = ${id} +) +select con.oid as id, con.conname as name, con.contype as type - FROM pg_catalog.pg_constraint con - INNER JOIN pg_catalog.pg_class rel - ON rel.oid = con.conrelid - INNER JOIN pg_catalog.pg_namespace nsp - ON nsp.oid = connamespace - WHERE nsp.nspname = '${schema}' - AND rel.relname = '${table}'; +from pg_catalog.pg_constraint con +inner join pg_catalog.pg_class rel + on rel.oid = con.conrelid +inner join pg_catalog.pg_namespace nsp + on nsp.oid = connamespace +inner join table_info ti + on ti.schema = nsp.nspname + and ti.name = rel.relname; `.trim() return sql @@ -47,23 +57,31 @@ export type TableConstraintsVariables = GetTableConstraintsVariables & { export type TableConstraintsData = Constraint[] export type TableConstraintsError = ExecuteSqlError -export const useTableConstraintsQuery = ( - { projectRef, connectionString, schema, table }: TableConstraintsVariables, - options: UseQueryOptions = {} -) => { - return useExecuteSqlQuery( +export async function getTableConstraints( + { projectRef, connectionString, id }: TableConstraintsVariables, + signal?: AbortSignal +) { + const sql = getTableConstraintsSql({ id }) + const { result } = await executeSql( + { projectRef, connectionString, sql, queryKey: ['table-constraints', id] }, + signal + ) + + return (result as TableConstraintsData) ?? [] +} + +export const useTableConstraintsQuery = ( + { projectRef, connectionString, id }: TableConstraintsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + databaseKeys.tableConstraints(projectRef, id), + ({ signal }) => getTableConstraints({ projectRef, connectionString, id }, signal), { - projectRef, - connectionString, - sql: getTableConstraints({ schema, table }), - queryKey: ['table-constraints'], - }, - { - enabled: typeof schema !== 'undefined' && typeof table !== 'undefined', - select(data) { - return (data as any)?.result ?? [] - }, + enabled: enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined', ...options, } ) -} diff --git a/apps/studio/data/database/entity-definition-query.ts b/apps/studio/data/database/entity-definition-query.ts index 5c8ffcb8832..ad960f55f1b 100644 --- a/apps/studio/data/database/entity-definition-query.ts +++ b/apps/studio/data/database/entity-definition-query.ts @@ -1,7 +1,7 @@ -import { UseQueryOptions } from '@tanstack/react-query' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' import { SupportedAssistantEntities } from 'components/ui/AIAssistantPanel/AIAssistant.types' -import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query' +import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query' import { databaseKeys } from './keys' const generatePolicyDefinition = (policy: { @@ -21,14 +21,19 @@ CREATE POLICY "${policy.name}" on "${policy.schema}"."${policy.table}" AS ${poli } // [Joshen] Eventually should support table definition and view definition as well if possible -export const getEntityDefinitionQuery = ({ +export const getEntityDefinitionSql = ({ id, type, }: { id?: number type?: SupportedAssistantEntities | 'table' | 'view' | null }) => { - if (!id || !type) return '' + if (!id) { + throw new Error('id is required') + } + if (!type) { + throw new Error('type is required') + } switch (type) { case 'functions': @@ -79,32 +84,54 @@ export type EntityDefinitionVariables = { connectionString?: string } -export type EntityDefinitionData = string -export type EntityDefinitionError = ExecuteSqlError +export async function getEntityDefinition( + { projectRef, connectionString, id, type }: EntityDefinitionVariables, + signal?: AbortSignal +) { + if (!id) { + throw new Error('id is required') + } -export const useEntityDefinitionQuery = ( - { id, type, projectRef, connectionString }: EntityDefinitionVariables, - { enabled = true, ...options }: UseQueryOptions = {} -) => { - return useExecuteSqlQuery( + const sql = getEntityDefinitionSql({ id, type }) + const { result } = await executeSql( { projectRef, connectionString, - sql: getEntityDefinitionQuery({ id, type }), - queryKey: databaseKeys.entityDefinition(projectRef, id), + sql, + queryKey: ['entity-definition', id], }, + signal + ) + + if (type === 'functions') { + return result[0].pg_get_functiondef + } else if (type === 'rls-policies') { + return generatePolicyDefinition(result[0]) + } else { + return result[0] + } +} + +export type EntityDefinitionData = string +export type EntityDefinitionError = ExecuteSqlError + +export const useEntityDefinitionQuery = ( + { projectRef, connectionString, id, type }: EntityDefinitionVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + databaseKeys.entityDefinition(projectRef, id), + ({ signal }) => getEntityDefinition({ projectRef, connectionString, id, type }, signal), { - select(data) { - if (type === 'functions') { - return data.result[0].pg_get_functiondef - } else if (type === 'rls-policies') { - return generatePolicyDefinition(data.result[0]) - } else { - return data.result[0] - } - }, - enabled: enabled && typeof id !== 'undefined' && typeof type !== 'undefined', + enabled: + enabled && + typeof projectRef !== 'undefined' && + typeof id !== 'undefined' && + !isNaN(id) && + typeof type !== 'undefined', ...options, } ) -} diff --git a/apps/studio/data/database/entity-definitions-query.ts b/apps/studio/data/database/entity-definitions-query.ts index 4506ea58ec9..f159eb37256 100644 --- a/apps/studio/data/database/entity-definitions-query.ts +++ b/apps/studio/data/database/entity-definitions-query.ts @@ -1,9 +1,9 @@ -import { UseQueryOptions } from '@tanstack/react-query' -import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query' import { CREATE_PG_GET_TABLEDEF_SQL } from './database-query-constants' import { databaseKeys } from './keys' -export const getEntityDefinitionsQuery = ({ +export const getEntityDefinitionsSql = ({ schemas, limit = 100, }: { @@ -82,32 +82,40 @@ export type EntityDefinitionsVariables = { schemas: string[] } +export async function getEntityDefinitions( + { projectRef, connectionString, schemas, limit }: EntityDefinitionsVariables, + signal?: AbortSignal +) { + const sql = getEntityDefinitionsSql({ schemas, limit }) + const { result } = await executeSql( + { + projectRef, + connectionString, + sql, + queryKey: ['entity-definitions', schemas], + }, + signal + ) + + return result[0].data.definitions +} + type EntityDefinition = { id: number; sql: string } export type EntityDefinitionsData = EntityDefinition[] export type EntityDefinitionsError = ExecuteSqlError -export const useEntityDefinitionsQuery = < - TData extends EntityDefinitionsData = EntityDefinitionsData, ->( - { schemas, limit, projectRef, connectionString }: EntityDefinitionsVariables, +export const useEntityDefinitionsQuery = ( + { projectRef, connectionString, schemas, limit }: EntityDefinitionsVariables, { enabled = true, ...options - }: UseQueryOptions = {} -) => { - return useExecuteSqlQuery( + }: UseQueryOptions = {} +) => + useQuery( + databaseKeys.entityDefinitions(projectRef, schemas), + ({ signal }) => getEntityDefinitions({ projectRef, connectionString, schemas, limit }, signal), { - projectRef, - connectionString, - sql: getEntityDefinitionsQuery({ schemas, limit }), - queryKey: databaseKeys.entityDefinitions(projectRef, schemas), - }, - { - select(data) { - return data.result[0].data.definitions - }, - enabled: enabled && schemas.length > 0, + enabled: enabled && typeof projectRef !== 'undefined' && schemas.length > 0, ...options, } ) -} diff --git a/apps/studio/data/database/keys.ts b/apps/studio/data/database/keys.ts index c3a98169d40..ad10299daac 100644 --- a/apps/studio/data/database/keys.ts +++ b/apps/studio/data/database/keys.ts @@ -2,9 +2,13 @@ import { SupportedAssistantEntities } from 'components/ui/AIAssistantPanel/AIAss export const databaseKeys = { entityDefinition: (projectRef: string | undefined, id?: number) => - ['projects', projectRef, 'entity-definition', id].filter(Boolean), + ['projects', projectRef, 'entity-definition', id] as const, entityDefinitions: (projectRef: string | undefined, schemas: string[]) => ['projects', projectRef, 'entity-definitions', schemas] as const, + tableDefinition: (projectRef: string | undefined, id?: number) => + ['projects', projectRef, 'table-definition', id] as const, + viewDefinition: (projectRef: string | undefined, id?: number) => + ['projects', projectRef, 'view-definition', id] as const, backups: (projectRef: string | undefined) => [projectRef, 'database', 'backups'] as const, poolingConfiguration: (projectRef: string | undefined) => [projectRef, 'database', 'pooling-configuration'] as const, @@ -12,4 +16,6 @@ export const databaseKeys = { ['projects', projectRef, 'indexes', { query }] as const, indexAdvisorFromQuery: (projectRef: string | undefined, query: string) => ['projects', projectRef, 'index-advisor', { query }] as const, + tableConstraints: (projectRef: string | undefined, id?: number) => + ['projects', projectRef, 'table-constraints', id] as const, } diff --git a/apps/studio/data/database/table-definition-query.ts b/apps/studio/data/database/table-definition-query.ts index be3f657168b..190de796118 100644 --- a/apps/studio/data/database/table-definition-query.ts +++ b/apps/studio/data/database/table-definition-query.ts @@ -1,53 +1,81 @@ -import { UseQueryOptions } from '@tanstack/react-query' -import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query' import { CREATE_PG_GET_TABLEDEF_SQL } from './database-query-constants' +import { databaseKeys } from './keys' -type GetTableDefinition = { - schema?: string - name?: string +type GetTableDefinitionArgs = { + id?: number } // [Joshen] Eventually move this into entity-definition-query -export const getTableDefinitionQuery = ({ schema = 'public', name }: GetTableDefinition) => { +const getTableDefinitionSql = ({ id }: GetTableDefinitionArgs) => { const sql = /* SQL */ ` ${CREATE_PG_GET_TABLEDEF_SQL} + with table_info as ( + select + n.nspname::text as schema, + c.relname::text as name + from pg_class c + join pg_namespace n on n.oid = c.relnamespace + where c.oid = ${id} + ) select pg_temp.pg_get_tabledef ( - '${schema}', - '${name}', + t.schema, + t.name, false, 'FKEYS_INTERNAL', 'INCLUDE_TRIGGERS' ) as definition + from table_info t; `.trim() return sql } -export type TableDefinitionVariables = GetTableDefinition & { +export type TableDefinitionVariables = GetTableDefinitionArgs & { projectRef?: string connectionString?: string } +export async function getTableDefinition( + { projectRef, connectionString, id }: TableDefinitionVariables, + signal?: AbortSignal +) { + if (!id) { + throw new Error('id is required') + } + + const sql = getTableDefinitionSql({ id }) + const { result } = await executeSql( + { + projectRef, + connectionString, + sql, + queryKey: ['table-definition', id], + }, + signal + ) + + return result[0].definition.trim() as string +} + export type TableDefinitionData = string export type TableDefinitionError = ExecuteSqlError -export const useTableDefinitionQuery = ( - { projectRef, connectionString, schema, name }: TableDefinitionVariables, - options: UseQueryOptions = {} -) => { - return useExecuteSqlQuery( +export const useTableDefinitionQuery = ( + { projectRef, connectionString, id }: TableDefinitionVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + databaseKeys.tableDefinition(projectRef, id), + ({ signal }) => getTableDefinition({ projectRef, connectionString, id }, signal), { - projectRef, - connectionString, - sql: getTableDefinitionQuery({ schema, name }), - queryKey: ['table-definition', schema, name], - }, - { - select(data) { - return data.result[0].definition.trim() - }, + enabled: + enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined' && !isNaN(id), ...options, } ) -} diff --git a/apps/studio/data/database/view-definition-query.ts b/apps/studio/data/database/view-definition-query.ts index fe2a98567aa..7091077635d 100644 --- a/apps/studio/data/database/view-definition-query.ts +++ b/apps/studio/data/database/view-definition-query.ts @@ -1,45 +1,73 @@ -import { UseQueryOptions } from '@tanstack/react-query' -import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query' +import { databaseKeys } from './keys' -type GetViewDefinition = { - schema?: string - name?: string +type GetViewDefinitionArgs = { + id?: number } // [Joshen] Eventually move this into entity-definition-query -export const getViewDefinitionQuery = ({ schema, name }: GetViewDefinition) => { +export const getViewDefinitionSql = ({ id }: GetViewDefinitionArgs) => { + if (!id) { + throw new Error('id is required') + } + const sql = /* SQL */ ` - select pg_get_viewdef(to_regclass('"${schema}"."${name}"'), true) as definition + with table_info as ( + select + n.nspname::text as schema, + c.relname::text as name, + to_regclass(concat('"', n.nspname, '"."', c.relname, '"')) as regclass + from pg_class c + join pg_namespace n on n.oid = c.relnamespace + where c.oid = ${id} + ) + select pg_get_viewdef(t.regclass, true) as definition + from table_info t `.trim() return sql } -export type ViewDefinitionVariables = GetViewDefinition & { +export type ViewDefinitionVariables = GetViewDefinitionArgs & { projectRef?: string connectionString?: string } +export async function getViewDefinition( + { projectRef, connectionString, id }: ViewDefinitionVariables, + signal?: AbortSignal +) { + const sql = getViewDefinitionSql({ id }) + const { result } = await executeSql( + { + projectRef, + connectionString, + sql, + queryKey: ['view-definition', id], + }, + signal + ) + + return result[0].definition.trim() +} + export type ViewDefinitionData = string export type ViewDefinitionError = ExecuteSqlError -export const useViewDefinitionQuery = ( - { projectRef, connectionString, schema, name }: ViewDefinitionVariables, - options: UseQueryOptions = {} -) => { - return useExecuteSqlQuery( +export const useViewDefinitionQuery = ( + { projectRef, connectionString, id }: ViewDefinitionVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + databaseKeys.viewDefinition(projectRef, id), + ({ signal }) => getViewDefinition({ projectRef, connectionString, id }, signal), { - projectRef, - connectionString, - sql: getViewDefinitionQuery({ schema, name }), - queryKey: ['view-definition', schema, name], - }, - { - select(data) { - return data.result[0].definition.trim() - }, - enabled: typeof schema !== 'undefined' && typeof name !== 'undefined', + enabled: + enabled && typeof projectRef !== 'undefined' && typeof id !== 'undefined' && !isNaN(id), ...options, } ) -} diff --git a/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx b/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx index 054f2c906f1..c86eb3542b2 100644 --- a/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx +++ b/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx @@ -1,4 +1,4 @@ -import { useQueryClient } from '@tanstack/react-query' +import { QueryClient, useQueryClient } from '@tanstack/react-query' import Link from 'next/link' import { useRouter } from 'next/router' import { ComponentProps, PropsWithChildren, useCallback } from 'react' @@ -14,9 +14,54 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte import { prefetchTableEditor } from 'data/table-editor/table-editor-query' import { prefetchTableRows } from 'data/table-rows/table-rows-query' import { useFlag } from 'hooks/ui/useFlag' +import { ImpersonationRole } from 'lib/role-impersonation' import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state' import { TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE } from 'state/table-editor' +interface PrefetchEditorTablePageArgs { + queryClient: QueryClient + projectRef: string + connectionString?: string + id: number + sorts?: Sort[] + filters?: Filter[] + impersonatedRole?: ImpersonationRole +} + +export function prefetchEditorTablePage({ + queryClient, + projectRef, + connectionString, + id, + sorts, + filters, + impersonatedRole, +}: PrefetchEditorTablePageArgs) { + return prefetchTableEditor(queryClient, { + projectRef, + connectionString, + id, + }).then((entity) => { + if (entity) { + const supaTable = parseSupaTable(entity) + + const { sorts: localSorts = [], filters: localFilters = [] } = + loadTableEditorSortsAndFiltersFromLocalStorage(projectRef, entity.name, entity.schema) ?? {} + + prefetchTableRows(queryClient, { + projectRef, + connectionString, + tableId: id, + sorts: sorts ?? formatSortURLParams(supaTable.name, localSorts), + filters: filters ?? formatFilterURLParams(localFilters), + page: 1, + limit: TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE, + impersonatedRole, + }) + } + }) +} + export function usePrefetchEditorTablePage() { const router = useRouter() const queryClient = useQueryClient() @@ -35,38 +80,17 @@ export function usePrefetchEditorTablePage() { router.prefetch(`/project/${project.ref}/editor/${id}`) // Prefetch the data - prefetchTableEditor(queryClient, { + prefetchEditorTablePage({ + queryClient, projectRef: project.ref, connectionString: project.connectionString, id, + sorts, + filters, + impersonatedRole: roleImpersonationState.role, + }).catch(() => { + // eat prefetching errors as they are not critical }) - .then((entity) => { - if (entity) { - const supaTable = parseSupaTable(entity) - - const { sorts: localSorts = [], filters: localFilters = [] } = - loadTableEditorSortsAndFiltersFromLocalStorage( - project.ref, - entity.name, - entity.schema - ) ?? {} - - prefetchTableRows(queryClient, { - queryKey: [supaTable.schema, supaTable.name], - projectRef: project?.ref, - connectionString: project?.connectionString, - table: supaTable, - sorts: sorts ?? formatSortURLParams(supaTable.name, localSorts), - filters: filters ?? formatFilterURLParams(localFilters), - page: 1, - limit: TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE, - impersonatedRole: roleImpersonationState.role, - }) - } - }) - .catch(() => { - // eat prefetching errors as they are not critical - }) }, [project, queryClient, roleImpersonationState.role, router, tableEditorPrefetchingEnabled] ) diff --git a/apps/studio/data/table-editor/table-editor-query.ts b/apps/studio/data/table-editor/table-editor-query.ts index ddcd42bcdca..b9fcdc975d9 100644 --- a/apps/studio/data/table-editor/table-editor-query.ts +++ b/apps/studio/data/table-editor/table-editor-query.ts @@ -56,7 +56,7 @@ export const useTableEditorQuery = ( export function prefetchTableEditor( client: QueryClient, - { projectRef, connectionString, id }: Required + { projectRef, connectionString, id }: TableEditorVariables ) { return client.fetchQuery(tableEditorKeys.tableEditor(projectRef, id), ({ signal }) => getTableEditor( diff --git a/apps/studio/data/table-rows/get-cell-value-mutation.ts b/apps/studio/data/table-rows/get-cell-value-mutation.ts index 096a2771e40..7c22ef6855a 100644 --- a/apps/studio/data/table-rows/get-cell-value-mutation.ts +++ b/apps/studio/data/table-rows/get-cell-value-mutation.ts @@ -51,8 +51,6 @@ export const useGetCellValueMutation = ({ (vars) => getCellValue(vars), { async onSuccess(data, variables, context) { - const { projectRef, table } = variables - // await queryClient.invalidateQueries(sqlKeys.query(projectRef, [table.schema, table.name])) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/data/table-rows/keys.ts b/apps/studio/data/table-rows/keys.ts new file mode 100644 index 00000000000..38313e014ea --- /dev/null +++ b/apps/studio/data/table-rows/keys.ts @@ -0,0 +1,12 @@ +import type { GetTableRowsArgs } from './table-rows-query' + +type TableRowKeyArgs = Omit & { table?: { id?: number } } + +export const tableRowKeys = { + tableRows: (projectRef?: string, { table, ...args }: TableRowKeyArgs = {}) => + [projectRef, 'table-rows', table?.id, 'rows', args] as const, + tableRowsCount: (projectRef?: string, { table, ...args }: TableRowKeyArgs = {}) => + [projectRef, 'table-rows', table?.id, 'count', args] as const, + tableRowsAndCount: (projectRef?: string, tableId?: number) => + [projectRef, 'table-rows', tableId] as const, +} diff --git a/apps/studio/data/table-rows/table-row-create-mutation.ts b/apps/studio/data/table-rows/table-row-create-mutation.ts index 7624ca09ccd..6dce6c2b5db 100644 --- a/apps/studio/data/table-rows/table-row-create-mutation.ts +++ b/apps/studio/data/table-rows/table-row-create-mutation.ts @@ -3,15 +3,15 @@ import { toast } from 'sonner' import { Query } from 'components/grid/query/Query' import { executeSql } from 'data/sql/execute-sql-query' -import { sqlKeys } from 'data/sql/keys' import { ImpersonationRole, wrapWithRoleImpersonation } from 'lib/role-impersonation' import { isRoleImpersonationEnabled } from 'state/role-impersonation-state' import type { ResponseError } from 'types' +import { tableRowKeys } from './keys' export type TableRowCreateVariables = { projectRef: string connectionString?: string - table: { name: string; schema?: string } + table: { id: number; name: string; schema?: string } payload: any enumArrayColumns: string[] returning?: boolean @@ -74,7 +74,7 @@ export const useTableRowCreateMutation = ({ { async onSuccess(data, variables, context) { const { projectRef, table } = variables - await queryClient.invalidateQueries(sqlKeys.query(projectRef, [table.schema, table.name])) + await queryClient.invalidateQueries(tableRowKeys.tableRowsAndCount(projectRef, table.id)) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/data/table-rows/table-row-delete-all-mutation.ts b/apps/studio/data/table-rows/table-row-delete-all-mutation.ts index 15a7ad0a6b3..c09c65708aa 100644 --- a/apps/studio/data/table-rows/table-row-delete-all-mutation.ts +++ b/apps/studio/data/table-rows/table-row-delete-all-mutation.ts @@ -4,10 +4,10 @@ import { toast } from 'sonner' import { Query } from 'components/grid/query/Query' import type { Filter, SupaTable } from 'components/grid/types' import { executeSql } from 'data/sql/execute-sql-query' -import { sqlKeys } from 'data/sql/keys' import { ImpersonationRole, wrapWithRoleImpersonation } from 'lib/role-impersonation' import { isRoleImpersonationEnabled } from 'state/role-impersonation-state' import type { ResponseError } from 'types' +import { tableRowKeys } from './keys' import { formatFilterValue } from './utils' export type TableRowDeleteAllVariables = { @@ -76,7 +76,7 @@ export const useTableRowDeleteAllMutation = ({ { async onSuccess(data, variables, context) { const { projectRef, table } = variables - await queryClient.invalidateQueries(sqlKeys.query(projectRef, [table.schema, table.name])) + await queryClient.invalidateQueries(tableRowKeys.tableRowsAndCount(projectRef, table.id)) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/data/table-rows/table-row-delete-mutation.tsx b/apps/studio/data/table-rows/table-row-delete-mutation.tsx index afb9cbfec88..f79dfc45f38 100644 --- a/apps/studio/data/table-rows/table-row-delete-mutation.tsx +++ b/apps/studio/data/table-rows/table-row-delete-mutation.tsx @@ -6,11 +6,11 @@ import type { SupaRow } from 'components/grid/types' import { Markdown } from 'components/interfaces/Markdown' import { DocsButton } from 'components/ui/DocsButton' import { executeSql } from 'data/sql/execute-sql-query' -import { sqlKeys } from 'data/sql/keys' import { Entity } from 'data/table-editor/table-editor-types' import { ImpersonationRole, wrapWithRoleImpersonation } from 'lib/role-impersonation' import { isRoleImpersonationEnabled } from 'state/role-impersonation-state' import type { ResponseError } from 'types' +import { tableRowKeys } from './keys' import { getPrimaryKeys } from './utils' export type TableRowDeleteVariables = { @@ -76,7 +76,7 @@ export const useTableRowDeleteMutation = ({ { async onSuccess(data, variables, context) { const { projectRef, table } = variables - await queryClient.invalidateQueries(sqlKeys.query(projectRef, [table.schema, table.name])) + await queryClient.invalidateQueries(tableRowKeys.tableRowsAndCount(projectRef, table.id)) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/data/table-rows/table-row-truncate-mutation.ts b/apps/studio/data/table-rows/table-row-truncate-mutation.ts index 051b5d216ce..f00ddc1153a 100644 --- a/apps/studio/data/table-rows/table-row-truncate-mutation.ts +++ b/apps/studio/data/table-rows/table-row-truncate-mutation.ts @@ -3,13 +3,13 @@ import { toast } from 'sonner' import { Query } from 'components/grid/query/Query' import { executeSql } from 'data/sql/execute-sql-query' -import { sqlKeys } from 'data/sql/keys' import type { ResponseError } from 'types' +import { tableRowKeys } from './keys' export type TableRowTruncateVariables = { projectRef: string connectionString?: string - table: { name: string; schema?: string } + table: { id: number; name: string; schema?: string } } export function getTableRowTruncateSql({ table }: Pick) { @@ -51,7 +51,7 @@ export const useTableRowTruncateMutation = ({ { async onSuccess(data, variables, context) { const { projectRef, table } = variables - await queryClient.invalidateQueries(sqlKeys.query(projectRef, [table.schema, table.name])) + await queryClient.invalidateQueries(tableRowKeys.tableRowsAndCount(projectRef, table.id)) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/data/table-rows/table-row-update-mutation.ts b/apps/studio/data/table-rows/table-row-update-mutation.ts index 5c5d49783c1..e5c4470cdd5 100644 --- a/apps/studio/data/table-rows/table-row-update-mutation.ts +++ b/apps/studio/data/table-rows/table-row-update-mutation.ts @@ -3,15 +3,15 @@ import { toast } from 'sonner' import { Query } from 'components/grid/query/Query' import { executeSql } from 'data/sql/execute-sql-query' -import { sqlKeys } from 'data/sql/keys' import { ImpersonationRole, wrapWithRoleImpersonation } from 'lib/role-impersonation' import { isRoleImpersonationEnabled } from 'state/role-impersonation-state' import type { ResponseError } from 'types' +import { tableRowKeys } from './keys' export type TableRowUpdateVariables = { projectRef: string connectionString?: string - table: { name: string; schema?: string } + table: { id: number; name: string; schema?: string } configuration: { identifiers: any } payload: any enumArrayColumns: string[] @@ -59,6 +59,7 @@ export async function updateTableRow({ connectionString, sql, isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRole), + queryKey: ['table-row-update', table.id], }) return result @@ -81,7 +82,9 @@ export const useTableRowUpdateMutation = ({ { async onSuccess(data, variables, context) { const { projectRef, table } = variables - await queryClient.invalidateQueries(sqlKeys.query(projectRef, [table.schema, table.name])) + await queryClient.invalidateQueries( + tableRowKeys.tableRows(projectRef, { table: { id: table.id } }) + ) await onSuccess?.(data, variables, context) }, async onError(data, variables, context) { diff --git a/apps/studio/data/table-rows/table-rows-count-query.ts b/apps/studio/data/table-rows/table-rows-count-query.ts index 36d14f5d58a..39b9de14901 100644 --- a/apps/studio/data/table-rows/table-rows-count-query.ts +++ b/apps/studio/data/table-rows/table-rows-count-query.ts @@ -1,20 +1,22 @@ -import type { QueryKey, UseQueryOptions } from '@tanstack/react-query' +import { QueryClient, useQuery, useQueryClient, type UseQueryOptions } from '@tanstack/react-query' import { Query } from 'components/grid/query/Query' +import { parseSupaTable } from 'components/grid/SupabaseGrid.utils' import type { Filter, SupaTable } from 'components/grid/types' +import { prefetchTableEditor } from 'data/table-editor/table-editor-query' import { ImpersonationRole, wrapWithRoleImpersonation } from 'lib/role-impersonation' -import { useIsRoleImpersonationEnabled } from 'state/role-impersonation-state' -import { ExecuteSqlData, ExecuteSqlError, useExecuteSqlQuery } from '../sql/execute-sql-query' +import { isRoleImpersonationEnabled } from 'state/role-impersonation-state' +import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query' +import { tableRowKeys } from './keys' import { formatFilterValue } from './utils' type GetTableRowsCountArgs = { table?: SupaTable filters?: Filter[] enforceExactCount?: boolean - impersonatedRole?: ImpersonationRole } export const THRESHOLD_COUNT = 50000 -const COUNT_ESTIMATE_SQL = ` +const COUNT_ESTIMATE_SQL = /* SQL */ ` CREATE OR REPLACE FUNCTION pg_temp.count_estimate( query text ) RETURNS integer LANGUAGE plpgsql AS $$ @@ -27,7 +29,7 @@ END; $$; `.trim() -export const getTableRowsCountSqlQuery = ({ +export const getTableRowsCountSql = ({ table, filters = [], enforceExactCount = false, @@ -92,56 +94,78 @@ export type TableRowsCount = { is_estimate?: boolean } -export type TableRowsCountVariables = GetTableRowsCountArgs & { +export type TableRowsCountVariables = Omit & { + queryClient: QueryClient + tableId?: number + impersonatedRole?: ImpersonationRole projectRef?: string connectionString?: string - queryKey?: QueryKey } export type TableRowsCountData = TableRowsCount export type TableRowsCountError = ExecuteSqlError -export const useTableRowsCountQuery = ( +export async function getTableRowsCount( { + queryClient, projectRef, connectionString, - queryKey, - table, - enforceExactCount, + tableId, + filters, impersonatedRole, - ...args + enforceExactCount, }: TableRowsCountVariables, - options: UseQueryOptions = {} -) => { - const isRoleImpersonationEnabled = useIsRoleImpersonationEnabled() + signal?: AbortSignal +) { + const entity = await prefetchTableEditor(queryClient, { + projectRef, + connectionString, + id: tableId, + }) + if (!entity) { + throw new Error('Table not found') + } - return useExecuteSqlQuery( + const table = parseSupaTable(entity) + + const sql = wrapWithRoleImpersonation( + getTableRowsCountSql({ table, filters, enforceExactCount }), + { + projectRef: projectRef ?? 'ref', + role: impersonatedRole, + } + ) + const { result } = await executeSql( { projectRef, connectionString, - sql: wrapWithRoleImpersonation( - getTableRowsCountSqlQuery({ table, enforceExactCount, ...args }), - { projectRef: projectRef ?? 'ref', role: impersonatedRole } - ), - queryKey: [ - ...(queryKey ?? []), - { - table: { name: table?.name, schema: table?.schema }, - enforceExactCount, - impersonatedRole, - ...args, - }, - ], - isRoleImpersonationEnabled, + sql, + queryKey: ['table-rows-count', table.id], + isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRole), }, + signal + ) + + return { + count: result[0].count, + is_estimate: result[0].is_estimate ?? false, + } as TableRowsCount +} + +export const useTableRowsCountQuery = ( + { projectRef, connectionString, tableId, ...args }: Omit, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => { + const queryClient = useQueryClient() + return useQuery( + tableRowKeys.tableRowsCount(projectRef, { table: { id: tableId }, ...args }), + ({ signal }) => + getTableRowsCount({ queryClient, projectRef, connectionString, tableId, ...args }, signal), { - select(data) { - return { - count: data.result[0].count, - is_estimate: data.result[0].is_estimate ?? false, - } as TData - }, - enabled: typeof projectRef !== 'undefined' && typeof table !== 'undefined', + enabled: enabled && typeof projectRef !== 'undefined' && typeof tableId !== 'undefined', ...options, } ) diff --git a/apps/studio/data/table-rows/table-rows-query.ts b/apps/studio/data/table-rows/table-rows-query.ts index 39ecb7dce9a..f1a6aca4db0 100644 --- a/apps/studio/data/table-rows/table-rows-query.ts +++ b/apps/studio/data/table-rows/table-rows-query.ts @@ -1,34 +1,33 @@ -import type { QueryClient, QueryKey, UseQueryOptions } from '@tanstack/react-query' +import { + useQuery, + useQueryClient, + type QueryClient, + type UseQueryOptions, +} from '@tanstack/react-query' import { IS_PLATFORM } from 'common' import { Query } from 'components/grid/query/Query' +import { parseSupaTable } from 'components/grid/SupabaseGrid.utils' import { Filter, Sort, SupaRow, SupaTable } from 'components/grid/types' import { JSON_TYPES, TEXT_TYPES, } from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.constants' -import { sqlKeys } from 'data/sql/keys' +import { prefetchTableEditor } from 'data/table-editor/table-editor-query' import { KB } from 'lib/constants' import { ImpersonationRole, ROLE_IMPERSONATION_NO_RESULTS, wrapWithRoleImpersonation, } from 'lib/role-impersonation' -import { - isRoleImpersonationEnabled, - useIsRoleImpersonationEnabled, -} from 'state/role-impersonation-state' -import { - ExecuteSqlData, - ExecuteSqlError, - executeSql, - useExecuteSqlQuery, -} from '../sql/execute-sql-query' +import { isRoleImpersonationEnabled } from 'state/role-impersonation-state' +import { ExecuteSqlError, executeSql } from '../sql/execute-sql-query' import { getPagination } from '../utils/pagination' +import { tableRowKeys } from './keys' import { THRESHOLD_COUNT } from './table-rows-count-query' import { formatFilterValue } from './utils' -type GetTableRowsArgs = { +export interface GetTableRowsArgs { table?: SupaTable filters?: Filter[] sorts?: Sort[] @@ -135,7 +134,7 @@ export const fetchAllTableRows = async ({ return rows.filter((row) => row[ROLE_IMPERSONATION_NO_RESULTS] !== 1) } -export const getTableRowsSqlQuery = ({ +export const getTableRowsSql = ({ table, filters = [], sorts = [], @@ -199,58 +198,79 @@ export const getTableRowsSqlQuery = ({ export type TableRows = { rows: SupaRow[] } -export type TableRowsVariables = GetTableRowsArgs & { +export type TableRowsVariables = Omit & { + queryClient: QueryClient projectRef?: string connectionString?: string - queryKey?: QueryKey + tableId?: number } export type TableRowsData = TableRows export type TableRowsError = ExecuteSqlError -function getTableRowsQueryKey({ queryKey, table, impersonatedRole, ...args }: TableRowsVariables) { - return [ - ...(queryKey ?? []), +export async function getTableRows( + { + queryClient, + projectRef, + connectionString, + tableId, + impersonatedRole, + filters, + sorts, + limit, + page, + }: TableRowsVariables, + signal?: AbortSignal +) { + const entity = await prefetchTableEditor(queryClient, { + projectRef, + connectionString, + id: tableId, + }) + if (!entity) { + throw new Error('Table not found') + } + + const table = parseSupaTable(entity) + + const sql = wrapWithRoleImpersonation( + getTableRowsSql({ table, filters, sorts, limit, page, impersonatedRole }), { - table: { - name: table?.name, - schema: table?.schema, - columns: table?.columns.map((c) => c.name), - }, - impersonatedRole, - ...args, - }, - ] -} - -export const useTableRowsQuery = ( - { projectRef, connectionString, queryKey, table, impersonatedRole, ...args }: TableRowsVariables, - options: UseQueryOptions = {} -) => { - const isRoleImpersonationEnabled = useIsRoleImpersonationEnabled() - - return useExecuteSqlQuery( + projectRef: projectRef ?? 'ref', + role: impersonatedRole, + } + ) + const { result } = await executeSql( { projectRef, connectionString, - sql: wrapWithRoleImpersonation(getTableRowsSqlQuery({ table, ...args }), { - projectRef: projectRef ?? 'ref', - role: impersonatedRole, - }), - queryKey: getTableRowsQueryKey({ queryKey, table, impersonatedRole, ...args }), - isRoleImpersonationEnabled, + sql, + queryKey: ['table-rows', table?.id], + isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRole), }, - { - select(data: { result: Record[] }) { - const rows = data.result.map((x, index) => { - return { idx: index, ...x } as SupaRow - }) + signal + ) - return { - rows, - } as TData - }, - enabled: typeof projectRef !== 'undefined' && typeof table !== 'undefined', + const rows = result.map((x: any, index: number) => { + return { idx: index, ...x } + }) as SupaRow[] + + return { + rows, + } +} + +export const useTableRowsQuery = ( + { projectRef, connectionString, tableId, ...args }: Omit, + { enabled = true, ...options }: UseQueryOptions = {} +) => { + const queryClient = useQueryClient() + return useQuery( + tableRowKeys.tableRows(projectRef, { table: { id: tableId }, ...args }), + ({ signal }) => + getTableRows({ queryClient, projectRef, connectionString, tableId, ...args }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined' && typeof tableId !== 'undefined', ...options, } ) @@ -258,23 +278,17 @@ export const useTableRowsQuery = ( export function prefetchTableRows( client: QueryClient, - { projectRef, connectionString, queryKey, table, impersonatedRole, ...args }: TableRowsVariables + { + projectRef, + connectionString, + tableId, + impersonatedRole, + ...args + }: Omit ) { return client.fetchQuery( - sqlKeys.query(projectRef, getTableRowsQueryKey({ queryKey, table, impersonatedRole, ...args })), + tableRowKeys.tableRows(projectRef, { table: { id: tableId }, ...args }), ({ signal }) => - executeSql( - { - projectRef, - connectionString, - sql: wrapWithRoleImpersonation(getTableRowsSqlQuery({ table, ...args }), { - projectRef: projectRef ?? 'ref', - role: impersonatedRole, - }), - queryKey: getTableRowsQueryKey({ queryKey, table, impersonatedRole, ...args }), - isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRole), - }, - signal - ) + getTableRows({ queryClient: client, projectRef, connectionString, tableId, ...args }, signal) ) } From 1826e838aa98e369dcce544dbbfe9512b45a5830 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Thu, 14 Nov 2024 04:01:37 -0330 Subject: [PATCH 50/86] Dont allow editing cron job names (#30443) * Dont allow editing cron job names * Cleanup * Update apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx --------- Co-authored-by: Joshen Lim --- .../Integrations/CronJobs/CreateCronJobSheet.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx index 5b4e7a5b70f..524b5915c96 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx @@ -17,7 +17,9 @@ import { Button, Form_Shadcn_, FormControl_Shadcn_, + FormDescription_Shadcn_, FormField_Shadcn_, + FormLabel_Shadcn_, Input_Shadcn_, RadioGroupStacked, RadioGroupStackedItem, @@ -43,6 +45,7 @@ import { SqlFunctionSection } from './SqlFunctionSection' import { SqlSnippetSection } from './SqlSnippetSection' import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' import { checkDomainOfScale } from 'recharts/types/util/ChartUtils' +import { InfoIcon } from 'lucide-react' export interface CreateCronJobSheetProps { selectedCronJob?: Pick @@ -229,10 +232,14 @@ export const CreateCronJobSheet = ({ control={form.control} name="name" render={({ field }) => ( - + - + + + + Cron jobs cannot be renamed once created + )} /> From d6b32f05e9e19bad02e81a5d0f36d81afcfa9a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Thu, 14 Nov 2024 18:54:23 +0800 Subject: [PATCH 51/86] fix: billing page not rendering with Google Translate (#30468) Markdown component seems to be the root of evil here. --- .../BillingBreakdown/BillingBreakdown.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx index cee1b4f4cb9..19ddf0ca50a 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx @@ -15,7 +15,6 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import UpcomingInvoice from './UpcomingInvoice' -import { Markdown } from 'components/interfaces/Markdown' const BillingBreakdown = () => { const { slug: orgSlug } = useParams() @@ -70,10 +69,13 @@ const BillingBreakdown = () => { {invoiceFeatureEnabled && ( <> - +

+ The table shows your upcoming invoice, excluding credits. This invoice will + continue updating until the end of your billing period on{' '} + {billingCycleEnd.format('MMMM DD')}. See{' '} + usage page for a more detailed usage + breakdown. +

From 1157a20fbd6fa1ded377bed247d3035c58c43552 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 14 Nov 2024 13:02:29 +0100 Subject: [PATCH 52/86] feat: Queues v0.5 (#30418) * Rename the regular/normal queues to basic. * Move the partitioned type of queues to the end. * Add metrics for the queues. * Add actions for postponing and deleting a message. * Add queue actions for purging and deleting a queue. * Minor fixes for the empty stateof the queues page. * Add a modal to send a message to the queue. * Minor fixes. * Correct some button texts. * Refactor the metrics to switch to a rougher estimate method if it timeouts. * Fix type errors. * More minor fixes. --- .../Integrations/Queues/Queues.constants.tsx | 20 +-- .../Queues/QueuesDisabledState.tsx | 13 +- .../Integrations/Queues/QueuesListing.tsx | 9 +- .../Integrations/Queues/QueuesRows.tsx | 93 ++++++++---- .../SingleQueue/MessageDetailsPanel.tsx | 134 +++++++++++++---- .../Queues/SingleQueue/PurgeQueue.tsx | 65 +++++++++ .../Queues/SingleQueue/QueueDataGrid.tsx | 9 +- .../Queues/SingleQueue/SendMessageModal.tsx | 138 ++++++++++++++++++ ...database-queue-messages-delete-mutation.ts | 68 +++++++++ .../database-queue-messages-read-mutation.ts | 70 +++++++++ .../database-queue-messages-send-mutation.ts | 70 +++++++++ .../database-queues-delete-mutation.ts | 2 +- .../database-queues-metrics-query.ts | 90 ++++++++++++ .../database-queues-purge-mutation.ts | 61 ++++++++ apps/studio/data/database-queues/keys.ts | 6 +- .../[ref]/integrations/queues/[name].tsx | 75 ++++++++-- .../src/components/shadcn/ui/breadcrumb.tsx | 10 +- 17 files changed, 828 insertions(+), 105 deletions(-) create mode 100644 apps/studio/components/interfaces/Integrations/Queues/SingleQueue/PurgeQueue.tsx create mode 100644 apps/studio/components/interfaces/Integrations/Queues/SingleQueue/SendMessageModal.tsx create mode 100644 apps/studio/data/database-queues/database-queue-messages-delete-mutation.ts create mode 100644 apps/studio/data/database-queues/database-queue-messages-read-mutation.ts create mode 100644 apps/studio/data/database-queues/database-queue-messages-send-mutation.ts create mode 100644 apps/studio/data/database-queues/database-queues-metrics-query.ts create mode 100644 apps/studio/data/database-queues/database-queues-purge-mutation.ts diff --git a/apps/studio/components/interfaces/Integrations/Queues/Queues.constants.tsx b/apps/studio/components/interfaces/Integrations/Queues/Queues.constants.tsx index 6ba001477a5..a80a56be71e 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/Queues.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/Queues.constants.tsx @@ -2,10 +2,17 @@ import { Rows2, Rows3, Rows4 } from 'lucide-react' export const QUEUE_TYPES = [ { - value: 'normal', + value: 'basic', icon: , - label: 'Normal queue', - description: 'Create a normal queue.', + label: 'Basic queue', + description: 'Create a basic queue.', + }, + { + value: 'unlogged', + icon: , + label: 'Unlogged queue', + description: + 'Creates an unlogged queue which loses all data on database restart. Can be useful when write throughput is more important than durability.', }, { value: 'partitioned', @@ -13,11 +20,4 @@ export const QUEUE_TYPES = [ label: 'Partitioned queue', description: 'Create a partitioned queue which is optimized for large amount of messages', }, - - { - value: 'unlogged', - icon: , - label: 'Unlogged queue', - description: 'Creates an unlogged queue which loses all data on database restart.', - }, ] as const diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesDisabledState.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesDisabledState.tsx index ad4331f6706..5de88c73820 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/QueuesDisabledState.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesDisabledState.tsx @@ -1,12 +1,11 @@ import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { ExternalLink } from 'lucide-react' -import Link from 'next/link' import { useState } from 'react' import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' +import { DocsButton } from 'components/ui/DocsButton' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { Button } from 'ui' @@ -65,15 +64,7 @@ export const QueuesDisabledState = () => { )} - +
diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesListing.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesListing.tsx index f34ad985e9d..eba0a0779b3 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/QueuesListing.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesListing.tsx @@ -69,10 +69,10 @@ export const QueuesListing = () => { {queues.length === 0 ? (
-

No queues created yet

+

No queues created yet

) : ( @@ -98,9 +98,12 @@ export const QueuesListing = () => { Type - + Created at + +
Size
+
} diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesRows.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesRows.tsx index ec68ed69772..629b0347d9f 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/QueuesRows.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesRows.tsx @@ -1,22 +1,76 @@ import dayjs from 'dayjs' import { includes, sortBy } from 'lodash' -import { ChevronRight } from 'lucide-react' +import { ChevronRight, Loader2 } from 'lucide-react' +import { useRouter } from 'next/router' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import Table from 'components/to-be-cleaned/Table' +import { useQueuesMetricsQuery } from 'data/database-queues/database-queues-metrics-query' import { PostgresQueue } from 'data/database-queues/database-queues-query' import { DATETIME_FORMAT } from 'lib/constants' -import { useRouter } from 'next/router' interface QueuesRowsProps { queues: PostgresQueue[] filterString: string } -export const QueuesRows = ({ queues: fetchedQueues, filterString }: QueuesRowsProps) => { +const QueueRow = ({ queue }: { queue: PostgresQueue }) => { const router = useRouter() const { project: selectedProject } = useProjectContext() + const { data: metrics, isLoading } = useQueuesMetricsQuery( + { + queueName: queue.queue_name, + projectRef: selectedProject?.ref, + connectionString: selectedProject?.connectionString, + }, + { + staleTime: 30 * 1000, // 60 seconds, talk with Oli whether this is ok to call every minute + } + ) + + const type = queue.is_partitioned ? 'Partitioned' : queue.is_unlogged ? 'Unlogged' : 'Basic' + + return ( + { + router.push(`/project/${selectedProject?.ref}/integrations/queues/${queue.queue_name}`) + }} + className="hover:" + > + +

{queue.queue_name}

+
+ +

+ {type} +

+
+ +

{dayjs(queue.created_at).format(DATETIME_FORMAT)}

+
+ +
+ {isLoading ? ( + + ) : ( +

+ {metrics?.queue_length} {metrics?.method === 'estimated' ? '(Approximate)' : null} +

+ )} +
+
+ +
+ +
+
+
+ ) +} + +export const QueuesRows = ({ queues: fetchedQueues, filterString }: QueuesRowsProps) => { const filteredQueues = fetchedQueues.filter((x) => includes(x.queue_name.toLowerCase(), filterString.toLowerCase()) ) @@ -25,7 +79,7 @@ export const QueuesRows = ({ queues: fetchedQueues, filterString }: QueuesRowsPr if (queues.length === 0 && filterString.length > 0) { return ( - +

No results found

Your search for "{filterString}" did not return any results @@ -37,34 +91,9 @@ export const QueuesRows = ({ queues: fetchedQueues, filterString }: QueuesRowsPr return ( <> - {queues.map((q) => { - const type = q.is_partitioned ? 'Partitioned' : q.is_unlogged ? 'Unlogged' : 'Regular' - - return ( - { - router.push(`/project/${selectedProject?.ref}/integrations/queues/${q.queue_name}`) - }} - className="hover:" - > - -

{q.queue_name}

-
- -

- {type} -

-
- -

{dayjs(q.created_at).format(DATETIME_FORMAT)}

-
- - - -
- ) - })} + {queues.map((q) => ( + + ))} ) } diff --git a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/MessageDetailsPanel.tsx b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/MessageDetailsPanel.tsx index e7fed0b97e2..9de86466dec 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/MessageDetailsPanel.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/MessageDetailsPanel.tsx @@ -1,12 +1,15 @@ +import { useEscapeKeydown } from '@radix-ui/react-use-escape-keydown' import { isNil, noop } from 'lodash' -import { Archive, X } from 'lucide-react' +import { Archive, Clock12, Trash2, X } from 'lucide-react' import { useState } from 'react' import { useParams } from 'common' import { MonacoEditor } from 'components/grid/components/common/MonacoEditor' import { RowAction, RowData } from 'components/interfaces/Auth/Users/UserOverview' import { useDatabaseQueueMessageArchiveMutation } from 'data/database-queues/database-queue-messages-archive-mutation' +import { useDatabaseQueueMessageDeleteMutation } from 'data/database-queues/database-queue-messages-delete-mutation' import { PostgresQueueMessage } from 'data/database-queues/database-queue-messages-infinite-query' +import { useDatabaseQueueMessageReadMutation } from 'data/database-queues/database-queue-messages-read-mutation' import dayjs from 'dayjs' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { prettifyJSON } from 'lib/helpers' @@ -48,7 +51,25 @@ export const MessageDetailsPanel = ({ const { name: queueName } = useParams() const project = useSelectedProject() - const { mutate, isLoading, isSuccess } = useDatabaseQueueMessageArchiveMutation() + useEscapeKeydown(() => setSelectedMessage(null)) + + const { + mutate: archiveMessage, + isLoading: isLoadingArchive, + isSuccess: isSuccessArchive, + } = useDatabaseQueueMessageArchiveMutation() + + const { + mutate: readMessage, + isLoading: isLoadingRead, + isSuccess: isSuccessRead, + } = useDatabaseQueueMessageReadMutation() + + const { + mutate: deleteMessage, + isLoading: isLoadingDelete, + isSuccess: isSuccessDelete, + } = useDatabaseQueueMessageDeleteMutation() const initialValue = JSON.stringify(selectedMessage?.message) const jsonString = prettifyJSON(!isNil(initialValue) ? tryFormatInitialValue(initialValue) : '') @@ -100,7 +121,7 @@ export const MessageDetailsPanel = ({
-

Payload

+

Payload

-
+
{!selectedMessage.archived_at ? ( - , - text: 'Archive', - isLoading: isLoading, - onClick: () => { - mutate({ - projectRef: project!.ref, - connectionString: project?.connectionString, - queryName: queueName!, - messageId: selectedMessage.msg_id, - }) - }, - }} - success={ - isSuccess - ? { - title: 'Archived', - description: 'The message is archived successfully.', - } - : undefined - } - /> + <> + , + text: 'Postpone', + isLoading: isLoadingRead, + onClick: () => { + readMessage({ + projectRef: project!.ref, + connectionString: project?.connectionString, + queryName: queueName!, + messageId: selectedMessage.msg_id, + duration: 60, + }) + }, + }} + success={ + isSuccessRead + ? { + title: 'Postponed', + description: 'The message was postponed for 60 seconds.', + } + : undefined + } + /> + , + text: 'Archive', + isLoading: isLoadingArchive, + type: 'warning', + onClick: () => { + archiveMessage({ + projectRef: project!.ref, + connectionString: project?.connectionString, + queryName: queueName!, + messageId: selectedMessage.msg_id, + }) + }, + }} + success={ + isSuccessArchive + ? { + title: 'Archived', + description: 'The message was archived successfully.', + } + : undefined + } + /> + , + text: 'Delete', + type: 'danger', + isLoading: isLoadingDelete, + onClick: () => { + deleteMessage({ + projectRef: project!.ref, + connectionString: project?.connectionString, + queueName: queueName!, + messageId: selectedMessage.msg_id, + }) + }, + }} + success={ + isSuccessDelete + ? { + title: 'Deleted', + description: 'The message was deleted successfully.', + } + : undefined + } + /> + ) : null}
diff --git a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/PurgeQueue.tsx b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/PurgeQueue.tsx new file mode 100644 index 00000000000..5743748e97a --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/PurgeQueue.tsx @@ -0,0 +1,65 @@ +import { useRouter } from 'next/router' +import { toast } from 'sonner' + +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useDatabaseQueuePurgeMutation } from 'data/database-queues/database-queues-purge-mutation' +import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' + +interface PurgeQueueProps { + queueName: string + visible: boolean + onClose: () => void +} + +const PurgeQueue = ({ queueName, visible, onClose }: PurgeQueueProps) => { + const { project } = useProjectContext() + const router = useRouter() + + const { mutate: purgeDatabaseQueue, isLoading } = useDatabaseQueuePurgeMutation({ + onSuccess: () => { + toast.success(`Successfully purged queue ${queueName}`) + router.push(`/project/${project?.ref}/integrations/queues`) + onClose() + }, + }) + + async function handlePurge() { + if (!project) return console.error('Project is required') + + purgeDatabaseQueue({ + queueName: queueName, + projectRef: project.ref, + connectionString: project.connectionString, + }) + } + + if (!queueName) { + return null + } + + return ( + onClose()} + onConfirm={handlePurge} + title="Purge this queue" + loading={isLoading} + confirmLabel={`Purge queue ${queueName}`} + confirmPlaceholder="Type in name of queue" + confirmString={queueName ?? 'Unknown'} + text={ + <> + This will purge the queue{' '} + {queueName} + + } + alert={{ + title: + "This action will delete all messages from the queue. They can't be recovered afterwards.", + }} + /> + ) +} + +export default PurgeQueue diff --git a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/QueueDataGrid.tsx b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/QueueDataGrid.tsx index e5e30783d41..c77211a1c7f 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/QueueDataGrid.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/QueueDataGrid.tsx @@ -6,17 +6,18 @@ import { UIEvent, useMemo, useRef } from 'react' import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid' import { PostgresQueueMessage } from 'data/database-queues/database-queue-messages-infinite-query' -import { Badge, ResizableHandle, ResizablePanel, ResizablePanelGroup, cn } from 'ui' +import { Badge, Button, ResizableHandle, ResizablePanel, ResizablePanelGroup, cn } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { DATE_FORMAT, MessageDetailsPanel } from './MessageDetailsPanel' interface QueueDataGridProps { isLoading: boolean messages: PostgresQueueMessage[] + showMessageModal: () => void fetchNextPage: () => void } -function isAtBottom({ currentTarget }: React.UIEvent): boolean { +function isAtBottom({ currentTarget }: UIEvent): boolean { return currentTarget.scrollTop + 10 >= currentTarget.scrollHeight - currentTarget.clientHeight } @@ -123,6 +124,7 @@ const columns = messagesCols.map((col) => { export const QueueMessagesDataGrid = ({ isLoading, messages, + showMessageModal, fetchNextPage, }: QueueDataGridProps) => { const gridRef = useRef(null) @@ -188,6 +190,9 @@ export const QueueMessagesDataGrid = ({

The selected queue doesn't have any messages.

+
), diff --git a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/SendMessageModal.tsx b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/SendMessageModal.tsx new file mode 100644 index 00000000000..26fb2c27c1b --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/SendMessageModal.tsx @@ -0,0 +1,138 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { SubmitHandler, useForm } from 'react-hook-form' +import z from 'zod' + +import { useParams } from 'common' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import CodeEditor from 'components/ui/CodeEditor/CodeEditor' +import { useDatabaseQueueMessageSendMutation } from 'data/database-queues/database-queue-messages-send-mutation' +import { useEffect } from 'react' +import { toast } from 'sonner' +import { Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input, Modal } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +interface SendMessageModalProps { + visible: boolean + onClose: () => void +} + +const FormSchema = z.object({ + delay: z.coerce.number().int().gte(0).default(5), + payload: z.string().refine( + (val) => { + try { + JSON.parse(val) + } catch { + return false + } + }, + { + message: 'The payload should be a JSON object', + } + ), +}) + +export type SendMessageForm = z.infer + +const FORM_ID = 'QUEUES_SEND_MESSAGE_FORM' + +export const SendMessageModal = ({ visible, onClose }: SendMessageModalProps) => { + const { name: queueName } = useParams() + const { project } = useProjectContext() + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + delay: 1, + payload: '{}', + }, + }) + + const { isLoading, mutate } = useDatabaseQueueMessageSendMutation({ + onSuccess: () => { + toast.success(`Successfully added a message to the queue.`) + onClose() + }, + }) + + const onSubmit: SubmitHandler = (values) => { + mutate({ + projectRef: project?.ref!, + connectionString: project?.connectionString, + queueName: queueName!, + payload: values.payload, + delay: values.delay, + }) + } + + useEffect(() => { + if (visible) { + form.reset({ delay: 1, payload: '{}' }) + } + }, [visible]) + + return ( + { + const values = form.getValues() + onSubmit(values) + }} + > + + +
+ ( + + + sec

} + /> +
+
+ )} + /> + ( + + + field.onChange(e)} + options={{ wordWrap: 'off', contextmenu: false }} + value={field.value} + /> + + + )} + /> + +
+
+
+ ) +} diff --git a/apps/studio/data/database-queues/database-queue-messages-delete-mutation.ts b/apps/studio/data/database-queues/database-queue-messages-delete-mutation.ts new file mode 100644 index 00000000000..65c0c124bf2 --- /dev/null +++ b/apps/studio/data/database-queues/database-queue-messages-delete-mutation.ts @@ -0,0 +1,68 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { executeSql } from 'data/sql/execute-sql-query' +import type { ResponseError } from 'types' +import { databaseQueuesKeys } from './keys' + +export type DatabaseQueueMessageDeleteVariables = { + projectRef: string + connectionString?: string + queueName: string + messageId: number +} + +export async function deleteDatabaseQueueMessage({ + projectRef, + connectionString, + queueName, + messageId, +}: DatabaseQueueMessageDeleteVariables) { + const { result } = await executeSql({ + projectRef, + connectionString, + sql: `SELECT * FROM pgmq.delete('${queueName}', ${messageId})`, + queryKey: databaseQueuesKeys.create(), + }) + + return result +} + +type DatabaseQueueMessageDeleteData = Awaited> + +export const useDatabaseQueueMessageDeleteMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions< + DatabaseQueueMessageDeleteData, + ResponseError, + DatabaseQueueMessageDeleteVariables + >, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation< + DatabaseQueueMessageDeleteData, + ResponseError, + DatabaseQueueMessageDeleteVariables + >((vars) => deleteDatabaseQueueMessage(vars), { + async onSuccess(data, variables, context) { + const { projectRef, queueName } = variables + await queryClient.invalidateQueries( + databaseQueuesKeys.getMessagesInfinite(projectRef, queueName) + ) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to delete database queue message: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/data/database-queues/database-queue-messages-read-mutation.ts b/apps/studio/data/database-queues/database-queue-messages-read-mutation.ts new file mode 100644 index 00000000000..601675001cb --- /dev/null +++ b/apps/studio/data/database-queues/database-queue-messages-read-mutation.ts @@ -0,0 +1,70 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { executeSql } from 'data/sql/execute-sql-query' +import type { ResponseError } from 'types' +import { databaseQueuesKeys } from './keys' + +export type DatabaseQueueMessageReadVariables = { + projectRef: string + connectionString?: string + queryName: string + duration: number + messageId: number +} + +export async function readDatabaseQueueMessage({ + projectRef, + connectionString, + queryName, + messageId, + duration, +}: DatabaseQueueMessageReadVariables) { + const { result } = await executeSql({ + projectRef, + connectionString, + sql: `select * from pgmq.set_vt('${queryName}', ${messageId}, ${duration})`, + queryKey: databaseQueuesKeys.create(), + }) + + return result +} + +type DatabaseQueueMessageReadData = Awaited> + +export const useDatabaseQueueMessageReadMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions< + DatabaseQueueMessageReadData, + ResponseError, + DatabaseQueueMessageReadVariables + >, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation< + DatabaseQueueMessageReadData, + ResponseError, + DatabaseQueueMessageReadVariables + >((vars) => readDatabaseQueueMessage(vars), { + async onSuccess(data, variables, context) { + const { projectRef, queryName } = variables + await queryClient.invalidateQueries( + databaseQueuesKeys.getMessagesInfinite(projectRef, queryName) + ) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to postpone database queue message: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/data/database-queues/database-queue-messages-send-mutation.ts b/apps/studio/data/database-queues/database-queue-messages-send-mutation.ts new file mode 100644 index 00000000000..e06600ff413 --- /dev/null +++ b/apps/studio/data/database-queues/database-queue-messages-send-mutation.ts @@ -0,0 +1,70 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { executeSql } from 'data/sql/execute-sql-query' +import type { ResponseError } from 'types' +import { databaseQueuesKeys } from './keys' + +export type DatabaseQueueMessageSendVariables = { + projectRef: string + connectionString?: string + queueName: string + payload: string + delay: number +} + +export async function sendDatabaseQueueMessage({ + projectRef, + connectionString, + queueName, + payload, + delay, +}: DatabaseQueueMessageSendVariables) { + const { result } = await executeSql({ + projectRef, + connectionString, + sql: `select * from pgmq.send( '${queueName}', '${payload}', ${delay})`, + queryKey: databaseQueuesKeys.create(), + }) + + return result +} + +type DatabaseQueueMessageSendData = Awaited> + +export const useDatabaseQueueMessageSendMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions< + DatabaseQueueMessageSendData, + ResponseError, + DatabaseQueueMessageSendVariables + >, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation< + DatabaseQueueMessageSendData, + ResponseError, + DatabaseQueueMessageSendVariables + >((vars) => sendDatabaseQueueMessage(vars), { + async onSuccess(data, variables, context) { + const { projectRef, queueName } = variables + await queryClient.invalidateQueries( + databaseQueuesKeys.getMessagesInfinite(projectRef, queueName) + ) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to send database queue message: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/data/database-queues/database-queues-delete-mutation.ts b/apps/studio/data/database-queues/database-queues-delete-mutation.ts index 7cdd8b3df41..83c2c828359 100644 --- a/apps/studio/data/database-queues/database-queues-delete-mutation.ts +++ b/apps/studio/data/database-queues/database-queues-delete-mutation.ts @@ -20,7 +20,7 @@ export async function deleteDatabaseQueue({ projectRef, connectionString, sql: `select * from pgmq.drop_queue('${queueName}');`, - queryKey: databaseQueuesKeys.delete(), + queryKey: databaseQueuesKeys.delete(queueName), }) return result diff --git a/apps/studio/data/database-queues/database-queues-metrics-query.ts b/apps/studio/data/database-queues/database-queues-metrics-query.ts new file mode 100644 index 00000000000..a7617213b7c --- /dev/null +++ b/apps/studio/data/database-queues/database-queues-metrics-query.ts @@ -0,0 +1,90 @@ +import { UseQueryOptions, useQuery } from '@tanstack/react-query' +import { handleError } from 'data/fetchers' +import { executeSql } from 'data/sql/execute-sql-query' +import { ResponseError } from 'types' +import { databaseQueuesKeys } from './keys' + +export type DatabaseQueuesMetricsVariables = { + projectRef?: string + connectionString?: string + queueName: string +} + +export type PostgresQueueMetric = { + queue_name: string + queue_length: number + method: 'estimated' | 'precise' +} + +const preciseMetricsSqlQuery = (queueName: string) => ` + set local statement_timeout = '1s'; + SELECT + COUNT(*) AS row_count + FROM + "pgmq"."q_${queueName}"; +` + +const estimateMetricsSqlQuery = (queueName: string) => ` + select + reltuples::bigint as estimated_rows + from + pg_class + where + relname = 'q_${queueName}' + and relnamespace = 'pgmq'::regnamespace; +` + +export async function getDatabaseQueuesMetrics({ + projectRef, + connectionString, + queueName, +}: DatabaseQueuesMetricsVariables) { + if (!projectRef) throw new Error('Project ref is required') + + try { + const { result } = await executeSql({ + projectRef, + connectionString, + sql: preciseMetricsSqlQuery(queueName), + }) + return { + queue_name: queueName, + queue_length: result[0].row_count, + method: 'precise', + } as PostgresQueueMetric + } catch (error: any) { + // if the error is caused because the count timeouted, try to fetch an approximate count + if (error?.message === 'canceling statement due to statement timeout') { + const { result } = await executeSql({ + projectRef, + connectionString, + sql: estimateMetricsSqlQuery(queueName), + }) + return { + queue_name: queueName, + queue_length: result[0].estimated_rows, + method: 'estimated', + } as PostgresQueueMetric + } + return handleError(error) + } +} + +export type DatabaseQueuesMetricsData = PostgresQueueMetric +export type DatabaseQueuesMetricsError = ResponseError + +export const useQueuesMetricsQuery = ( + { projectRef, connectionString, queueName }: DatabaseQueuesMetricsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + databaseQueuesKeys.metrics(projectRef, queueName), + () => getDatabaseQueuesMetrics({ projectRef, connectionString, queueName }), + { + enabled: enabled && typeof projectRef !== 'undefined', + ...options, + } + ) diff --git a/apps/studio/data/database-queues/database-queues-purge-mutation.ts b/apps/studio/data/database-queues/database-queues-purge-mutation.ts new file mode 100644 index 00000000000..7a4419b64ae --- /dev/null +++ b/apps/studio/data/database-queues/database-queues-purge-mutation.ts @@ -0,0 +1,61 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { executeSql } from 'data/sql/execute-sql-query' +import type { ResponseError } from 'types' +import { databaseQueuesKeys } from './keys' + +export type DatabaseQueuePurgeVariables = { + projectRef: string + connectionString?: string + queueName: string +} + +export async function purgeDatabaseQueue({ + projectRef, + connectionString, + queueName, +}: DatabaseQueuePurgeVariables) { + const { result } = await executeSql({ + projectRef, + connectionString, + sql: `select * from pgmq.purge_queue('${queueName}');`, + queryKey: databaseQueuesKeys.purge(queueName), + }) + + return result +} + +type DatabaseQueuePurgeData = Awaited> + +export const useDatabaseQueuePurgeMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => purgeDatabaseQueue(vars), + { + async onSuccess(data, variables, context) { + const { projectRef, queueName } = variables + await queryClient.invalidateQueries( + databaseQueuesKeys.getMessagesInfinite(projectRef, queueName) + ) + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to purge database queue: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/apps/studio/data/database-queues/keys.ts b/apps/studio/data/database-queues/keys.ts index c0c484c93aa..228c5caa694 100644 --- a/apps/studio/data/database-queues/keys.ts +++ b/apps/studio/data/database-queues/keys.ts @@ -1,7 +1,11 @@ export const databaseQueuesKeys = { create: () => ['queues', 'create'] as const, - delete: () => ['queues', 'delete'] as const, + delete: (name: string) => ['queues', name, 'delete'] as const, + purge: (name: string) => ['queues', name, 'purge'] as const, getMessagesInfinite: (projectRef: string | undefined, queueName: string, options?: object) => ['projects', projectRef, 'queues', queueName, options].filter(Boolean), list: (projectRef: string | undefined) => ['projects', projectRef, 'queues'] as const, + // invalidating queues.list will also invalidate queues.metrics + metrics: (projectRef: string | undefined, queueName: string) => + ['projects', projectRef, 'queues', 'metrics', queueName] as const, } diff --git a/apps/studio/pages/project/[ref]/integrations/queues/[name].tsx b/apps/studio/pages/project/[ref]/integrations/queues/[name].tsx index 0acaa3f36ca..1ed8a0dfd97 100644 --- a/apps/studio/pages/project/[ref]/integrations/queues/[name].tsx +++ b/apps/studio/pages/project/[ref]/integrations/queues/[name].tsx @@ -1,16 +1,29 @@ +import { Paintbrush, Trash2 } from 'lucide-react' +import Link from 'next/link' import { useMemo, useState } from 'react' import { useParams } from 'common' import DeleteQueue from 'components/interfaces/Integrations/Queues/SingleQueue/DeleteQueue' +import PurgeQueue from 'components/interfaces/Integrations/Queues/SingleQueue/PurgeQueue' import { QUEUE_MESSAGE_TYPE } from 'components/interfaces/Integrations/Queues/SingleQueue/Queue.utils' import { QueueMessagesDataGrid } from 'components/interfaces/Integrations/Queues/SingleQueue/QueueDataGrid' import { QueueFilters } from 'components/interfaces/Integrations/Queues/SingleQueue/QueueFilters' +import { SendMessageModal } from 'components/interfaces/Integrations/Queues/SingleQueue/SendMessageModal' import ProjectIntegrationsLayout from 'components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { FormHeader } from 'components/ui/Forms/FormHeader' import { useQueueMessagesInfiniteQuery } from 'data/database-queues/database-queue-messages-infinite-query' import type { NextPageWithLayout } from 'types' -import { Button, LoadingLine } from 'ui' +import { + Breadcrumb_Shadcn_, + BreadcrumbItem_Shadcn_, + BreadcrumbLink_Shadcn_, + BreadcrumbList_Shadcn_, + BreadcrumbPage_Shadcn_, + BreadcrumbSeparator_Shadcn_, + Button, + LoadingLine, + Separator, +} from 'ui' const QueueMessagesPage: NextPageWithLayout = () => { // TODO: Change this to the correct permissions @@ -23,6 +36,8 @@ const QueueMessagesPage: NextPageWithLayout = () => { const { name: queueName } = useParams() const { project } = useProjectContext() + const [sendMessageModalShown, setSendMessageModalShown] = useState(false) + const [purgeQueueModalShown, setPurgeQueueModalShown] = useState(false) const [deleteQueueModalShown, setDeleteQueueModalShown] = useState(false) const [selectedTypes, setSelectedTypes] = useState([]) @@ -38,34 +53,72 @@ const QueueMessagesPage: NextPageWithLayout = () => { ) const messages = useMemo(() => data?.pages.flatMap((p) => p), [data?.pages]) - if (isLoading && isError) { + if (isError) { return null } return (
- setDeleteQueueModalShown(true)}> - Delete the queue +
+
+ + + + + Queues + + + + + {queueName} + + + +
+
+ - } - /> + + {/* } */} +
+
setSendMessageModalShown(true)} fetchNextPage={fetchNextPage} /> + setSendMessageModalShown(false)} + /> setDeleteQueueModalShown(false)} /> + setPurgeQueueModalShown(false)} + />
) } diff --git a/packages/ui/src/components/shadcn/ui/breadcrumb.tsx b/packages/ui/src/components/shadcn/ui/breadcrumb.tsx index 7bddf8fa745..e76be186190 100644 --- a/packages/ui/src/components/shadcn/ui/breadcrumb.tsx +++ b/packages/ui/src/components/shadcn/ui/breadcrumb.tsx @@ -1,6 +1,6 @@ -import * as React from 'react' -import { ChevronRightIcon } from 'lucide-react' import { Slot } from '@radix-ui/react-slot' +import { ChevronRightIcon } from 'lucide-react' +import * as React from 'react' import { cn } from '../../../lib/utils' const Breadcrumb = React.forwardRef< @@ -64,7 +64,7 @@ const BreadcrumbPage = React.forwardRef ) @@ -108,10 +108,10 @@ BreadcrumbEllipsis.displayName = 'BreadcrumbEllipsis' export { Breadcrumb, - BreadcrumbList, + BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, + BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, - BreadcrumbEllipsis, } From 37a9a58880f8aa096efc72b49977375abbe02757 Mon Sep 17 00:00:00 2001 From: Etienne Stalmans Date: Thu, 14 Nov 2024 14:52:26 +0100 Subject: [PATCH 53/86] chore: add security as codeowners (#30432) Security need to review pages that contain payment processor content, as per PCI-DSS. --- .github/CODEOWNERS | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 40167c5036e..f3b6f7dccd5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,4 +10,10 @@ /apps/www/public/images/blog @supabase/marketing /apps/www/lib/redirects.js -/docker/ @supabase/cli +/docker/ @supabase/dev-workflows + +/apps/studio/next.config.js @supabase/security +/apps/studio/components/interfaces/Billing/Payment @supabase/security +/apps/studio/components/interfaces/Organization/BillingSettings/ @supabase/security +/apps/studio/components/interfaces/Organization/Documents/ @supabase/security +/apps/studio/pages/new/index.tsx @supabase/security \ No newline at end of file From ceba5ebc87acc5a94622fc9e90aa4537783e8a33 Mon Sep 17 00:00:00 2001 From: Fabrizio Date: Thu, 14 Nov 2024 17:35:02 +0100 Subject: [PATCH 54/86] feat: image transformatio toggle (#30094) * feat: image transformatio toggle * fix api types * move form field to top level * regenerate api types * fix ProjectDailyStatsAttribute type * fix ts * update styling * Update image transformation description --------- Co-authored-by: Alaister Young Co-authored-by: Joshen Lim --- .../StorageSettings/StorageSettings.tsx | 60 +++++++++++++++++-- .../project-storage-config-update-mutation.ts | 7 +++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx index 41099ee368b..acc0d6d5ca9 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageSettings/StorageSettings.tsx @@ -31,9 +31,17 @@ import { SelectTrigger_Shadcn_, SelectValue_Shadcn_, Select_Shadcn_, + Switch, } from 'ui' import { STORAGE_FILE_SIZE_LIMIT_MAX_BYTES, StorageSizeUnits } from './StorageSettings.constants' import { convertFromBytes, convertToBytes } from './StorageSettings.utils' +import { Markdown } from 'components/interfaces/Markdown' + +interface StorageSettingsState { + fileSizeLimit: number + unit: StorageSizeUnits + imageTransformationEnabled: boolean +} const StorageSettings = () => { const { ref: projectRef } = useParams() @@ -54,18 +62,30 @@ const StorageSettings = () => { }) const isFreeTier = isSuccessSubscription && subscription.plan.id === 'free' - const [initialValues, setInitialValues] = useState({ + const [initialValues, setInitialValues] = useState({ fileSizeLimit: 0, unit: StorageSizeUnits.BYTES, + imageTransformationEnabled: !isFreeTier, }) useEffect(() => { if (isSuccess && config) { - const { fileSizeLimit } = config + const { fileSizeLimit, features } = config const { value, unit } = convertFromBytes(fileSizeLimit ?? 0) - setInitialValues({ fileSizeLimit: value, unit: unit }) + const imageTransformationEnabled = features?.imageTransformation?.enabled ?? !isFreeTier + + setInitialValues({ + fileSizeLimit: value, + unit: unit, + imageTransformationEnabled, + }) + // Reset the form values when the config values load - form.reset({ fileSizeLimit: value, unit: unit }) + form.reset({ + fileSizeLimit: value, + unit: unit, + imageTransformationEnabled, + }) } }, [isSuccess, config]) @@ -77,6 +97,7 @@ const StorageSettings = () => { .object({ fileSizeLimit: z.coerce.number(), unit: z.nativeEnum(StorageSizeUnits), + imageTransformationEnabled: z.boolean(), }) .superRefine((data, ctx) => { const { unit, fileSizeLimit } = data @@ -95,7 +116,7 @@ const StorageSettings = () => { resolver: zodResolver(FormSchema), defaultValues: initialValues, }) - const { fileSizeLimit: limit, unit: storageUnit } = form.watch() + const { fileSizeLimit: limit, unit: storageUnit, imageTransformationEnabled } = form.watch() const { mutate: updateStorageConfig, isLoading: isUpdating } = useProjectStorageConfigUpdateUpdateMutation({ @@ -109,6 +130,11 @@ const StorageSettings = () => { updateStorageConfig({ projectRef, fileSizeLimit: convertToBytes(data.fileSizeLimit, data.unit), + features: { + imageTransformation: { + enabled: data.imageTransformationEnabled, + }, + }, }) } @@ -205,7 +231,31 @@ const StorageSettings = () => {

+ +
+
+

Enable Image Transformation

+
+
+
+ ( + + )} + /> +
+ +
+
+ {isFreeTier && (
Date: Fri, 15 Nov 2024 05:58:23 +0900 Subject: [PATCH 55/86] docs(auth): update PKCE flow and Next.js example for password-based auth (#29971) * docs(auth): update PKCE flow and Next.js example for password-based auth Update the password-based authentication documentation to improve consistency and clarity: - Add email redirect information to PKCE flow, mirroring the Implicit flow section - Align the Next.js example in app/auth/confirm/route.ts with the official Supabase documentation for server-side auth in Next.js - Update email template to include 'next' parameter for better redirect handling - Improve error handling and redirect logic in the Next.js example These changes aim to provide a more comprehensive and consistent guide for implementing password-based authentication, especially for Next.js applications using the PKCE flow. Ref: https://supabase.com/docs/guides/auth/server-side/nextjs * fix(docs): format email confirmation link and update sign-up function example --- apps/docs/content/guides/auth/passwords.mdx | 28 +++++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/apps/docs/content/guides/auth/passwords.mdx b/apps/docs/content/guides/auth/passwords.mdx index 892c127e233..25ffbaea936 100644 --- a/apps/docs/content/guides/auth/passwords.mdx +++ b/apps/docs/content/guides/auth/passwords.mdx @@ -142,7 +142,7 @@ The PKCE flow allows for server-side authentication. Unlike the implicit flow, w ##### Step 1: Update signup confirmation email -Update your signup email template to send the token hash. See [Email Templates](/docs/guides/auth/auth-email-templates) for how to configure your email templates. +Update your signup email template to send the token hash. For detailed instructions on how to configure your email templates, including the use of variables like `{{ .SiteURL }}`, `{{ .TokenHash }}`, and `{{ .RedirectTo }}`, please refer to our [Email Templates](/docs/guides/auth/auth-email-templates) guide. Your signup email template should contain the following HTML: @@ -151,7 +151,8 @@ Your signup email template should contain the following HTML:

Follow this link to confirm your user:

- Confirm your email

@@ -170,18 +171,16 @@ Create a new file at `app/auth/confirm/route.ts` and populate with the following ```ts app/auth/confirm/route.ts import { type EmailOtpType } from '@supabase/supabase-js' -import { cookies } from 'next/headers' -import { NextRequest, NextResponse } from 'next/server' -// The client you created from the Server-Side Auth instructions +import { type NextRequest } from 'next/server' + import { createClient } from '@/utils/supabase/server' +import { redirect } from 'next/navigation' export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const token_hash = searchParams.get('token_hash') const type = searchParams.get('type') as EmailOtpType | null const next = searchParams.get('next') ?? '/' - const redirectTo = request.nextUrl.clone() - redirectTo.pathname = next if (token_hash && type) { const supabase = await createClient() @@ -191,13 +190,13 @@ export async function GET(request: NextRequest) { token_hash, }) if (!error) { - return NextResponse.redirect(redirectTo) + // redirect user to specified redirect URL or root of app + redirect(next) } } - // return the user to an error page with some instructions - redirectTo.pathname = '/auth/auth-code-error' - return NextResponse.redirect(redirectTo) + // redirect the user to an error page with some instructions + redirect('/auth/auth-code-error') } ``` @@ -381,11 +380,18 @@ app.get("/auth/confirm", async function (req, res) { To sign up the user, call [signUp()](/docs/reference/javascript/auth-signup) with their email address and password: +You can optionally specify a URL to redirect to after the user clicks the confirmation link. This URL must be configured as a [Redirect URL](/docs/guides/auth/redirect-urls), which you can do in the [dashboard](/dashboard/project/_/auth/url-configuration) for hosted projects, or in the [configuration file](/docs/guides/cli/config#auth.additional_redirect_urls) for self-hosted projects. + +If you don't specify a redirect URL, the user is automatically redirected to your site URL. This defaults to `localhost:3000`, but you can also configure this. + ```js async function signUpNewUser() { const { data, error } = await supabase.auth.signUp({ email: 'example@email.com', password: 'example-password', + options: { + emailRedirectTo: 'https://example.com/welcome', + }, }) } ``` From 59ecdeb1b9999c66261969a0967b09ff479422a3 Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Fri, 15 Nov 2024 15:48:11 +0800 Subject: [PATCH 56/86] chore: update compute redirect with flag (#30408) * Update ComputeInstanceSidePanel.tsx * add redirect on new page * Update ComputeInstanceSidePanel.tsx * Fix order * Small refactor --------- Co-authored-by: Joshen Lim --- .../DiskManagement/DiskManagementForm.tsx | 28 ++++++++++----- .../Addons/ComputeInstanceSidePanel.tsx | 34 ++++++++++--------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx index b9f2d95490f..ab1c663830f 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagementForm.tsx @@ -4,6 +4,7 @@ import { useQueryClient } from '@tanstack/react-query' import { AnimatePresence, motion } from 'framer-motion' import { ChevronRight } from 'lucide-react' import Link from 'next/link' +import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' @@ -24,8 +25,10 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { AddonVariantId } from 'data/subscriptions/types' +import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { useFlag } from 'hooks/ui/useFlag' import { GB, PROJECT_STATUS } from 'lib/constants' import { Button, @@ -56,7 +59,6 @@ import { } from './ui/DiskManagement.constants' import { NoticeBar } from './ui/NoticeBar' import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection' -import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query' export function DiskManagementForm() { const { @@ -67,6 +69,8 @@ export function DiskManagementForm() { const org = useSelectedOrganization() const { ref: projectRef } = useParams() const queryClient = useQueryClient() + const router = useRouter() + const diskAndComputeForm = useFlag('diskAndComputeForm') const { data: resourceWarnings } = useResourceWarningsQuery() const projectResourceWarnings = (resourceWarnings ?? [])?.find( @@ -186,14 +190,6 @@ export function DiskManagementForm() { isFlyArchitecture const disableComputeInputs = isPlanUpgradeRequired - - useEffect(() => { - // Initialize field values properly when data has been loaded, preserving any user changes - if (isSuccess) { - form.reset(defaultValues, {}) - } - }, [isSuccess]) - const isDirty = !!Object.keys(form.formState.dirtyFields).length const isProjectResizing = project?.status === PROJECT_STATUS.RESIZING const isProjectRequestingDiskChanges = isRequestingChanges && !isProjectResizing @@ -262,6 +258,20 @@ export function DiskManagementForm() { } } + useEffect(() => { + // Initialize field values properly when data has been loaded, preserving any user changes + if (isSuccess) { + form.reset(defaultValues, {}) + } + }, [isSuccess]) + + // Redirect logic incase disk and compute feature is not live yet + useEffect(() => { + if (diskAndComputeForm !== undefined && !diskAndComputeForm && projectRef) { + router.push(`/project/${projectRef}/settings/addons?panel=computeInstance`) + } + }, [diskAndComputeForm, projectRef, router]) + return (
diff --git a/apps/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx b/apps/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx index f3c8beb61d3..789ee83d5b9 100644 --- a/apps/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/ComputeInstanceSidePanel.tsx @@ -1,11 +1,11 @@ import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' +import { ExternalLink, Info } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' -import { ExternalLink, Info } from 'lucide-react' import { useParams } from 'common' import { @@ -32,7 +32,6 @@ import { AlertTitle_Shadcn_, Badge, Button, - CriticalIcon, Modal, Radio, SidePanel, @@ -45,17 +44,14 @@ const ComputeInstanceSidePanel = () => { const { ref: projectRef } = useParams() const { project: selectedProject } = useProjectContext() const organization = useSelectedOrganization() - const computeSizeChangesDisabled = useFlag('disableComputeSizeChanges') + const diskAndComputeForm = useFlag('diskAndComputeForm') const [showConfirmationModal, setShowConfirmationModal] = useState(false) - const canUpdateCompute = useCheckPermissions( PermissionAction.BILLING_WRITE, 'stripe.subscriptions' ) - const isProjectActive = useIsProjectActive() - const { panel, setPanel, closePanel } = useAddonsPagePanel() const visible = panel === 'computeInstance' @@ -176,16 +172,6 @@ const ComputeInstanceSidePanel = () => { hasReadReplicas && (isDowngradingToBelowSmall || hasMoreThanTwoReplicasForXLAndAbove) - useEffect(() => { - if (visible) { - if (subscriptionCompute !== undefined) { - setSelectedOption(subscriptionCompute.variant.identifier) - } else { - setSelectedOption(defaultInstanceSize) - } - } - }, [visible, isLoading]) - const onConfirmUpdateComputeInstance = async () => { if (!projectRef) return console.error('Project ref is required') if (!projectId) return console.error('Project ID is required') @@ -210,6 +196,22 @@ const ComputeInstanceSidePanel = () => { } } + useEffect(() => { + if (visible) { + if (subscriptionCompute !== undefined) { + setSelectedOption(subscriptionCompute.variant.identifier) + } else { + setSelectedOption(defaultInstanceSize) + } + } + }, [visible, isLoading]) + + useEffect(() => { + if (visible && diskAndComputeForm) { + router.push(`/project/${projectRef}/settings/compute-and-disk`) + } + }, [visible, diskAndComputeForm, router, projectRef]) + return ( <> Date: Fri, 15 Nov 2024 17:01:20 +0800 Subject: [PATCH 57/86] fix: copy for downgrading Fly partner org (#30464) --- .../Subscription/ExitSurveyModal.tsx | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx index 080ad8e8767..f62c0a34626 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx @@ -54,9 +54,6 @@ const ExitSurveyModal = ({ visible, subscription, projects, onClose }: ExitSurve const hasProjectsWithComputeDowngrade = projectsWithComputeDowngrade.length > 0 - const willPlanDowngradeHappenImmediately = - subscription?.billing_via_partner === false || subscription?.billing_partner !== 'fly' - function reducer(state: any, action: any) { if (includes(state, action.target.value)) { return without(state, action.target.value) @@ -105,11 +102,9 @@ const ExitSurveyModal = ({ visible, subscription, projects, onClose }: ExitSurve // [Joshen] In this case we don't raise any errors if the exit survey fails to send since it shouldn't block the user } finally { toast.success( - willPlanDowngradeHappenImmediately - ? hasProjectsWithComputeDowngrade - ? 'Successfully downgraded organization to the Free Plan. Your projects are currently restarting to update their compute instances.' - : 'Successfully downgraded organization to the Free Plan' - : 'Your organization is scheduled for the downgrade at the end of your current billing cycle', + hasProjectsWithComputeDowngrade + ? 'Successfully downgraded organization to the Free Plan. Your projects are currently restarting to update their compute instances.' + : 'Successfully downgraded organization to the Free Plan', { duration: hasProjectsWithComputeDowngrade ? 8000 : 4000 } ) onClose(true) @@ -199,7 +194,7 @@ const ExitSurveyModal = ({ visible, subscription, projects, onClose }: ExitSurve This is due to changes in compute instances from the downgrade. Affected projects include {projectsWithComputeDowngrade.map((project) => project.name).join(', ')}. @@ -209,11 +204,11 @@ const ExitSurveyModal = ({ visible, subscription, projects, onClose }: ExitSurve
- {willPlanDowngradeHappenImmediately && ( -

- The unused amount for the remaining of your billing cycle will be refunded as credits -

- )} +

+ The unused amount for the remaining time of your billing cycle will be refunded as + credits +

+
- - {!canUpdateRoles && ( - - -
- You need additional permissions to add a new role -
-
- )} - + } + onClick={() => setIsCreatingRole(true)} + tooltip={{ + content: { + side: 'bottom', + text: !canUpdateRoles + ? 'You need additional permissions to add a new role' + : undefined, + }, + }} + > + Add role +
- {supabaseRoles.length > 0 && ( -
-
-

Roles managed by Supabase

- Protected -
- - {isLoading - ? Array.from({ length: 5 }).map((_, i) => ) - : supabaseRoles.map((role) => ( - - ))} +
+
+

Roles managed by Supabase

+ Protected
- )} - {otherRoles.length > 0 && ( -
-
-

Other database roles

-
+ {isLoading + ? Array.from({ length: 5 }).map((_, i) => ) + : supabaseRoles.map((role) => ( + + ))} +
- {isLoading - ? Array.from({ length: 3 }).map((_, i) => ) - : otherRoles.map((role) => ( - - ))} +
+
+

Other database roles

- )} + + {isLoading + ? Array.from({ length: 3 }).map((_, i) => ) + : otherRoles.map((role) => ( + + ))} +
{filterString.length > 0 && filteredRoles.length === 0 && ( diff --git a/apps/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx b/apps/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx index 6cab2ab1fe7..af42e06dbd5 100644 --- a/apps/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx +++ b/apps/studio/components/interfaces/Database/Wrappers/EditWrapper.tsx @@ -49,7 +49,7 @@ const EditWrapper = () => { connectionString: project?.connectionString, }) - const wrappers = data?.result ?? [] + const wrappers = data ?? [] const foundWrapper = wrappers.find((w) => Number(w.id) === Number(id)) // this call to useImmutableValue should be removed if the redirect after update is also removed const wrapper = useImmutableValue(foundWrapper) diff --git a/apps/studio/components/interfaces/Database/Wrappers/Wrappers.tsx b/apps/studio/components/interfaces/Database/Wrappers/Wrappers.tsx index 83b04d5f72f..76c92a2eb58 100644 --- a/apps/studio/components/interfaces/Database/Wrappers/Wrappers.tsx +++ b/apps/studio/components/interfaces/Database/Wrappers/Wrappers.tsx @@ -19,7 +19,7 @@ const Wrappers = ({ isEnabled }: { isEnabled: boolean }) => { const [open, setOpen] = useState('') const [selectedWrapperToDelete, setSelectedWrapperToDelete] = useState() - const wrappers = data?.result ?? [] + const wrappers = data ?? [] return ( <> diff --git a/apps/studio/components/interfaces/DiskManagement/ui/DiskSpaceBar.tsx b/apps/studio/components/interfaces/DiskManagement/ui/DiskSpaceBar.tsx index b634f3d35df..26c007ef661 100644 --- a/apps/studio/components/interfaces/DiskManagement/ui/DiskSpaceBar.tsx +++ b/apps/studio/components/interfaces/DiskManagement/ui/DiskSpaceBar.tsx @@ -6,6 +6,7 @@ import { UseFormReturn } from 'react-hook-form' import { useParams } from 'common' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useRemainingDurationForDiskAttributeUpdate } from 'data/config/disk-attributes-query' import { useDiskUtilizationQuery } from 'data/config/disk-utilization-query' import { useDatabaseSizeQuery } from 'data/database/database-size-query' import { GB } from 'lib/constants' @@ -19,7 +20,6 @@ import { } from 'ui' import { DiskStorageSchemaType } from '../DiskManagement.schema' import { AUTOSCALING_THRESHOLD } from './DiskManagement.constants' -import { useRemainingDurationForDiskAttributeUpdate } from 'data/config/disk-attributes-query' interface DiskSpaceBarProps { form: UseFormReturn @@ -56,7 +56,7 @@ export default function DiskSpaceBar({ form }: DiskSpaceBarProps) { }) const { remainingDuration } = useRemainingDurationForDiskAttributeUpdate({ projectRef: ref }) - const databaseSizeBytes = data?.result[0].db_size ?? 0 + const databaseSizeBytes = data ?? 0 return (
diff --git a/apps/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx b/apps/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx index 0f7ff4d7fbd..0763d3e39fc 100644 --- a/apps/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx @@ -62,7 +62,7 @@ const DiskSizeConfiguration = ({ disabled = false }: DiskSizeConfigurationProps) projectRef: project?.ref, connectionString: project?.connectionString, }) - const databaseSizeBytesUsed = data?.result[0].db_size ?? 0 + const databaseSizeBytesUsed = data ?? 0 return (
diff --git a/apps/studio/components/interfaces/Settings/Vault/Secrets/SecretRow.tsx b/apps/studio/components/interfaces/Settings/Vault/Secrets/SecretRow.tsx index 54a7340795c..75687538d5b 100644 --- a/apps/studio/components/interfaces/Settings/Vault/Secrets/SecretRow.tsx +++ b/apps/studio/components/interfaces/Settings/Vault/Secrets/SecretRow.tsx @@ -103,9 +103,7 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>

- +
- - -
) diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx index 3e76ab0df6a..1397050ade5 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/index.tsx @@ -88,6 +88,7 @@ const PolicyTableRow = ({ {policies?.map((policy) => ( Date: Tue, 19 Nov 2024 22:41:24 +0800 Subject: [PATCH 73/86] chore: update auth url config for preview (#30544) --- .github/workflows/config-preview.yaml | 63 ------------------------ .github/workflows/config-production.yaml | 28 ----------- supabase/config.toml | 10 ++-- supabase/remotes/.gitignore | 34 ------------- supabase/remotes/preview.tf | 41 --------------- supabase/remotes/production.tf | 15 ------ supabase/remotes/provider.tf | 10 ---- 7 files changed, 6 insertions(+), 195 deletions(-) delete mode 100644 .github/workflows/config-preview.yaml delete mode 100644 .github/workflows/config-production.yaml delete mode 100644 supabase/remotes/.gitignore delete mode 100644 supabase/remotes/preview.tf delete mode 100644 supabase/remotes/production.tf delete mode 100644 supabase/remotes/provider.tf diff --git a/.github/workflows/config-preview.yaml b/.github/workflows/config-preview.yaml deleted file mode 100644 index cca3c2d7de3..00000000000 --- a/.github/workflows/config-preview.yaml +++ /dev/null @@ -1,63 +0,0 @@ -name: Config (Preview) - -on: - pull_request: - types: - - opened - - reopened - - synchronize - branches: - - master - paths: - - "supabase/**" - workflow_dispatch: - -jobs: - wait: - runs-on: ubuntu-latest - outputs: - status: ${{ steps.check.outputs.conclusion }} - steps: - - uses: fountainhead/action-wait-for-check@v1.2.0 - id: check - with: - checkName: Supabase - ref: ${{ github.event.pull_request.head.sha }} - token: ${{ secrets.GITHUB_TOKEN }} - - apply: - needs: - - wait - if: ${{ needs.wait.outputs.status == 'success' }} - runs-on: ubuntu-latest - env: - SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} - TF_VAR_linked_project: xguihxuzqibwxjnimxev - TF_VAR_git_branch: ${{ github.head_ref }} - TF_CLI_ARGS_apply: -target=supabase_settings.preview - defaults: - run: - working-directory: supabase/remotes - outputs: - db_user: ${{ steps.branch.outputs.user }} - db_pass: ${{ steps.branch.outputs.password }} - db_host: ${{ steps.branch.outputs.host }} - db_port: ${{ steps.branch.outputs.port }} - jwt_secret: ${{ steps.branch.outputs.jwt_secret }} - ref: ${{ steps.branch.outputs.id }} - status: ${{ steps.branch.outputs.status }} - version: ${{ steps.branch.outputs.version }} - - steps: - - uses: actions/checkout@v4 - - uses: hashicorp/setup-terraform@v3 - with: - terraform_wrapper: false - - - run: terraform init - - run: terraform apply -auto-approve -no-color - - id: branch - run: | - terraform output -json branch_database \ - | jq -r "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]" \ - >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/config-production.yaml b/.github/workflows/config-production.yaml deleted file mode 100644 index b7f3dd3d63c..00000000000 --- a/.github/workflows/config-production.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Config (Production) - -on: - push: - branches: - - master - paths: - - "supabase/remotes/**" - workflow_dispatch: - -jobs: - apply: - runs-on: ubuntu-latest - env: - SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} - TF_VAR_linked_project: xguihxuzqibwxjnimxev - defaults: - run: - working-directory: supabase/remotes - - steps: - - uses: actions/checkout@v4 - - uses: hashicorp/setup-terraform@v3 - with: - terraform_wrapper: false - - - run: terraform init - - run: terraform apply -auto-approve -no-color diff --git a/supabase/config.toml b/supabase/config.toml index 961f4351a25..12986999396 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -30,8 +30,6 @@ port = 54323 [inbucket] # Port to use for the email testing server web interface. port = 54324 -smtp_port = 54325 -pop3_port = 54326 [storage] # The maximum file size allowed (e.g. "5MB", "500KB"). @@ -42,7 +40,11 @@ file_size_limit = "50MiB" # in emails. site_url = "http://localhost:3000" # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. -additional_redirect_urls = ["https://localhost:3000"] +additional_redirect_urls = [ + "https://*-supabase.vercel.app/*/*", + "https://supabase.com/*/*", + "http://localhost:3000/*/*", +] # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one # week). jwt_expiry = 3600 @@ -61,7 +63,7 @@ enable_confirmations = false # Uncomment the following to use gh oAuth app locally with your own test app # to use env vars locally, run > `source ./supabase/.env && supabase [command...]` [auth.external.github] -enabled = true +enabled = false client_id = "env(GITHUB_CLIENT_ID)" secret = "env(GITHUB_SECRET)" # Overrides the default auth redirectUrl. diff --git a/supabase/remotes/.gitignore b/supabase/remotes/.gitignore deleted file mode 100644 index 9b8a46e692b..00000000000 --- a/supabase/remotes/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# Local .terraform directories -**/.terraform/* - -# .tfstate files -*.tfstate -*.tfstate.* - -# Crash log files -crash.log -crash.*.log - -# Exclude all .tfvars files, which are likely to contain sensitive data, such as -# password, private keys, and other secrets. These should not be part of version -# control as they are data points which are potentially sensitive and subject -# to change depending on the environment. -*.tfvars -*.tfvars.json - -# Ignore override files as they are usually used to override resources locally and so -# are not checked in -override.tf -override.tf.json -*_override.tf -*_override.tf.json - -# Include override files you do wish to add to version control using negated pattern -# !example_override.tf - -# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan -# example: *tfplan* - -# Ignore CLI configuration files -.terraformrc -terraform.rc diff --git a/supabase/remotes/preview.tf b/supabase/remotes/preview.tf deleted file mode 100644 index 663af43d97e..00000000000 --- a/supabase/remotes/preview.tf +++ /dev/null @@ -1,41 +0,0 @@ -variable "git_branch" { - type = string - default = null -} - -# Fetch all branches of a linked project -data "supabase_branch" "all" { - parent_project_ref = var.linked_project -} - -# Import an existing branch database -import { - for_each = { - for b in data.supabase_branch.all.branches : - b.git_branch => b - if b.git_branch == var.git_branch - } - to = supabase_branch.imported[0] - id = each.value.id -} - -resource "supabase_branch" "imported" { - count = length(var.git_branch[*]) - parent_project_ref = var.linked_project - git_branch = var.git_branch -} - -# Override auth settings for the current branch -resource "supabase_settings" "preview" { - count = length(var.git_branch[*]) - project_ref = supabase_branch.imported[0].database.id - - auth = jsonencode({ - site_url = "http://localhost:3001" - }) -} - -output "branch_database" { - value = one(supabase_branch.imported[*].database) - sensitive = true -} diff --git a/supabase/remotes/production.tf b/supabase/remotes/production.tf deleted file mode 100644 index 92a1b31140b..00000000000 --- a/supabase/remotes/production.tf +++ /dev/null @@ -1,15 +0,0 @@ -# Define a linked project variable as user input -variable "linked_project" { - type = string -} - -# Configure api settings for the linked project -resource "supabase_settings" "production" { - project_ref = var.linked_project - - api = jsonencode({ - db_schema = "public, storage, graphql_public" - db_extra_search_path = "public, extensions" - max_rows = 1000 - }) -} diff --git a/supabase/remotes/provider.tf b/supabase/remotes/provider.tf deleted file mode 100644 index 6fba014c65c..00000000000 --- a/supabase/remotes/provider.tf +++ /dev/null @@ -1,10 +0,0 @@ -terraform { - required_providers { - supabase = { - source = "supabase/supabase" - version = "~> 1.0" - } - } -} - -provider "supabase" {} From c931e3001aebbab616956a2c22287dee8abb69fc Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 19 Nov 2024 12:03:29 -0330 Subject: [PATCH 74/86] Chore/realtime policies (#30552) Allow editing realtime messages rls policies --- .../interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx index 9e67d6764e9..5f75945624d 100644 --- a/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/PolicyTableRow/PolicyRow.tsx @@ -33,7 +33,7 @@ interface PolicyRowProps { const PolicyRow = ({ policy, - isLocked, + isLocked: isLockedDefault, onSelectEditPolicy = noop, onSelectDeletePolicy = noop, }: PolicyRowProps) => { @@ -44,6 +44,10 @@ const PolicyRow = ({ const { project } = useProjectContext() const { data: authConfig } = useAuthConfigQuery({ projectRef: project?.ref }) + // override islocked for Realtime messages table + const isLocked = + policy.schema === 'realtime' && policy.table === 'messages' ? false : isLockedDefault + // TODO(km): Simple check for roles that allow authenticated access. // In the future, we'll use splinter to return proper warnings for policies that allow anonymous user access. const appliesToAnonymousUsers = From 6e456911c76ec2dd265af9acb36ea8816cabd5e2 Mon Sep 17 00:00:00 2001 From: Jason Chu <54270747+major-winter@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:56:13 +0700 Subject: [PATCH 75/86] Fix: Dynamic texts for CHECK constraint placeholder (#29230) * Create dynamic texts for CHECK constraint placeholder relative to the selected option of Postgres Data types * modify text to use column name instead of hard-coded string * prettier and small updates * only update placeholder when format or name changes * remove extra whitespace --------- Co-authored-by: Jason Co-authored-by: Alaister Young Co-authored-by: Alaister Young --- .../ColumnEditor/ColumnEditor.tsx | 13 ++++- .../ColumnEditor/ColumnEditor.utils.ts | 48 ++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx index 3d717734a9f..bd5264360ea 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx @@ -35,6 +35,7 @@ import { generateColumnFieldFromPostgresColumn, generateCreateColumnPayload, generateUpdateColumnPayload, + getPlaceholderText, validateFields, } from './ColumnEditor.utils' import ColumnForeignKey from './ColumnForeignKey' @@ -74,6 +75,9 @@ const ColumnEditor = ({ const [errors, setErrors] = useState>({}) const [columnFields, setColumnFields] = useState() const [fkRelations, setFkRelations] = useState([]) + const [placeholder, setPlaceholder] = useState( + getPlaceholderText(columnFields?.format, columnFields?.name) + ) const { data: types } = useEnumeratedTypesQuery({ projectRef: project?.ref, @@ -129,6 +133,7 @@ const ColumnEditor = ({ } const changedName = 'name' in changes && changes.name !== columnFields.name + const changedFormat = 'format' in changes && changes.format !== columnFields.format if ( changedName && @@ -144,6 +149,12 @@ const ColumnEditor = ({ ) } + if (changedName || changedFormat) { + setPlaceholder( + getPlaceholderText(changes.format || columnFields.format, changes.name || columnFields.name) + ) + } + const updatedColumnFields: ColumnField = { ...columnFields, ...changes } setColumnFields(updatedColumnFields) updateEditorDirty() @@ -342,7 +353,7 @@ const ColumnEditor = ({ onUpdateField({ check: event.target.value })} diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts index 60319888c7a..8c71db7e8f3 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts @@ -4,7 +4,6 @@ import type { Dictionary } from 'types' import { FOREIGN_KEY_CASCADE_ACTION } from 'data/database/database-query-constants' import type { ForeignKeyConstraint } from 'data/database/foreign-key-constraints-query' -import type { PartitionedTable, Table } from 'data/table-editor/table-editor-types' import { uuidv4 } from 'lib/helpers' import { ColumnField, @@ -250,3 +249,50 @@ export const getForeignKeyCascadeAction = (action?: string) => { return undefined } } + +export const getPlaceholderText = (format?: string, columnFieldName?: string) => { + const columnName = columnFieldName || 'column_name' + + switch (format) { + case 'int2': + case 'int4': + case 'int8': + case 'numeric': + return `"${columnName}" > 0` + + case 'float4': + case 'float8': + return `"${columnName}" > 0.0` + + case 'text': + case 'varchar': + return `length("${columnName}") <= 50` + + case 'json': + case 'jsonb': + return `jsonb_typeof("${columnName}"->'active') = 'boolean'` + + case 'bool': + return `"${columnName}" in (true, false)` + + case 'date': + return `"${columnName}" > '2024-01-01'` + + case 'time': + return `"${columnName}" between '09:00:00' and '12:00:00'` + + case 'timetz': + return `"${columnName}" at time zone 'UTC' between '09:00:00+00' and '17:00:00+00'` + + case 'uuid': + return `"${columnName}" '00000000-0000-0000-0000-000000000000'` + + case 'timestamp': + return `"${columnName}" > '2023-01-01 00:00' and "${columnName}" < '2025-01-01 00:00'` + case 'timestamptz': + return `"${columnName}" > '2023-01-01 00:00:00+00' and "${columnName}" < '2025-01-01 00:00:00+00'` + + default: + return `length("${columnName}") < 500` + } +} From 315f9634862dedd94fafc1c5672d0a38a5cd9f0f Mon Sep 17 00:00:00 2001 From: Chris Gwilliams <517923+encima@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:10:36 +0200 Subject: [PATCH 76/86] Add WAL disk usage to Disk Usage docs (#28248) * add WAL info to db size docs * Apply suggestions from code review Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> --------- Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> --- apps/docs/content/guides/platform/database-size.mdx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/guides/platform/database-size.mdx b/apps/docs/content/guides/platform/database-size.mdx index 3ddc6cb224c..bb9174b5f9f 100644 --- a/apps/docs/content/guides/platform/database-size.mdx +++ b/apps/docs/content/guides/platform/database-size.mdx @@ -16,7 +16,7 @@ This SQL query will show the size of all databases in your Postgres cluster: ```sql select - sum(pg_database_size(pg_database.datname)) / (1024 * 1024) as db_size_mb + pg_size_pretty(sum(pg_database_size(pg_database.datname))) from pg_database; ``` @@ -30,6 +30,16 @@ Depending on your billing plan, your database can go into read-only mode which c +### Disk space usage + +Your database size is part of the disk usage for your Supabase project, there are many components to Postgres that consume additional disk space. One of the primary components, is the [Write Ahead Log (WAL)](https://www.postgresql.org/docs/current/wal-intro.html). Postgres will store database changes in log files that are cleared away after they are applied to the database. These same files are also used by [Read Replicas](/docs/guides/platform/read-replicas) or other replication methods. + +If you would like to determine the size of the WAL files stored on disk, Postgres provides `pg_ls_waldir` as a helper function; the following query can be run: + +```sql +select pg_size_pretty(sum(size)) as wal_size from pg_ls_waldir(); +``` + ### Vacuum operations Postgres does not immediately reclaim the physical space used by dead tuples (i.e., deleted rows) in the DB. They are marked as "removed" until a [vacuum operation](https://www.postgresql.org/docs/current/routine-vacuuming.html) is executed. As a result, deleting data from your database may not immediately reduce the reported disk usage. You can use the [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started) `inspect db bloat` command to view all dead tuples in your database. Alternatively, you can run the [query](https://github.com/supabase/cli/blob/c9cce58025fded16b4c332747f819a44f45c3b83/internal/inspect/bloat/bloat.go#L17) found in the CLI's GitHub repo in the [SQL Editor](https://supabase.com/dashboard/project/_/sql/) From a4961911228ba04c832ef4ca9b858d1c42e0c01a Mon Sep 17 00:00:00 2001 From: Chris Gwilliams <517923+encima@users.noreply.github.com> Date: Wed, 20 Nov 2024 09:12:12 +0200 Subject: [PATCH 77/86] Add warning about custom role passwords omitted in backups to relevant docs pages (#28760) add note on custom roles for backups --- apps/docs/content/guides/platform/upgrading.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index aef800a9d67..1bdab44de90 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -22,6 +22,11 @@ This upgrade method is currently in Beta. + + For security purposes, passwords for custom roles are not backed up and, following a restore, they + would need to be reset. See [here](/docs/guides/platform/backups#daily-backups) for more details + + pg_upgrade performs an in-place upgrade on your database. For projects larger than 1GB, pg_upgrade is generally faster than a pause+restore cycle, and the speed advantage grows with the size of the database. 1. Plan for an appropriate downtime window, and ensure you have reviewed the [caveats](#caveats) section of this document before executing the upgrade. From 821a6afd233f076512e9a348c4228174ffd889ac Mon Sep 17 00:00:00 2001 From: renee <50965960+renee-schultz-wu@users.noreply.github.com> Date: Tue, 19 Nov 2024 23:25:47 -0800 Subject: [PATCH 78/86] update Chinese README (#30562) update Chinese docs to reflect Python and Swift as official clients Co-authored-by: renee <50965960+wurenee@users.noreply.github.com> --- i18n/README.zh-cn.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/i18n/README.zh-cn.md b/i18n/README.zh-cn.md index 92196948240..d88db745230 100644 --- a/i18n/README.zh-cn.md +++ b/i18n/README.zh-cn.md @@ -110,6 +110,24 @@ Supabase 是一个[托管平台](https://supabase.com/dashboard)。你可以注
+ + + + + + + + + + + + + + + + + + @@ -150,15 +168,6 @@ Supabase 是一个[托管平台](https://supabase.com/dashboard)。你可以注 - - - - - - - - - @@ -177,15 +186,6 @@ Supabase 是一个[托管平台](https://supabase.com/dashboard)。你可以注 - - - - - - - - - From aa4269f764a6d1763fbffed70889a9d2ff402a45 Mon Sep 17 00:00:00 2001 From: Thibaut Fatus Date: Wed, 20 Nov 2024 09:07:28 +0100 Subject: [PATCH 79/86] Fix PostGIS doc (#30520) postgis uses `"gis"` schema, thus running the command as it is in the docs leads to an error `type "geography" does not exist` prefixing using the schema `gis.geometry` fixes the issue --- apps/docs/content/guides/database/extensions/postgis.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/database/extensions/postgis.mdx b/apps/docs/content/guides/database/extensions/postgis.mdx index 9c470366db1..72b85b09aff 100644 --- a/apps/docs/content/guides/database/extensions/postgis.mdx +++ b/apps/docs/content/guides/database/extensions/postgis.mdx @@ -58,7 +58,7 @@ Let’s create our table. Each row represents a restaurant with its location sto create table if not exists public.restaurants ( id int generated by default as identity primary key, name text not null, - location geography(POINT) not null + location gis.geography(POINT) not null ); ``` From a579396dcd9ac204d26d655f445e32699505caaf Mon Sep 17 00:00:00 2001 From: Lara Raoof <47983973+Ge6ben@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:19:12 +0300 Subject: [PATCH 80/86] Update actions/checkout to use v4 (node 20) (#30502) --- apps/docs/content/guides/functions/examples/github-actions.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/functions/examples/github-actions.mdx b/apps/docs/content/guides/functions/examples/github-actions.mdx index 3298913f089..5b665d8f4c8 100644 --- a/apps/docs/content/guides/functions/examples/github-actions.mdx +++ b/apps/docs/content/guides/functions/examples/github-actions.mdx @@ -34,7 +34,7 @@ jobs: PROJECT_ID: YOUR_SUPABASE_PROJECT_ID steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: supabase/setup-cli@v1 with: From 8a311b9b2569c4534b1452a9c0c92639aa47f8c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:01:36 +0800 Subject: [PATCH 81/86] chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 (#30542) Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6. - [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md) - [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6) --- updated-dependencies: - dependency-name: cross-spawn dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9350354435e..75f96c23839 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13162,14 +13162,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@rollup/plugin-commonjs/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/@rollup/pluginutils": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", @@ -20332,8 +20324,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "license": "MIT", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -33956,9 +33949,10 @@ } }, "node_modules/npm-run-all/node_modules/cross-spawn": { - "version": "6.0.5", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, - "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", From b80a9bbb9e688e2c1cc86878e3b66b0736112115 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Wed, 20 Nov 2024 17:50:51 +0800 Subject: [PATCH 82/86] fix: document skip_nonce_check in expo guide (#30537) * fix: add section about skip_nonce_check * fix: update Google Auth docs --- apps/docs/content/guides/auth/social-login/auth-google.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/docs/content/guides/auth/social-login/auth-google.mdx b/apps/docs/content/guides/auth/social-login/auth-google.mdx index 260a32c341e..11b24c66efd 100644 --- a/apps/docs/content/guides/auth/social-login/auth-google.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-google.mdx @@ -439,6 +439,8 @@ Unlike the OAuth flow which requires the use of a web browser, the native Sign i When the user provides consent, Google issues an identity token (commonly abbreviated as ID token) that is then sent to your project's Supabase Auth server. When valid, a new user session is started by issuing an access and refresh token from Supabase Auth. +By default, Supabase Auth implements nonce validation during the authentication flow. This can be disabled in production under `Authentication > Providers > Google > Skip Nonce Check` in the Dashboard, or when developing locally by setting `auth.external..skip_nonce_check`. Only disable this if your client libraries cannot properly handle nonce verification. + When working with Expo, you can use the [`react-native-google-signin/google-signin` library](https://github.com/react-native-google-signin/google-signin#expo-installation) library to obtain an ID token that you can pass to supabase-js [`signInWithIdToken` method](/docs/reference/javascript/auth-signinwithidtoken). Follow the [Expo installation docs](https://react-native-google-signin.github.io/docs/setting-up/expo) for installation and configuration instructions. See the [supabase-js reference](/docs/reference/javascript/initializing?example=react-native-options-async-storage) for instructions on initializing the supabase-js client in React Native. From a2b3415236086d5b6c445755cfd2fafa34544ab5 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Wed, 20 Nov 2024 19:01:42 +0800 Subject: [PATCH 83/86] feat: display trigger condition (#30521) --- .../Database/Triggers/CreateTrigger.tsx | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx b/apps/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx index cf37b3147bc..eb96c6c1877 100644 --- a/apps/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx @@ -8,6 +8,7 @@ import { Badge, Button, Checkbox, Input, Listbox, SidePanel } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import FormEmptyBox from 'components/ui/FormBoxEmpty' import NoTableState from 'components/ui/States/NoTableState' import { @@ -19,9 +20,9 @@ import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/databas import { useTablesQuery } from 'data/tables/tables-query' import { BASE_PATH } from 'lib/constants' import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' +import { PauseCircle, PlayCircle, Terminal } from 'lucide-react' import type { Dictionary } from 'types' import ChooseFunctionForm from './ChooseFunctionForm' -import { PauseCircle, PlayCircle, Terminal } from 'lucide-react' class CreateTriggerFormState { id: number | undefined @@ -46,6 +47,8 @@ class CreateTriggerFormState { table: { value: string } // @ts-ignore tableId: { value: number; error?: string } + // @ts-ignore + condition: { value: string } constructor() { makeAutoObservable(this) @@ -80,6 +83,7 @@ class CreateTriggerFormState { this.schema = { value: trigger?.schema ?? '' } this.table = { value: trigger?.table ?? '' } this.tableId = { value: trigger?.table_id ?? '' } + this.condition = { value: trigger?.condition ?? '' } } update(state: Dictionary) { @@ -342,6 +346,7 @@ const CreateTrigger = ({ trigger, visible, setVisible }: CreateTriggerProps) =>
+ {_localState.formState.condition.value && }
) : ( <> @@ -492,6 +497,34 @@ const SelectEnabledMode = observer(({}) => { ) }) +const DisplayCondition = observer(() => { + const _localState = useContext(CreateTriggerContext) + + return ( +
+
+ +
+
+ +
+ This condition must be met for the trigger to fire. +
+ + To update the condition, you must drop and recreate this trigger. + +
+
+
+ ) +}) + const SelectOrientation = observer(({}) => { const _localState = useContext(CreateTriggerContext) return ( From 72e8c84bf9e554881f9738d90cc8f4bf0ad61aa5 Mon Sep 17 00:00:00 2001 From: Monica Khoury <99693443+monicakh@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:14:20 +0100 Subject: [PATCH 84/86] fix: LinkedIn (OIDC) & Slack (OIDC) providers filter under Authentication > Users (#30474) * fix: linkedin & slack providers filter * Fix typecheck --- .../components/interfaces/Auth/Users/Users.constants.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Auth/Users/Users.constants.ts b/apps/studio/components/interfaces/Auth/Users/Users.constants.ts index c66df1abdba..97b20ae76c6 100644 --- a/apps/studio/components/interfaces/Auth/Users/Users.constants.ts +++ b/apps/studio/components/interfaces/Auth/Users/Users.constants.ts @@ -3,7 +3,12 @@ import { PROVIDER_PHONE, PROVIDERS_SCHEMAS } from '../AuthProvidersFormValidatio export const PROVIDER_FILTER_OPTIONS = PROVIDERS_SCHEMAS.map((provider) => ({ name: provider.title, - value: provider.title.toLowerCase(), + value: + provider.title === 'Slack (OIDC)' + ? 'slack_oidc' + : provider.title === 'LinkedIn (OIDC)' + ? 'linkedin_oidc' + : provider.title.toLowerCase(), icon: `${BASE_PATH}/img/icons/${provider.misc.iconKey}.svg`, iconClass: provider.title === 'GitHub' ? 'dark:invert' : '', })).concat( From 4fb9b584861706a7128e34e0efbbb15ae7632253 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Wed, 20 Nov 2024 08:18:22 -0330 Subject: [PATCH 85/86] Chore/cron ai nudges (#30527) * Adjust prompt, ui * Account for seconds properly * More minute adjustments * Account for seconds properly, again * Lint --------- Co-authored-by: Joshen Lim --- .../CronJobs/CreateCronJobSheet.tsx | 30 +++-- .../CronJobs/CronJobScheduleSection.tsx | 108 ++++++++++++------ .../CronJobs/CronJobs.utils.test.ts | 36 +++++- .../Integrations/CronJobs/CronJobs.utils.ts | 5 + .../Integrations/CronJobs/CronSyntaxChart.tsx | 15 ++- packages/ai-commands/src/sql/cron.ts | 35 +++--- 6 files changed, 159 insertions(+), 70 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx index 524b5915c96..c3130c51d6f 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CreateCronJobSheet.tsx @@ -17,7 +17,6 @@ import { Button, Form_Shadcn_, FormControl_Shadcn_, - FormDescription_Shadcn_, FormField_Shadcn_, FormLabel_Shadcn_, Input_Shadcn_, @@ -35,7 +34,13 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { CRONJOB_DEFINITIONS } from './CronJobs.constants' -import { buildCronQuery, buildHttpRequestCommand, parseCronJobCommand } from './CronJobs.utils' +import { + buildCronQuery, + buildHttpRequestCommand, + cronPattern, + secondsPattern, + parseCronJobCommand, +} from './CronJobs.utils' import { CronJobScheduleSection } from './CronJobScheduleSection' import { EdgeFunctionSection } from './EdgeFunctionSection' import { HTTPHeaderFieldsSection } from './HttpHeaderFieldsSection' @@ -44,8 +49,6 @@ import { HttpRequestSection } from './HttpRequestSection' import { SqlFunctionSection } from './SqlFunctionSection' import { SqlSnippetSection } from './SqlSnippetSection' import EnableExtensionModal from 'components/interfaces/Database/Extensions/EnableExtensionModal' -import { checkDomainOfScale } from 'recharts/types/util/ChartUtils' -import { InfoIcon } from 'lucide-react' export interface CreateCronJobSheetProps { selectedCronJob?: Pick @@ -94,13 +97,18 @@ const FormSchema = z.object({ .trim() .min(1) .refine((value) => { - try { - CronToString(value) - } catch { - return false + if (cronPattern.test(value)) { + try { + CronToString(value) + return true + } catch { + return false + } + } else if (secondsPattern.test(value)) { + return true } - return true - }, 'The schedule needs to be in a Cron format.'), + return false + }, 'The schedule needs to be in a valid Cron format or specify seconds like "x seconds".'), values: z.discriminatedUnion('type', [ edgeFunctionSchema, httpRequestSchema, @@ -135,7 +143,7 @@ export const CreateCronJobSheet = ({ resolver: zodResolver(FormSchema), defaultValues: { name: selectedCronJob?.jobname || '', - schedule: selectedCronJob?.schedule || '* * * * * *', + schedule: selectedCronJob?.schedule || '*/5 * * * *', values: cronJobValues, }, }) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx index 5adbd02dd10..f77e38953c5 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobScheduleSection.tsx @@ -26,17 +26,19 @@ import { import { Input } from 'ui-patterns/DataInputs/Input' import { CreateCronJobForm } from './CreateCronJobSheet' import CronSyntaxChart from './CronSyntaxChart' +import { secondsPattern } from './CronJobs.utils' interface CronJobScheduleSectionProps { form: UseFormReturn } -const presets = [ - { name: 'Every minute', expression: '* * * * * *' }, +const PRESETS = [ + { name: 'Every minute', expression: '* * * * *' }, { name: 'Every 5 minutes', expression: '*/5 * * * *' }, { name: 'Every first of the month, at 00:00', expression: '0 0 1 * *' }, - { name: 'Every Monday at midnight', expression: '0 0 * * 1' }, - { name: 'Every night at midnight', expression: '0 0 0 * * *' }, + { name: 'Every night at midnight', expression: '0 0 * * *' }, + { name: 'Every Monday at 2 AM', expression: '0 2 * * 1' }, + { name: 'Every 30 seconds', expression: '30 seconds' }, ] as const export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => { @@ -46,7 +48,7 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => const [presetValue, setPresetValue] = useState(initialValue) const [inputValue, setInputValue] = useState(initialValue) - const [debouncedValue] = useDebounce(inputValue, 500) + const [debouncedValue] = useDebounce(inputValue, 750) const [useNaturalLanguage, setUseNaturalLanguage] = useState(false) const [scheduleString, setScheduleString] = useState('') @@ -78,19 +80,31 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => }, [debouncedValue, useNaturalLanguage]) useEffect(() => { - if (!inputValue) return + if (!inputValue || inputValue.length < 5) return // set a min length before showing invalid message // update the cronstrue string when the input value changes try { setScheduleString(CronToString(inputValue)) form.setValue('schedule', inputValue) - } catch {} + } catch (error) { + console.error('Error converting cron expression to string:', error) + } }, [form, inputValue]) useEffect(() => { - if (!useNaturalLanguage) { - setPresetValue(schedule) + if (useNaturalLanguage) return + + setPresetValue(schedule) + + if (!schedule) { + setScheduleString('') + return + } + + try { setScheduleString(CronToString(schedule)) + } catch (error) { + console.error('Error converting cron expression to string:', error) } }, [schedule]) @@ -113,6 +127,11 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => value={inputValue} placeholder="E.g. every 5 minutes" className={cn(!useNaturalLanguage && 'hidden')} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault() + } + }} onChange={(e) => setInputValue(e.target.value)} /> ) : ( @@ -136,6 +155,7 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => setInputValue('') setPresetValue('') setScheduleString('') + form.setValue('schedule', '') }} />

Use natural language

@@ -143,7 +163,7 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) =>
    - {presets.map((preset) => ( + {PRESETS.map((preset) => (
) @@ -235,7 +247,7 @@ export const CronJobScheduleSection = ({ form }: CronJobScheduleSectionProps) => const CronSyntaxLoader = () => { return (
- {['*', '*', '*', '*', '*', '*'].map((char, i) => ( + {['*', '*', '*', '*', '*'].map((char, i) => ( {
) } + +const LoadingDots = () => { + return ( + + {[0, 1, 2].map((i) => ( + + . + + ))} + + ) +} diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts index e54f33da4e8..ad1dab0ead7 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.test.ts @@ -1,5 +1,5 @@ import { it } from 'vitest' -import { parseCronJobCommand } from './CronJobs.utils' +import { parseCronJobCommand, secondsPattern, cronPattern } from './CronJobs.utils' describe('parseCronJobCommand', () => { it('should return a default object when the command is null', () => { @@ -34,4 +34,38 @@ describe('parseCronJobCommand', () => { type: 'edge_function', }) }) + + // Array of test cases for secondsPattern + const secondsPatternTests = [ + { description: '10 seconds', command: '10 seconds' }, + { description: '30 seconds', command: '30 seconds' }, + ] + + // Run tests for secondsPattern + secondsPatternTests.forEach(({ description, command }) => { + it(`should match the regex for a secondsPattern with "${description}"`, () => { + expect(command).toMatch(secondsPattern) + }) + }) + + // Array of test cases for cronPattern + const cronPatternTests = [ + { description: 'every hour', command: '0 * * * *' }, + { description: 'every day at midnight', command: '0 0 * * *' }, + { description: 'every Monday at 9 AM', command: '0 9 * * 1' }, + { description: 'every 15th of the month at noon', command: '0 12 15 * *' }, + { description: 'every January 1st at 3 AM', command: '0 3 1 1 *' }, + { description: 'every 30 minutes', command: '30 * * * *' }, + { description: 'every weekday at 6 PM', command: '0 18 * * 1-5' }, + { description: 'every weekend at 10 AM', command: '0 10 * * 0,6' }, + { description: 'every quarter hour', command: '*/15 * * * *' }, + { description: 'twice daily at 8 AM and 8 PM', command: '0 8,20 * * *' }, + ] + + // Replace the single cronPattern test with forEach + cronPatternTests.forEach(({ description, command }) => { + it(`should match the regex for a cronPattern with "${description}"`, () => { + expect(command).toMatch(cronPattern) + }) + }) }) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts index f316468e5cf..af9f02fa528 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobs.utils.ts @@ -110,3 +110,8 @@ export const parseCronJobCommand = (originalCommand: string): CronJobType => { return DEFAULT_CRONJOB_COMMAND } + +// detect seconds like "10 seconds" or normal cron syntax like "*/5 * * * *" +export const secondsPattern = /^\d+\s+seconds$/ +export const cronPattern = + /^(\*|(\d+|\*\/\d+)|\d+\/\d+|\d+-\d+|\d+(,\d+)*)(\s+(\*|(\d+|\*\/\d+)|\d+\/\d+|\d+-\d+|\d+(,\d+)*)){4}$/ diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx index 6bd57df71d3..30d4c97ac8b 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx @@ -4,14 +4,13 @@ export default function CronSyntaxChart() {
         {/* prettier-ignore */}
         {`
-┌───────────── second (0 - 59)
-│  ┌───────────── minute (0 - 59)
-│  │  ┌───────────── hour (0 - 23)
-│  │  │  ┌───────────── day of month (1 - 31)
-│  │  │  │  ┌───────────── month (1 - 12)
-│  │  │  │  │  ┌───────────── day of week (0 - 7)
-│  │  │  │  │  │
-*  *  *  *  *  * `}
+ ┌───────────── minute (0 - 59)
+ │  ┌───────────── hour (0 - 23)
+ │  │  ┌───────────── day of month (1 - 31)
+ │  │  │  ┌───────────── month (1 - 12)
+ │  │  │  │  ┌───────────── day of week (0 - 7)
+ │  │  │  │  │
+ *  *  *  *  * `}
       
) diff --git a/packages/ai-commands/src/sql/cron.ts b/packages/ai-commands/src/sql/cron.ts index c9f0ce5fdb1..27df01ec0fd 100644 --- a/packages/ai-commands/src/sql/cron.ts +++ b/packages/ai-commands/src/sql/cron.ts @@ -12,28 +12,37 @@ export async function generateCron(openai: OpenAI, prompt: string) { You are a cron syntax expert. Your purpose is to convert natural language time descriptions into valid cron expressions for pg_cron. Rules for responses: - - Output cron expressions in the 6-field format supported by pg_cron (seconds minute hour day month weekday) - - The seconds field is optional - if not specified, it defaults to 0 - - Provide a brief explanation of what the cron expression does + - Output cron expressions in the 5-field format supported by pg_cron + - Do not provide any explanation of what the cron expression does - Format output as markdown with the cron expression in a code block - - If the request is unclear or impossible, ask for clarification - - If the request isn't related to cron scheduling, explain that you can only help with cron expressions + - Do not ask for clarification if you need it. Just output the cron expression. Example input: "Every Monday at 3am" Example output: This cron expression runs every Monday at 3:00:00 AM: \`\`\` - 0 0 3 * * 1 + 0 3 * * 1 \`\`\` - Common patterns to remember: - - * means "every" for that field - - */n means "every n intervals" - - n means "exactly at n" - - n,m means "at n and m" - - n-m means "every value from n to m" - Field order: seconds (0-59), minute (0-59), hour (0-23), day (1-31), month (1-12), weekday (0-6, Sunday=0) + This cron expression runs every minute: + \`\`\` + * * * * * + \`\`\` + + Additional examples: + - Every minute: \`* * * * *\` + - Every 5 minutes: \`*/5 * * * *\` + - Every first of the month, at 00:00: \`0 0 1 * *\` + - Every night at midnight: \`0 0 * * *\` + - Every Monday at 2am: \`0 2 * * 1\` + + Field order: + - minute (0-59) + - hour (0-23) + - day (1-31) + - month (1-12) + - weekday (0-6, Sunday=0) Here is the user's prompt: ${prompt} From acfbc8a5e5d0f814aab1eb982f8316935809aa00 Mon Sep 17 00:00:00 2001 From: Ozan Tekin Date: Wed, 20 Nov 2024 20:44:24 +0300 Subject: [PATCH 86/86] style(docs): remove unused account import from app file in react native (#30567) --- apps/docs/content/guides/auth/quickstarts/react-native.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/docs/content/guides/auth/quickstarts/react-native.mdx b/apps/docs/content/guides/auth/quickstarts/react-native.mdx index 4a0176049f3..220a8db979a 100644 --- a/apps/docs/content/guides/auth/quickstarts/react-native.mdx +++ b/apps/docs/content/guides/auth/quickstarts/react-native.mdx @@ -219,7 +219,6 @@ hideToc: true import { useState, useEffect } from 'react' import { supabase } from './lib/supabase' import Auth from './components/Auth' - import Account from './components/Account' import { View, Text } from 'react-native' import { Session } from '@supabase/supabase-js'
realtime-dart storage-dart functions-dart
Swiftsupabase-swiftpostgrest-swiftgotrue-swiftrealtime-swiftstorage-swiftfunctions-swift
Pythonsupabase-pypostgrest-pygotrue-pyrealtime-pystorage-pyfunctions-py
💚 社区 💚storage-kt functions-kt
Pythonsupabase-pypostgrest-pygotrue-pyrealtime-pystorage-pyfunctions-py
Ruby supabase-rb- -
Swiftsupabase-swiftpostgrest-swiftgotrue-swiftrealtime-swiftstorage-swiftfunctions-swift
Godot Engine (GDScript) supabase-gdscript