diff --git a/apps/docs/app/contributing/content.mdx b/apps/docs/app/contributing/content.mdx index c3632971a2a52..1bff38834c02a 100644 --- a/apps/docs/app/contributing/content.mdx +++ b/apps/docs/app/contributing/content.mdx @@ -354,11 +354,13 @@ Some guides and tutorials will require that users copy their Supabase project UR ```mdx - + + ``` - + + ### Step Hike diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx index 5ce453b67de41..97620f6186e1b 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx @@ -94,7 +94,7 @@ export function NavigationMenuGuideListWrapper({ key={id} type="single" value={firstLevelRoute} - className="transition-all duration-150 ease-out opacity-100 ml-0 delay-150" + className="transition-all duration-150 ease-out opacity-100 ml-0 delay-150 w-full" > {children} diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx index bf34f7541a4c5..9f9e2a691583e 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx @@ -1,6 +1,7 @@ import * as Accordion from '@radix-ui/react-accordion' import { usePathname } from 'next/navigation' import { useTheme } from 'next-themes' +import { ChevronDown } from 'lucide-react' import Image from 'next/legacy/image' import Link from 'next/link' import React, { useEffect, useRef } from 'react' @@ -33,6 +34,9 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any const activeItem = props.subItem.url === pathname const activeItemRef = useRef(null) + const isChildActive = + props.subItem.items && props.subItem.items.some((child: any) => child.url === pathname) + const LinkContainer = (props) => { const isExternal = props.url.startsWith('https://') @@ -67,7 +71,62 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any > )} - + {props.subItem.items && props.subItem.items.length > 0 ? ( + + + + + + {props.subItem.icon && ( + + )} + {props.subItem.name} + + + + + + {props.subItem.items + .filter((subItem) => subItem.enabled !== false) + .map((subSubItem) => { + return ( + + + {subSubItem.name} + + + ) + })} + + + + ) : ( - {props.subItem.icon && ( - - )} - {props.subItem.name} + + {props.subItem.icon && ( + + )} + {props.subItem.name} + - - {props.subItem.items && props.subItem.items.length > 0 && ( - - {props.subItem.items - .filter((subItem) => subItem.enabled !== false) - .map((subSubItem) => { - return ( - - - {subSubItem.name} - - - ) - })} - - )} - + )} > ) }) diff --git a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx index b34ed122426bb..d384746540b0e 100644 --- a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx +++ b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx @@ -6,7 +6,7 @@ import type { Project, Variable, } from '~/components/ProjectConfigVariables/ProjectConfigVariables.utils' -import type { ProjectApiData } from '~/lib/fetch/projectApi' +import type { ProjectKeys, ProjectSettings } from '~/lib/fetch/projectApi' import { Check, Copy } from 'lucide-react' import Link from 'next/link' @@ -34,7 +34,7 @@ import { useCopy } from '~/hooks/useCopy' import { useBranchesQuery } from '~/lib/fetch/branches' import { useOrganizationsQuery } from '~/lib/fetch/organizations' import { type SupavisorConfigData, useSupavisorConfigQuery } from '~/lib/fetch/pooler' -import { useProjectApiQuery } from '~/lib/fetch/projectApi' +import { useProjectSettingsQuery, useProjectKeysQuery } from '~/lib/fetch/projectApi' import { isProjectPaused, useProjectsQuery } from '~/lib/fetch/projects' import { retrieve, storeOrRemoveNull } from '~/lib/storage' import { useOnLogout } from '~/lib/userAuth' @@ -274,14 +274,25 @@ function VariableView({ variable, className }: { variable: Variable; className?: const hasBranches = selectedProject?.is_branch_enabled ?? false const ref = hasBranches ? selectedBranch?.project_ref : selectedProject?.ref - const needsApiQuery = variable === 'publishableKey' || variable === 'url' + const needsApiQuery = variable === 'publishable' || variable === 'anon' || variable === 'url' const needsSupavisorQuery = variable === 'sessionPooler' const { - data: apiData, - isPending: isApiPending, - isError: isApiError, - } = useProjectApiQuery( + data: apiSettingsData, + isPending: isApiSettingsPending, + isError: isApiSettingsError, + } = useProjectSettingsQuery( + { + projectRef: ref, + }, + { enabled: isLoggedIn && !!ref && !projectPaused && needsApiQuery } + ) + + const { + data: apiKeysData, + isPending: isApiKeysPending, + isError: isApiKeysError, + } = useProjectKeysQuery( { projectRef: ref, }, @@ -299,15 +310,6 @@ function VariableView({ variable, className }: { variable: Variable; className?: { enabled: isLoggedIn && !!ref && !projectPaused && needsSupavisorQuery } ) - function isInvalidApiData(apiData: ProjectApiData) { - switch (variable) { - case 'url': - return !apiData.app_config?.endpoint - case 'publishableKey': - return !apiData.service_api_keys?.some((key) => key.tags === 'anon') - } - } - function isInvalidSupavisorData(supavisorData: SupavisorConfigData) { return supavisorData.length === 0 } @@ -320,24 +322,29 @@ function VariableView({ variable, className }: { variable: Variable; className?: ? 'loggedIn.noSelectedProject' : projectPaused ? 'loggedIn.selectedProject.projectPaused' - : (needsApiQuery ? isApiPending : isSupavisorPending) + : (needsApiQuery ? isApiSettingsPending || isApiKeysPending : isSupavisorPending) ? 'loggedIn.selectedProject.dataPending' : ( needsApiQuery - ? isApiError || isInvalidApiData(apiData!) + ? isApiSettingsError || isApiKeysError : isSupavisorError || isInvalidSupavisorData(supavisorConfig!) ) ? 'loggedIn.selectedProject.dataError' : 'loggedIn.selectedProject.dataSuccess' let variableValue: string = '' + if (stateSummary === 'loggedIn.selectedProject.dataSuccess') { switch (variable) { case 'url': - variableValue = `https://${apiData?.app_config?.endpoint}` + variableValue = `https://${apiSettingsData?.app_config?.endpoint}` + break + case 'anon': + variableValue = + apiKeysData?.find((key) => key.type === 'legacy' && key.id === 'anon')?.api_key || '' break - case 'publishableKey': - variableValue = apiData?.service_api_keys?.find((key) => key.tags === 'anon')?.api_key || '' + case 'publishable': + variableValue = apiKeysData?.find((key) => key.type === 'publishable')?.api_key || '' break case 'sessionPooler': variableValue = supavisorConfig?.[0]?.connection_string || '' diff --git a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts index 2ed28261bedd4..cb06766dbad7b 100644 --- a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts +++ b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts @@ -6,7 +6,7 @@ export type Org = OrganizationsData[number] export type Project = ProjectsData[number] export type Branch = BranchesData[number] -export type Variable = 'url' | 'publishableKey' | 'sessionPooler' +export type Variable = 'url' | 'publishable' | 'anon' | 'sessionPooler' function removeDoubleQuotes(str: string) { return str.replaceAll('"', '') @@ -30,7 +30,8 @@ function unescapeDoubleQuotes(str: string) { export const prettyFormatVariable: Record = { url: 'Project URL', - publishableKey: 'Publishable key', + anon: 'Anon key', + publishable: 'Publishable key', sessionPooler: 'Connection string (pooler session mode)', } diff --git a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx index bbefe0d40c0c5..4440e53b0e8e7 100644 --- a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx +++ b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx @@ -39,10 +39,11 @@ pnpm add @supabase/ssr @supabase/supabase-js ## Set environment variables -In your environment variables file, set your Supabase URL and Supabase Anon Key: +In your environment variables file, set your Supabase URL and Key: - + + diff --git a/apps/docs/content/guides/auth/server-side/nextjs.mdx b/apps/docs/content/guides/auth/server-side/nextjs.mdx index ff02120c23259..7dfb218570adb 100644 --- a/apps/docs/content/guides/auth/server-side/nextjs.mdx +++ b/apps/docs/content/guides/auth/server-side/nextjs.mdx @@ -39,7 +39,8 @@ Create a `.env.local` file in your project root directory. Fill in your `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`: - + + @@ -560,7 +561,8 @@ Create a `.env.local` file in your project root directory. Fill in your `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`: - + + diff --git a/apps/docs/content/guides/auth/server-side/sveltekit.mdx b/apps/docs/content/guides/auth/server-side/sveltekit.mdx index b8de7de0425ee..e14fdf6aef94e 100644 --- a/apps/docs/content/guides/auth/server-side/sveltekit.mdx +++ b/apps/docs/content/guides/auth/server-side/sveltekit.mdx @@ -34,7 +34,8 @@ Create a `.env.local` file in your project root directory. Fill in your `PUBLIC_SUPABASE_URL` and `PUBLIC_SUPABASE_PUBLISHABLE_KEY`: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx b/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx index 54142ac73c5b6..b36cbd8f6c992 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx @@ -58,7 +58,8 @@ hideToc: true Open `lib/main.dart` and edit the main function to initialize Supabase using your project URL and public API (anon) key: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/hono.mdx b/apps/docs/content/guides/getting-started/quickstarts/hono.mdx index 565ee5fae01a7..7efe526cbd8fa 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/hono.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/hono.mdx @@ -50,7 +50,8 @@ hideToc: true Lastly, [enable anonymous sign-ins](/dashboard/project/_/auth/providers) in the Auth settings. - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx b/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx index 9424fd47d9f77..2cda6ab608902 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx @@ -42,7 +42,8 @@ hideToc: true Create a new `Supabase.swift` file add a new Supabase instance using your project URL and public API (anon) key: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx b/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx index 59f7b6cf283bc..12711786238cf 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx @@ -79,7 +79,8 @@ hideToc: true Replace the `supabaseUrl` and `supabaseKey` with your own: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx index e7fc33911c063..2a58a49ab6fec 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx @@ -40,7 +40,8 @@ hideToc: true Rename `.env.example` to `.env.local` and populate with your Supabase connection variables: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx index 0da48358c7fc4..d9a6a20cd747b 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx @@ -56,7 +56,8 @@ hideToc: true Create a `.env` file and populate with your Supabase connection variables: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx index 1bf67565651f1..d9bf9f3d13892 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx @@ -56,7 +56,8 @@ hideToc: true Create a `.env.local` file and populate with your Supabase connection variables: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/refine.mdx b/apps/docs/content/guides/getting-started/quickstarts/refine.mdx index a6cb4ff632ed0..11214198c801e 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/refine.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/refine.mdx @@ -78,7 +78,8 @@ hideToc: true You now have to update the `supabaseClient` with the `SUPABASE_URL` and `SUPABASE_KEY` of your Supabase API. The `supabaseClient` is used in auth provider and data provider methods that allow the refine app to connect to your Supabase backend. - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx index 10aa736884a14..a0d82752f2e8e 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx @@ -56,7 +56,8 @@ hideToc: true Create a `.env.local` file and populate with your Supabase connection variables: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx index c5b20f469cb02..82061a1386828 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx @@ -57,7 +57,8 @@ hideToc: true Create a `.env` file at the root of your project and populate with your Supabase connection variables: - + + diff --git a/apps/docs/content/guides/getting-started/quickstarts/vue.mdx b/apps/docs/content/guides/getting-started/quickstarts/vue.mdx index fb1de8db617fd..715acdd2eafb7 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/vue.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/vue.mdx @@ -56,7 +56,8 @@ hideToc: true Create a `.env.local` file and populate with your Supabase connection variables: - + + diff --git a/apps/docs/content/guides/queues/pgmq.mdx b/apps/docs/content/guides/queues/pgmq.mdx index d5c01326e8fd2..6b7f85d8cb5e0 100644 --- a/apps/docs/content/guides/queues/pgmq.mdx +++ b/apps/docs/content/guides/queues/pgmq.mdx @@ -77,7 +77,7 @@ select pgmq.create_unlogged('my_unlogged'); #### `detach_archive` -Drop the queue's archive table as a member of the PGMQ extension. Useful for preventing the queue's archive table from being drop when `drop extension pgmq` is executed. +Drop the queue's archive table as a member of the PGMQ extension. Useful for preventing the queue's archive table from being dropped when `drop extension pgmq` is executed. This does not prevent the further archives() from appending to the archive table. {/* prettier-ignore */} diff --git a/apps/docs/content/guides/self-hosting/docker.mdx b/apps/docs/content/guides/self-hosting/docker.mdx index a704a2ef93ea0..ea07e3eb45eb2 100644 --- a/apps/docs/content/guides/self-hosting/docker.mdx +++ b/apps/docs/content/guides/self-hosting/docker.mdx @@ -221,6 +221,7 @@ Update the `./docker/.env` file with your own secrets. In particular, these are - `SMTP_*`: mail server credentials. You can use any SMTP server. - `POOLER_TENANT_ID`: the tenant-id that will be used by Supavisor pooler for your connection string - `PG_META_CRYPTO_KEY`: encryption key for securing connection strings between Studio and postgres-meta +- `SECRET_KEY_BASE`: encryption key for securing Realtime and Supavisor communications. (Must be at least 64 characters; generate with `openssl rand -base64 48`) You will need to [restart](#restarting-all-services) the services for the changes to take effect. diff --git a/apps/docs/lib/fetch/fetchWrappers.ts b/apps/docs/lib/fetch/fetchWrappers.ts index f8a145f5878e1..50fcd93ebf1ea 100644 --- a/apps/docs/lib/fetch/fetchWrappers.ts +++ b/apps/docs/lib/fetch/fetchWrappers.ts @@ -28,7 +28,6 @@ async function constructHeaders(headersInit?: HeadersInit | undefined) { headers.set('Authorization', `Bearer ${accessToken}`) } } - return headers } diff --git a/apps/docs/lib/fetch/projectApi.ts b/apps/docs/lib/fetch/projectApi.ts index 8a88f8062de55..8fe4a68d487d7 100644 --- a/apps/docs/lib/fetch/projectApi.ts +++ b/apps/docs/lib/fetch/projectApi.ts @@ -4,41 +4,73 @@ import { UseQueryOptions, useQuery } from '@tanstack/react-query' const projectApiKeys = { api: (projectRef: string | undefined) => ['projects', projectRef, 'api'] as const, + settings: (projectRef: string | undefined) => ['projects', projectRef, 'settings'] as const, } export interface ProjectApiVariables { projectRef?: string } +type ProjectApiError = ResponseError + +export type ProjectKeys = Awaited> -async function getProjectApi({ projectRef }: ProjectApiVariables, signal?: AbortSignal) { +export type ProjectSettings = Awaited> + +async function getProjectKeys({ projectRef }: ProjectApiVariables, signal?: AbortSignal) { if (!projectRef) { throw Error('projectRef is required') } - const { data, error } = await get('/platform/projects/{ref}/settings', { + const { data, error } = await get('/v1/projects/{ref}/api-keys', { params: { path: { ref: projectRef }, }, signal, }) if (error) throw error - return data } -export type ProjectApiData = Awaited> -type ProjectApiError = ResponseError +async function getProjectSettings({ projectRef }: ProjectApiVariables, signal?: AbortSignal) { + if (!projectRef) { + throw Error('projectRef is required') + } -export function useProjectApiQuery( + const { data, error } = await get('/platform/projects/{ref}/settings', { + params: { + path: { ref: projectRef }, + }, + signal, + }) + if (error) throw error + return data +} + +export function useProjectKeysQuery( { projectRef }: ProjectApiVariables, { enabled = true, ...options - }: Omit, 'queryKey'> = {} + }: Omit, 'queryKey'> = {} ) { - return useQuery({ + return useQuery({ queryKey: projectApiKeys.api(projectRef), - queryFn: ({ signal }) => getProjectApi({ projectRef }, signal), + queryFn: ({ signal }) => getProjectKeys({ projectRef }, signal), + enabled, + ...options, + }) +} + +export function useProjectSettingsQuery( + { projectRef }: ProjectApiVariables, + { + enabled = true, + ...options + }: Omit, 'queryKey'> = {} +) { + return useQuery({ + queryKey: projectApiKeys.settings(projectRef), + queryFn: ({ signal }) => getProjectSettings({ projectRef }, signal), enabled, ...options, }) diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index 93ce13e3f7367..fc3397f00f205 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -396,11 +396,9 @@ export const CreateBranchModal = () => { description="Clone production data into this branch" > - + {hasPitrEnabled && ( + + )} )} diff --git a/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx b/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx index ecb2e2e90be0d..89e7f39f251f5 100644 --- a/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx +++ b/apps/studio/components/interfaces/Connect/content/ionicangular/supabasejs/content.tsx @@ -24,7 +24,7 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => { {` export const environment = { supabaseUrl: '${projectKeys.apiUrl ?? 'your-project-url'}', - supabaseKey: '${projectKeys.publishableKey ?? ''}', + supabaseKey: '${projectKeys.publishableKey ?? ''}', }; `} diff --git a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx index b5c8134bf45c3..25cd293a1edb9 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx @@ -173,6 +173,8 @@ const CustomTooltip = ({ ...(maxValueAttribute?.attribute ? [maxValueAttribute.attribute] : []), ] + const localTimeZone = dayjs.tz.guess() + const total = showTotal && calculateTotalChartAggregate(payload, attributesToIgnoreFromTotal) const getIcon = (color: string, isMax: boolean) => @@ -212,6 +214,7 @@ const CustomTooltip = ({ !isActiveHoveredChart && 'opacity-0' )} > + {localTimeZone} {dayjs(timestamp).format(DateTimeFormats.FULL_SECONDS)} {payload.reverse().map((entry: any, index: number) => ( diff --git a/apps/studio/data/reports/database-charts.ts b/apps/studio/data/reports/database-charts.ts index 3f8efcc622cf9..692edba8947d1 100644 --- a/apps/studio/data/reports/database-charts.ts +++ b/apps/studio/data/reports/database-charts.ts @@ -7,210 +7,6 @@ import { DiskAttributesData } from '../config/disk-attributes-query' import { MaxConnectionsData } from '../database/max-connections-query' import { Project } from '../projects/project-detail-query' -export const getReportAttributes = (diskConfig?: DiskAttributesData): ReportAttributes[] => { - return [ - { - id: 'ram-usage', - label: 'Memory usage', - availableIn: ['free', 'pro'], - hide: false, - showTooltip: false, - showLegend: false, - hideChartType: false, - defaultChartStyle: 'bar', - showMaxValue: false, - showGrid: false, - syncId: 'database-reports', - valuePrecision: 0, - format: '%', - attributes: [ - { - attribute: 'ram_usage', - provider: 'infra-monitoring', - label: 'Memory usage', - format: '%', - tooltip: 'RAM usage by the database', - }, - ], - }, - { - id: 'avg_cpu_usage', - label: 'Average CPU usage', - syncId: 'database-reports', - format: '%', - valuePrecision: 2, - availableIn: ['free', 'pro'], - hide: false, - showTooltip: false, - showLegend: false, - showMaxValue: false, - showGrid: false, - hideChartType: false, - defaultChartStyle: 'bar', - attributes: [ - { - attribute: 'avg_cpu_usage', - provider: 'infra-monitoring', - label: 'Average CPU usage', - format: '%', - tooltip: 'Average CPU usage', - }, - ], - }, - { - id: 'max_cpu_usage', - label: 'Max CPU usage', - syncId: 'database-reports', - format: '%', - valuePrecision: 2, - availableIn: ['free', 'pro'], - hide: false, - showTooltip: false, - showLegend: false, - showMaxValue: false, - showGrid: false, - hideChartType: false, - defaultChartStyle: 'bar', - attributes: [ - { - attribute: 'max_cpu_usage', - provider: 'infra-monitoring', - label: 'Max CPU usage', - format: '%', - tooltip: 'Max CPU usage', - }, - ], - }, - { - id: 'disk-iops', - label: 'Disk Input/Output operations per second (IOPS)', - syncId: 'database-reports', - availableIn: ['free', 'pro'], - hide: false, - showTooltip: true, - valuePrecision: 2, - showLegend: true, - hideChartType: false, - showGrid: true, - showMaxValue: false, - YAxisProps: { - width: 35, - tickFormatter: (value: any) => numberFormatter(value, 2), - }, - defaultChartStyle: 'line', - docsUrl: `${DOCS_URL}/guides/platform/compute-and-disk#compute-size`, - attributes: [ - { - attribute: 'disk_iops_write', - provider: 'infra-monitoring', - label: 'Write IOPS', - tooltip: - 'Number of write operations per second. High values indicate frequent data writes, logging, or transaction activity', - }, - { - attribute: 'disk_iops_read', - provider: 'infra-monitoring', - label: 'Read IOPS', - tooltip: - 'Number of read operations per second. High values suggest frequent disk reads due to queries or poor caching', - }, - { - attribute: 'disk_iops_max', - provider: 'reference-line', - label: 'Max IOPS', - value: diskConfig?.attributes?.iops, - tooltip: - 'Maximum IOPS (Input/Output Operations Per Second) for your current compute size', - isMaxValue: true, - }, - ], - }, - { - id: 'disk-io-usage', - label: 'Disk IO Usage', - syncId: 'database-reports', - availableIn: ['team', 'enterprise'], - hide: false, - format: '%', - attributes: [], - }, - { - id: 'pooler-database-connections', - label: 'Pooler to Database connections', - syncId: 'database-reports', - valuePrecision: 0, - availableIn: ['free', 'pro'], - hide: false, - showTooltip: false, - showLegend: false, - showMaxValue: false, - hideChartType: false, - showGrid: false, - defaultChartStyle: 'bar', - attributes: [ - { - attribute: 'pg_stat_database_num_backends', - provider: 'infra-monitoring', - label: 'Database connections', - tooltip: 'Number of pooler connections to the database', - }, - ], - }, - { - id: 'supavisor-connections', - label: 'Shared Pooler connections', - syncId: 'database-reports', - valuePrecision: 0, - availableIn: ['free', 'pro'], - hide: false, - showTooltip: false, - showLegend: false, - showMaxValue: false, - showGrid: false, - hideChartType: false, - defaultChartStyle: 'bar', - attributes: [ - { - attribute: 'supavisor_connections_active', - provider: 'infra-monitoring', - label: 'Client to Shared Pooler connections', - tooltip: 'Active connections from clients to the shared pooler', - }, - ], - }, - { - id: 'pgbouncer-connections', - label: 'Dedicated Pooler connections', - syncId: 'database-reports', - valuePrecision: 0, - availableIn: ['pro', 'team'], - hide: false, - showTooltip: false, - showLegend: false, - showMaxValue: false, - showGrid: false, - hideChartType: false, - defaultChartStyle: 'bar', - attributes: [ - { - attribute: 'client_connections_pgbouncer', - provider: 'infra-monitoring', - label: 'Client to Dedicated Pooler connections', - tooltip: 'PgBouncer connections', - }, - ], - }, - { - id: 'disk-size', - label: 'Disk Usage', - syncId: 'database-reports', - availableIn: ['team', 'enterprise'], - hide: false, - attributes: [], - }, - ] -} - export const getReportAttributesV2: ( org: Organization, project: Project, @@ -227,7 +23,7 @@ export const getReportAttributesV2: ( id: 'ram-usage', label: 'Memory usage', docsUrl: `${DOCS_URL}/guides/telemetry/reports#memory-usage`, - availableIn: ['team', 'enterprise'], + availableIn: ['free', 'pro', 'team', 'enterprise'], hide: false, showTooltip: true, showLegend: true, @@ -272,7 +68,7 @@ export const getReportAttributesV2: ( syncId: 'database-reports', format: '%', valuePrecision: 2, - availableIn: ['team', 'enterprise'], + availableIn: ['free', 'pro', 'team', 'enterprise'], hide: false, showTooltip: true, showLegend: true, @@ -339,7 +135,7 @@ export const getReportAttributesV2: ( label: 'Disk Input/Output operations per second (IOPS)', docsUrl: `${DOCS_URL}/guides/telemetry/reports#disk-inputoutput-operations-per-second-iops`, syncId: 'database-reports', - availableIn: ['team', 'enterprise'], + availableIn: ['free', 'pro', 'team', 'enterprise'], hide: false, showTooltip: true, valuePrecision: 2, @@ -408,12 +204,46 @@ export const getReportAttributesV2: ( ], }, { + // Client Connections metric for free tier + id: 'client-connections-basic', + label: 'Database Connections', + syncId: 'database-reports', + valuePrecision: 0, + availableIn: ['free'], + hide: !isFreePlan, + showTooltip: false, + showLegend: false, + showMaxValue: true, + hideChartType: false, + showGrid: true, + YAxisProps: { width: 30 }, + defaultChartStyle: 'line', + docsUrl: `${DOCS_URL}/guides/telemetry/reports#database-connections`, + attributes: [ + { + attribute: 'total_db_connections', + provider: 'infra-monitoring', + label: 'Total connections', + tooltip: 'Total number of active database connections', + }, + { + attribute: 'max_db_connections', + provider: 'reference-line', + label: 'Max connections', + value: maxConnections?.maxConnections, + tooltip: 'Max available connections for your current compute size', + isMaxValue: true, + }, + ], + }, + { + // advanced client connections metric for paid and above id: 'client-connections', label: 'Database Connections', syncId: 'database-reports', valuePrecision: 0, - availableIn: ['team', 'enterprise'], - hide: false, + availableIn: ['pro', 'team', 'enterprise'], + hide: isFreePlan, showTooltip: true, showLegend: true, showMaxValue: true, @@ -476,7 +306,7 @@ export const getReportAttributesV2: ( label: 'Dedicated Pooler Client Connections', syncId: 'database-reports', valuePrecision: 0, - availableIn: ['pro', 'team'], + availableIn: ['pro', 'team', 'enterprise'], hide: isFreePlan, showTooltip: true, showLegend: true, @@ -508,7 +338,7 @@ export const getReportAttributesV2: ( label: 'Shared Pooler (Supavisor) client connections', syncId: 'database-reports', valuePrecision: 0, - availableIn: ['pro', 'team'], + availableIn: ['pro', 'team', 'enterprise'], hide: isFreePlan, showTooltip: false, showLegend: false, @@ -531,7 +361,7 @@ export const getReportAttributesV2: ( label: 'Disk Usage', syncId: 'database-reports', valuePrecision: 2, - availableIn: ['free', 'pro', 'team'], + availableIn: ['free', 'pro', 'team', 'enterprise'], hide: false, showTooltip: true, showLegend: true, diff --git a/apps/studio/pages/project/[ref]/reports/database.tsx b/apps/studio/pages/project/[ref]/reports/database.tsx index 0ba53523e2e88..5eaca8b089d89 100644 --- a/apps/studio/pages/project/[ref]/reports/database.tsx +++ b/apps/studio/pages/project/[ref]/reports/database.tsx @@ -6,7 +6,7 @@ import Link from 'next/link' import { useEffect, useState } from 'react' import { toast } from 'sonner' -import { useFlag, useParams } from 'common' +import { useParams } from 'common' import ReportHeader from 'components/interfaces/Reports/ReportHeader' import ReportPadding from 'components/interfaces/Reports/ReportPadding' import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Reports.constants' @@ -33,7 +33,7 @@ import { useProjectDiskResizeMutation } from 'data/config/project-disk-resize-mu import { useDatabaseSizeQuery } from 'data/database/database-size-query' import { useMaxConnectionsQuery } from 'data/database/max-connections-query' import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query' -import { getReportAttributes, getReportAttributesV2 } from 'data/reports/database-charts' +import { getReportAttributesV2 } from 'data/reports/database-charts' import { useDatabaseReport } from 'data/reports/database-report-query' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' @@ -65,7 +65,6 @@ export default DatabaseReport const DatabaseUsage = () => { const { db, chart, ref } = useParams() - const isReportsV2 = useFlag('reportsDatabaseV2') const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() @@ -81,10 +80,6 @@ const DatabaseUsage = () => { handleDatePickerChange, } = useReportDateRange(REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES) - const isTeamsOrEnterprisePlan = - !isOrgPlanLoading && (orgPlan?.id === 'team' || orgPlan?.id === 'enterprise') - const showChartsV2 = isReportsV2 || isTeamsOrEnterprisePlan - const state = useDatabaseSelectorStateSnapshot() const queryClient = useQueryClient() @@ -130,8 +125,7 @@ const DatabaseUsage = () => { } ) - const REPORT_ATTRIBUTES = getReportAttributes(diskConfig) - const REPORT_ATTRIBUTES_V2 = getReportAttributesV2( + const REPORT_ATTRIBUTES = getReportAttributesV2( org!, project!, diskConfig, @@ -149,51 +143,22 @@ const DatabaseUsage = () => { const onRefreshReport = async () => { if (!selectedDateRange) return - // [Joshen] Since we can't track individual loading states for each chart - // so for now we mock a loading state that only lasts for a second setIsRefreshing(true) refresh() const { period_start, period_end, interval } = selectedDateRange - REPORT_ATTRIBUTES.forEach((attr) => { - queryClient.invalidateQueries( - analyticsKeys.infraMonitoring(ref, { - attribute: attr?.id, - startDate: period_start.date, - endDate: period_end.date, - interval, - databaseIdentifier: state.selectedDatabaseId, - }) - ) - }) - if (showChartsV2) { - REPORT_ATTRIBUTES_V2.forEach((chart: any) => { - chart.attributes.forEach((attr: any) => { - queryClient.invalidateQueries( - analyticsKeys.infraMonitoring(ref, { - attribute: attr.attribute, - startDate: period_start.date, - endDate: period_end.date, - interval, - databaseIdentifier: state.selectedDatabaseId, - }) - ) - }) - }) - } else { - REPORT_ATTRIBUTES.forEach((chart: any) => { - chart.attributes.forEach((attr: any) => { - queryClient.invalidateQueries( - analyticsKeys.infraMonitoring(ref, { - attribute: attr.attribute, - startDate: period_start.date, - endDate: period_end.date, - interval, - databaseIdentifier: state.selectedDatabaseId, - }) - ) - }) + REPORT_ATTRIBUTES.forEach((chart: any) => { + chart.attributes.forEach((attr: any) => { + queryClient.invalidateQueries( + analyticsKeys.infraMonitoring(ref, { + attribute: attr.attribute, + startDate: period_start.date, + endDate: period_end.date, + interval, + databaseIdentifier: state.selectedDatabaseId, + }) + ) }) - } + }) if (isReplicaSelected) { queryClient.invalidateQueries( analyticsKeys.infraMonitoring(ref, { @@ -274,56 +239,37 @@ const DatabaseUsage = () => { > {selectedDateRange && orgPlan?.id && - (showChartsV2 - ? REPORT_ATTRIBUTES_V2.filter((chart) => !chart.hide).map((chart) => ( - - )) - : REPORT_ATTRIBUTES.filter((chart) => !chart.hide).map((chart, i) => - chart.availableIn?.includes(orgPlan?.id) ? ( - - ) : ( - - ) - ))} + REPORT_ATTRIBUTES.filter((chart) => !chart.hide).map((chart) => + chart.availableIn?.includes(orgPlan?.id) ? ( + + ) : ( + + ) + )} {selectedDateRange && isReplicaSelected && ( diff --git a/packages/eslint-config-supabase/next.js b/packages/eslint-config-supabase/next.js index b19ed52548189..667ffba631de0 100644 --- a/packages/eslint-config-supabase/next.js +++ b/packages/eslint-config-supabase/next.js @@ -3,7 +3,8 @@ const js = require('@eslint/js') const { FlatCompat } = require('@eslint/eslintrc') const prettierConfig = require('eslint-config-prettier/flat') const { default: turboConfig } = require('eslint-config-turbo/flat') -const { off } = require('process') +const { fixupPluginRules } = require('@eslint/compat') +const tanstackQuery = require('@tanstack/eslint-plugin-query') const compat = new FlatCompat({ baseDirectory: __dirname, @@ -11,11 +12,25 @@ const compat = new FlatCompat({ allConfig: js.configs.all, }) +// Tanstack Query config is meant for the old non-flat esling configs. This adapts it to work with flat configs. v5 of +// the plugin supports flat configs natively. +const tanstackQueryConfig = { + name: '@tanstack/query', + plugins: { '@tanstack/query': fixupPluginRules(tanstackQuery) }, + rules: { + '@tanstack/query/exhaustive-deps': 'warn', + '@tanstack/query/no-deprecated-options': 'warn', + '@tanstack/query/prefer-query-object-syntax': 'warn', + '@tanstack/query/stable-query-client': 'warn', + }, +} + module.exports = defineConfig([ // Global ignore for the .next folder { ignores: ['.next', 'public'] }, turboConfig, prettierConfig, + tanstackQueryConfig, { extends: compat.extends('next/core-web-vitals'), linterOptions: { diff --git a/packages/eslint-config-supabase/package.json b/packages/eslint-config-supabase/package.json index 5610d5edeef11..4a73e5f229c55 100644 --- a/packages/eslint-config-supabase/package.json +++ b/packages/eslint-config-supabase/package.json @@ -7,14 +7,14 @@ "preinstall": "npx only-allow pnpm", "clean": "rimraf node_modules" }, - "dependencies": { - "eslint-config-next": "^15.5.0", - "eslint-config-prettier": "^10.0.0", - "eslint-config-turbo": "^2.5.0" - }, "devDependencies": { + "@eslint/compat": "^1.4.0", "@eslint/eslintrc": "^3.0.0", "@eslint/js": "^9.0.0", + "@tanstack/eslint-plugin-query": "^4.0.0", + "eslint-config-next": "^15.5.0", + "eslint-config-prettier": "^10.0.0", + "eslint-config-turbo": "^2.5.0", "typescript": "catalog:" } } diff --git a/packages/pg-meta/src/query/Query.utils.ts b/packages/pg-meta/src/query/Query.utils.ts index 47e2bb9f78f60..5a4af7e305d50 100644 --- a/packages/pg-meta/src/query/Query.utils.ts +++ b/packages/pg-meta/src/query/Query.utils.ts @@ -166,6 +166,23 @@ function applyFilters(query: string, filters: Filter[]) { if (filters.length === 0) return query query += ` where ${filters .map((filter) => { + // Handle composite values + if (Array.isArray(filter.column)) { + switch (filter.operator) { + case 'in': + return inTupleFilterSql(filter) + case '=': + case '<>': + case '>': + case '<': + case '>=': + case '<=': + return defaultTupleFilterSql(filter) + default: + throw new Error(`Cannot use ${filter.operator} operator in a tuple filter`) + } + } + switch (filter.operator) { case 'in': return inFilterSql(filter) @@ -185,16 +202,61 @@ function applyFilters(query: string, filters: Filter[]) { } function inFilterSql(filter: Filter) { - let values + let values: Array if (Array.isArray(filter.value)) { - values = filter.value.map((x: any) => filterLiteral(x)) + values = filter.value.map((x) => filterLiteral(x)) } else { const filterValueTxt = String(filter.value) - values = filterValueTxt.split(',').map((x: any) => filterLiteral(x)) + values = filterValueTxt.split(',').map((x) => filterLiteral(x)) } return `${ident(filter.column)} ${filter.operator} (${values.join(',')})` } +function defaultTupleFilterSql(filter: Filter) { + if (!Array.isArray(filter.column)) { + throw new Error('Use standard applyFilters for single column') + } + if (!Array.isArray(filter.value)) { + throw new Error('Tuple filter value must be an array') + } + if (filter.value.length !== filter.column.length) { + throw new Error('Tuple filter value must have the same length as the column array') + } + + const columns = `(${filter.column.map((c) => ident(c)).join(', ')})` + const values = `(${filter.value.map((v) => filterLiteral(v)).join(', ')})` + return `${columns} ${filter.operator} ${values}` +} + +function inTupleFilterSql(filter: Filter) { + if (!Array.isArray(filter.column)) { + throw new Error('Use inFilterSql for single columns') + } + if (!Array.isArray(filter.value)) { + throw new Error(`Values for a tuple 'in' filter must be an array`) + } + + const columns = `(${filter.column.map((c) => ident(c)).join(', ')})` + + const values = filter.value.map((v) => { + if (Array.isArray(v)) { + if (v.length !== filter.column.length) { + throw new Error(`Tuple value length must match column length`) + } + return `(${v.map((x) => filterLiteral(x)).join(', ')})` + } else { + const filterValueTxt = String(v) + const currValues = filterValueTxt.split(',') + if (currValues.length !== filter.column.length) { + throw new Error(`Tuple value length must match column length`) + } + return `(${currValues.map((x) => filterLiteral(x)).join(', ')})` + } + }) + + return `${columns} ${filter.operator} (${values.join(', ')})` +} + function isFilterSql(filter: Filter) { const filterValueTxt = String(filter.value) switch (filterValueTxt) { diff --git a/packages/pg-meta/src/query/QueryFilter.ts b/packages/pg-meta/src/query/QueryFilter.ts index b36019c5d2d8d..86e05a71e7b3b 100644 --- a/packages/pg-meta/src/query/QueryFilter.ts +++ b/packages/pg-meta/src/query/QueryFilter.ts @@ -18,7 +18,7 @@ export class QueryFilter implements IQueryFilter, IQueryModifier { protected actionOptions?: { returning: boolean; enumArrayColumns?: string[] } ) {} - filter(column: string, operator: FilterOperator, value: any) { + filter(column: string | string[], operator: FilterOperator, value: any) { this.filters.push({ column, operator, value }) return this } diff --git a/packages/pg-meta/src/query/types.ts b/packages/pg-meta/src/query/types.ts index 7e0045cdb81c6..08cfd6348f00a 100644 --- a/packages/pg-meta/src/query/types.ts +++ b/packages/pg-meta/src/query/types.ts @@ -20,7 +20,7 @@ export type FilterOperator = | 'is' export interface Filter { - column: string + column: string | Array operator: FilterOperator value: any } diff --git a/packages/pg-meta/test/query/query.test.ts b/packages/pg-meta/test/query/query.test.ts index 0302f41efc6fc..b53929d026f79 100644 --- a/packages/pg-meta/test/query/query.test.ts +++ b/packages/pg-meta/test/query/query.test.ts @@ -419,6 +419,174 @@ describe('Query.utils', () => { const result = QueryUtils.selectQuery(table, '*', { filters: filters }) expect(result).toContain("where name = 'O''Reilly'") }) + + test('should error if tuple filter value length does not match column length', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '=', value: [1] }] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError( + 'Tuple filter value must have the same length as the column array' + ) + }) + + test('should error if tuple filter value is not an array', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '=', value: 1 }] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError( + 'Tuple filter value must be an array' + ) + }) + + test('should correctly handle tuple filters with equality operator', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '=', value: [1, 2] }] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe('select * from public.users where (id, version) = (1, 2);') + }) + + test('should correctly handle tuple filters with greater than operator', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '>', value: [1, 2] }] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe('select * from public.users where (id, version) > (1, 2);') + }) + + test('should correctly handle tuple filters with greater than or equal operator', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '>=', value: [1, 2] }] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe('select * from public.users where (id, version) >= (1, 2);') + }) + + test('should correctly handle tuple filters with less than operator', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '<', value: [10, 5] }] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe('select * from public.users where (id, version) < (10, 5);') + }) + + test('should correctly handle tuple filters with less than or equal operator', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '<=', value: [10, 5] }] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe('select * from public.users where (id, version) <= (10, 5);') + }) + + test('should correctly handle tuple filters with not equal operator (<>)', () => { + const filters: Filter[] = [{ column: ['id', 'version'], operator: '<>', value: [1, 2] }] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe('select * from public.users where (id, version) <> (1, 2);') + }) + + test('should correctly handle tuple filters with in operator', () => { + const filters: Filter[] = [ + { + column: ['id', 'version'], + operator: 'in', + value: [ + [1, 2], + [3, 4], + [5, 6], + ], + }, + ] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe( + 'select * from public.users where (id, version) in ((1, 2), (3, 4), (5, 6));' + ) + }) + + test('should error if tuple filters with in operator do not have matching number of array values', () => { + const filters: Filter[] = [ + { + column: ['id', 'version'], + operator: 'in', + value: [[1, 2], [3, 4], [5]], + }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) + + test('should correctly handle tuple filters with in operator using strings', () => { + const filters: Filter[] = [ + { + column: ['id', 'version'], + operator: 'in', + value: ['one,two', 'three,four', 'five,six'], + }, + ] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe( + `select * from public.users where (id, version) in (('one', 'two'), ('three', 'four'), ('five', 'six'));` + ) + }) + + test('should error if tuple filters with in operator do not have matching number of stringified values', () => { + const filters: Filter[] = [ + { + column: ['id', 'version'], + operator: 'in', + value: ['one,two', 'three,four', 'five'], + }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) + + test('should correctly handle tuple filters with string values', () => { + const filters: Filter[] = [ + { column: ['first_name', 'last_name'], operator: '=', value: ['John', 'Doe'] }, + ] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe( + "select * from public.users where (first_name, last_name) = ('John', 'Doe');" + ) + }) + + test('should correctly handle mixed tuple and regular filters', () => { + const filters: Filter[] = [ + { column: ['id', 'version'], operator: '>', value: [1, 2] }, + { column: 'active', operator: '=', value: true }, + ] + const result = QueryUtils.selectQuery(table, '*', { filters: filters }) + expect(result).toBe( + 'select * from public.users where (id, version) > (1, 2) and active = true;' + ) + }) + + test('should error when trying to use "is" operator as a tuple filter', () => { + const filters: Filter[] = [ + { + column: ['id', 'version'], + operator: 'is', + value: [null, null], + }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) + + test('should error when trying to use "~~" operator as a tuple filter', () => { + const filters: Filter[] = [ + { + column: ['first_name', 'last_name'], + operator: '~~', + value: ['%John%', '%Doe%'], + }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) + + test('should error when trying to use "~~*" operator as a tuple filter', () => { + const filters: Filter[] = [ + { column: ['first_name', 'last_name'], operator: '~~*', value: ['%john%', '%doe%'] }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) + + test('should error when trying to use "!~~" operator as a tuple filter', () => { + const filters: Filter[] = [ + { column: ['first_name', 'last_name'], operator: '!~~', value: ['%Admin%', '%System%'] }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) + + test('should error when trying to use "!~~*" operator as a tuple filter', () => { + const filters: Filter[] = [ + { column: ['first_name', 'last_name'], operator: '!~~*', value: ['%admin%', '%system%'] }, + ] + expect(() => QueryUtils.selectQuery(table, '*', { filters: filters })).toThrowError() + }) }) describe('applySorts', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 655a22e78d140..f6797a4e14a98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2055,7 +2055,19 @@ importers: version: 5.9.2 packages/eslint-config-supabase: - dependencies: + devDependencies: + '@eslint/compat': + specifier: ^1.4.0 + version: 1.4.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) + '@eslint/eslintrc': + specifier: ^3.0.0 + version: 3.3.1(supports-color@8.1.1) + '@eslint/js': + specifier: ^9.0.0 + version: 9.37.0 + '@tanstack/eslint-plugin-query': + specifier: ^4.0.0 + version: 4.39.1(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1)) eslint-config-next: specifier: ^15.5.0 version: 15.5.4(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.2) @@ -2065,13 +2077,6 @@ importers: eslint-config-turbo: specifier: ^2.5.0 version: 2.5.8(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))(turbo@2.3.3) - devDependencies: - '@eslint/eslintrc': - specifier: ^3.0.0 - version: 3.3.1(supports-color@8.1.1) - '@eslint/js': - specifier: ^9.0.0 - version: 9.37.0 typescript: specifier: 'catalog:' version: 5.9.2 @@ -2127,7 +2132,7 @@ importers: version: 8.11.11 '@vitest/coverage-v8': specifier: ^3.0.9 - version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9) + version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -2363,7 +2368,7 @@ importers: version: 15.5.7 '@vitest/coverage-v8': specifier: ^3.0.9 - version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9) + version: 3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)) common: specifier: workspace:* version: link:../common @@ -3770,6 +3775,15 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/compat@1.4.0': + resolution: {integrity: sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.40 || 9 + peerDependenciesMeta: + eslint: + optional: true + '@eslint/config-array@0.21.0': resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -8974,6 +8988,11 @@ packages: resolution: {integrity: sha512-eXjzg2T7eiEruKXtu1XQ2bHV1oO8muGWs+TuDWH3tIFPwH1hIzahWpcTwz5lc7jW5xqud3gru2tK6wTk2JlIrw==} engines: {node: '>=12'} + '@tanstack/eslint-plugin-query@4.39.1': + resolution: {integrity: sha512-5YDX4mdRC0hllHKp531CnScFWZU7aFrJ1aTyyuaB6+z0/i0JfcKuckSTYaji3vUk82GALM90eWwHFVRAch+7tQ==} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + '@tanstack/history@1.114.22': resolution: {integrity: sha512-CNwKraj/Xa8H7DUyzrFBQC3wL96JzIxT4i9CW0hxqFNNmLDyUcMJr8264iqqfxC0u1lFSG96URad08T2Qhadpw==} engines: {node: '>=12'} @@ -22286,6 +22305,12 @@ snapshots: '@eslint-community/regexpp@4.12.1': {} + '@eslint/compat@1.4.0(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))': + dependencies: + '@eslint/core': 0.16.0 + optionalDependencies: + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + '@eslint/config-array@0.21.0(supports-color@8.1.1)': dependencies: '@eslint/object-schema': 2.1.6 @@ -28779,6 +28804,10 @@ snapshots: - tsx - yaml + '@tanstack/eslint-plugin-query@4.39.1(eslint@9.37.0(jiti@2.5.1)(supports-color@8.1.1))': + dependencies: + eslint: 9.37.0(jiti@2.5.1)(supports-color@8.1.1) + '@tanstack/history@1.114.22': {} '@tanstack/match-sorter-utils@8.8.4': @@ -30215,7 +30244,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9)': + '@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9(@types/node@22.13.14)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -30233,6 +30262,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/coverage-v8@3.0.9(supports-color@8.1.1)(vitest@3.0.9)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + debug: 4.4.3(supports-color@8.1.1) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6(supports-color@8.1.1) + istanbul-reports: 3.1.7 + magic-string: 0.30.19 + magicast: 0.3.5 + std-env: 3.9.0 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.0.9': dependencies: '@vitest/spy': 3.0.9 @@ -30308,7 +30355,7 @@ snapshots: sirv: 3.0.0 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + vitest: 3.0.9(@types/node@22.13.14)(@vitest/ui@3.0.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.4.11(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.1) '@vitest/utils@3.0.4': dependencies:
{localTimeZone}
{dayjs(timestamp).format(DateTimeFormats.FULL_SECONDS)}