From e94e6fb67b4f2e49441b05b30efb45c01a0d8408 Mon Sep 17 00:00:00 2001 From: Anthony Main Date: Thu, 24 Apr 2025 15:34:54 +0100 Subject: [PATCH 01/86] Update database-migrations.mdx (#35244) Added notes for seeding database --- .../guides/deployment/database-migrations.mdx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/guides/deployment/database-migrations.mdx b/apps/docs/content/guides/deployment/database-migrations.mdx index bae1145219b..6495069cbf5 100644 --- a/apps/docs/content/guides/deployment/database-migrations.mdx +++ b/apps/docs/content/guides/deployment/database-migrations.mdx @@ -303,7 +303,7 @@ supabase link - + [Push](/docs/reference/cli/supabase-db-push) your migrations to the remote database. @@ -318,4 +318,22 @@ supabase db push + + + + + [Push](/docs/reference/cli/supabase-db-push) your migrations and seed the remote database. + + + + +```bash name=Terminal +supabase db push --include-seed +``` + + + + + + Visiting your live project on [Supabase](https://supabase.com/dashboard/project/_), you'll see a new `employees` table, complete with the `department` column you added in the second migration above. From 7a3939d8f5876aa0ab636c493f207d82af0881ce Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 24 Apr 2025 21:35:31 +0700 Subject: [PATCH 02/86] Add link to table editor from wrappers (#35242) --- .../interfaces/Integrations/Wrappers/WrapperRow.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx index bfdd02cf96d..917a9cc87c6 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx @@ -79,10 +79,12 @@ const WrapperRow = ({ wrapper }: WrapperRowProps) => { /> - - - {table.schema}.{table.table_name} - + + + + {table.schema}.{table.table_name} + + ) })} From 495cff3df065cb227f867ea6785e83b11a91b49e Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Thu, 24 Apr 2025 19:33:39 +0200 Subject: [PATCH 03/86] remove lw14 promos (#35253) remove lw14 promo --- apps/docs/features/app.providers.tsx | 4 ++-- apps/www/components/Hero/Hero.tsx | 12 ------------ apps/www/pages/_app.tsx | 2 -- packages/ui-patterns/PromoToast/PromoToast.tsx | 18 +++++++----------- 4 files changed, 9 insertions(+), 27 deletions(-) diff --git a/apps/docs/features/app.providers.tsx b/apps/docs/features/app.providers.tsx index 63b3ace9bab..f5c4b77b007 100644 --- a/apps/docs/features/app.providers.tsx +++ b/apps/docs/features/app.providers.tsx @@ -11,7 +11,7 @@ import { QueryClientProvider } from './data/queryClient.client' import { PageTelemetry } from './telemetry/telemetry.client' import { ScrollRestoration } from './ui/helpers.scroll.client' import { ThemeSandbox } from './ui/theme.client' -import { PromoToast } from 'ui-patterns' +// import { PromoToast } from 'ui-patterns' /** * Global providers that wrap the entire app @@ -28,7 +28,7 @@ function GlobalProviders({ children }: PropsWithChildren) {
- + {/* */} {children} diff --git a/apps/www/components/Hero/Hero.tsx b/apps/www/components/Hero/Hero.tsx index 80cd1a11cf0..303015cfa2b 100644 --- a/apps/www/components/Hero/Hero.tsx +++ b/apps/www/components/Hero/Hero.tsx @@ -16,18 +16,6 @@ const Hero = () => {
-
- -

Build in a weekend Scale to millions diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx index f9b62cd6ea8..dbb1b0e053f 100644 --- a/apps/www/pages/_app.tsx +++ b/apps/www/pages/_app.tsx @@ -22,7 +22,6 @@ import MetaFaviconsPagesRouter, { DEFAULT_FAVICON_ROUTE, DEFAULT_FAVICON_THEME_COLOR, } from 'common/MetaFavicons/pages-router' -import { LW14Announcement } from 'ui-patterns/Banners/LW14Announcement' import { WwwCommandMenu } from '~/components/CommandMenu' import { API_URL, APP_NAME, DEFAULT_META_DESCRIPTION } from '~/lib/constants' import useDarkLaunchWeeks from '../hooks/useDarkLaunchWeeks' @@ -95,7 +94,6 @@ export default function App({ Component, pageProps }: AppProps) { > - diff --git a/packages/ui-patterns/PromoToast/PromoToast.tsx b/packages/ui-patterns/PromoToast/PromoToast.tsx index 04e2df02899..0f9610652a8 100644 --- a/packages/ui-patterns/PromoToast/PromoToast.tsx +++ b/packages/ui-patterns/PromoToast/PromoToast.tsx @@ -1,20 +1,18 @@ 'use client' -import Image from 'next/image' +// import Image from 'next/image' import Link from 'next/link' import { useEffect, useState } from 'react' import { cn } from 'ui/src/lib/utils/cn' import { Button } from 'ui/src/components/Button/Button' import { LOCAL_STORAGE_KEYS } from 'common' -import { useTheme } from 'next-themes' +// import { useTheme } from 'next-themes' import announcement from '../Banners/data.json' import './styles.css' -const LW14BG = `/docs/img/launchweek/14/promo-banner-bg.png` - const PromoToast = () => { const [visible, setVisible] = useState(false) - const { resolvedTheme } = useTheme() + // const { resolvedTheme } = useTheme() useEffect(() => { const shouldHide = @@ -47,9 +45,7 @@ const PromoToast = () => { fontFamily: 'Departure Mono, Source Code Pro, Office Code Pro, Menlo, monospace', }} className="relative z-10 text-foreground flex flex-col text-xl w-full leading-7" - > - Top 10 Launches -

+ >

@@ -62,15 +58,15 @@ const PromoToast = () => { Dismiss
- + /> */}
) } From 9227e83e5674482dc1bd2fa629921f1741924ad0 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Fri, 25 Apr 2025 06:37:13 +1200 Subject: [PATCH 04/86] chore: update cli reference doc (#35258) --- apps/docs/spec/cli_v1_commands.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index e8781085ddb..47e47d7fa86 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 2.22.4 + version: 2.22.6 title: Supabase CLI language: sh source: https://github.com/supabase/cli From 9d9fdcd9fcaaaee5e2ddac4d2b0e3435ebd03845 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Thu, 24 Apr 2025 14:59:38 -0400 Subject: [PATCH 05/86] feat (docs): on-demand revalidation for graphql and wrappers (#35177) Before: Revalidation was turned off entirely for wrappers pages, and done daily for GraphQL pages. After: Revalidation is via tag. There is an /api/revalidate endpoint that is used by GitHub Actions on the federated repos to trigger the revalidation. This decreases the number of unnecessary page generations by Vercel, and gives the maintainers of federated repos more fine-grained control over when to reploy their docs (see the docs deployment playbook for more info). --- apps/docs/app/api/revalidate/route.test.ts | 19 +++++++++++-------- apps/docs/app/api/revalidate/route.ts | 10 ++++------ .../extensions/wrappers/[[...slug]]/page.tsx | 6 ++++-- .../app/guides/graphql/[[...slug]]/page.tsx | 7 ++++--- apps/docs/features/helpers.fetch.ts | 11 +++++++++++ 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/apps/docs/app/api/revalidate/route.test.ts b/apps/docs/app/api/revalidate/route.test.ts index befeabd43e7..66a30304d41 100644 --- a/apps/docs/app/api/revalidate/route.test.ts +++ b/apps/docs/app/api/revalidate/route.test.ts @@ -40,6 +40,9 @@ describe('_handleRevalidateRequest', () => { process.env.NEXT_PUBLIC_SUPABASE_URL = 'http://localhost:3000' process.env.SUPABASE_SECRET_KEY = 'secret_key' + // Silence intentional console errors for cleaner test output + vi.spyOn(console, 'error').mockImplementation(() => {}) + // Mock current date mockDate = new Date('2023-01-01T12:00:00Z') vi.setSystemTime(mockDate) @@ -109,7 +112,7 @@ describe('_handleRevalidateRequest', () => { headers: { Authorization: 'Bearer basic_key', }, - body: JSON.stringify({ tags: ['tag1', 'tag2'] }), + body: JSON.stringify({ tags: ['graphql', 'wrappers'] }), }) vi.mocked(headers).mockReturnValue(new Headers(request.headers)) @@ -119,8 +122,8 @@ describe('_handleRevalidateRequest', () => { const response = await _handleRevalidateRequest(request) expect(response.status).toBe(204) expect(revalidateTag).toHaveBeenCalledTimes(2) - expect(revalidateTag).toHaveBeenCalledWith('tag1') - expect(revalidateTag).toHaveBeenCalledWith('tag2') + expect(revalidateTag).toHaveBeenCalledWith('graphql') + expect(revalidateTag).toHaveBeenCalledWith('wrappers') }) it('should return 429 if last revalidation was less than 6 hours ago with basic permissions', async () => { @@ -129,7 +132,7 @@ describe('_handleRevalidateRequest', () => { headers: { Authorization: 'Bearer basic_key', }, - body: JSON.stringify({ tags: ['tag1'] }), + body: JSON.stringify({ tags: ['graphql'] }), }) vi.mocked(headers).mockReturnValue(new Headers(request.headers)) @@ -150,7 +153,7 @@ describe('_handleRevalidateRequest', () => { headers: { Authorization: 'Bearer basic_key', }, - body: JSON.stringify({ tags: ['tag1'] }), + body: JSON.stringify({ tags: ['graphql'] }), }) vi.mocked(headers).mockReturnValue(new Headers(request.headers)) @@ -162,7 +165,7 @@ describe('_handleRevalidateRequest', () => { const response = await _handleRevalidateRequest(request) expect(response.status).toBe(204) - expect(revalidateTag).toHaveBeenCalledWith('tag1') + expect(revalidateTag).toHaveBeenCalledWith('graphql') }) it('should revalidate regardless of last revalidation time with override permissions', async () => { @@ -171,7 +174,7 @@ describe('_handleRevalidateRequest', () => { headers: { Authorization: 'Bearer override_key', }, - body: JSON.stringify({ tags: ['tag1'] }), + body: JSON.stringify({ tags: ['graphql'] }), }) vi.mocked(headers).mockReturnValue(new Headers(request.headers)) @@ -183,6 +186,6 @@ describe('_handleRevalidateRequest', () => { const response = await _handleRevalidateRequest(request) expect(response.status).toBe(204) - expect(revalidateTag).toHaveBeenCalledWith('tag1') + expect(revalidateTag).toHaveBeenCalledWith('graphql') }) }) diff --git a/apps/docs/app/api/revalidate/route.ts b/apps/docs/app/api/revalidate/route.ts index 1c751fb6512..86b033e4767 100644 --- a/apps/docs/app/api/revalidate/route.ts +++ b/apps/docs/app/api/revalidate/route.ts @@ -1,10 +1,10 @@ import { createClient } from '@supabase/supabase-js' +import { type Database } from 'common' import { revalidateTag } from 'next/cache' import { headers } from 'next/headers' import { type NextRequest } from 'next/server' import { z } from 'zod' - -import { type Database } from 'common' +import { VALID_REVALIDATION_TAGS } from '~/features/helpers.fetch' enum AuthorizationLevel { Unauthorized, @@ -13,7 +13,7 @@ enum AuthorizationLevel { } const requestBodySchema = z.object({ - tags: z.array(z.string()), + tags: z.array(z.enum(VALID_REVALIDATION_TAGS)), }) export const POST = handleError(_handleRevalidateRequest) @@ -35,14 +35,12 @@ export async function _handleRevalidateRequest(request: NextRequest) { } let authorizationLevel = AuthorizationLevel.Unauthorized - - const token = authorization.replace(/^Bearer\s+/, '') + const token = authorization.replace(/^Bearer /, '') if (overrideKeys.includes(token)) { authorizationLevel = AuthorizationLevel.Override } else if (basicKeys.includes(token)) { authorizationLevel = AuthorizationLevel.Basic } - if (authorizationLevel === AuthorizationLevel.Unauthorized) { return new Response('Invalid Authorization header', { status: 401 }) } diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx index 76c8c71bf48..0bbbee54595 100644 --- a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -11,12 +11,12 @@ import { removeRedundantH1, } from '~/features/docs/GuidesMdx.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' -import { fetchRevalidatePerDay } from '~/features/helpers.fetch' import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs' import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' +import { REVALIDATION_TAGS } from '~/features/helpers.fetch' export const dynamicParams = false @@ -196,7 +196,9 @@ const getContent = async (params: Params) => { const repoPath = `${org}/${repo}/${branch}/${docsDir}/${remoteFile}` editLink = `${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}` - const response = await fetchRevalidatePerDay(`https://raw.githubusercontent.com/${repoPath}`) + const response = await fetch(`https://raw.githubusercontent.com/${repoPath}`, { + next: { tags: [REVALIDATION_TAGS.WRAPPERS] }, + }) const rawContent = await response.text() const { content: contentWithoutFrontmatter } = matter(rawContent) diff --git a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx index fbf0d930735..69ba3cf793b 100644 --- a/apps/docs/app/guides/graphql/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/graphql/[[...slug]]/page.tsx @@ -5,11 +5,11 @@ import rehypeSlug from 'rehype-slug' import { genGuideMeta } from '~/features/docs/GuidesMdx.utils' import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' -import { fetchRevalidatePerDay_TEMP_TESTING } from '~/features/helpers.fetch' import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' +import { REVALIDATION_TAGS } from '~/features/helpers.fetch' export const dynamicParams = false @@ -135,8 +135,9 @@ const getContent = async ({ slug }: Params) => { const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`) - const response = await fetchRevalidatePerDay_TEMP_TESTING( - `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}` + const response = await fetch( + `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`, + { next: { tags: [REVALIDATION_TAGS.GRAPHQL] } } ) const content = await response.text() diff --git a/apps/docs/features/helpers.fetch.ts b/apps/docs/features/helpers.fetch.ts index 494e0444fe4..1275a4817c1 100644 --- a/apps/docs/features/helpers.fetch.ts +++ b/apps/docs/features/helpers.fetch.ts @@ -8,6 +8,17 @@ import { ONE_DAY_IN_SECONDS } from './helpers.time' +export const REVALIDATION_TAGS = { + GRAPHQL: 'graphql', + WRAPPERS: 'wrappers', +} as const +// Casting to avoid problems with using this as a Zod enum, TypeScript does +// not recognize the casted type as a supertype of the original type +export const VALID_REVALIDATION_TAGS = Object.values(REVALIDATION_TAGS) as unknown as readonly [ + string, + ...string[], +] + function fetchWithNextOptions({ next, cache, From 9846052217ef739012beef67edbd9f13390e1409 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Thu, 24 Apr 2025 15:04:03 -0400 Subject: [PATCH 06/86] build (docs): lower docs preview build times (#35183) An experiment in lowering docs preview build times. Build times have gotten quite long again (~12 minutes) because we are generating types on code blocks during prerendering. This wastes compute time building a lot of pages on preview branches that never get viewed, and slows down iteration speed. Instead, let's only prerender on prod, and build on-demand for preview sites. Limiting to realtime section for now to monitor the effects of changes. --- .../app/guides/realtime/[[...slug]]/page.tsx | 16 ++++++++++------ apps/docs/features/helpers.fn.ts | 10 ++++++++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/docs/app/guides/realtime/[[...slug]]/page.tsx b/apps/docs/app/guides/realtime/[[...slug]]/page.tsx index 7a19de3cd7b..ef9cd4969f9 100644 --- a/apps/docs/app/guides/realtime/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/realtime/[[...slug]]/page.tsx @@ -1,11 +1,10 @@ +import { GuideTemplate } from '~/features/docs/GuidesMdx.template' import { - getGuidesMarkdown, genGuideMeta, genGuidesStaticParams, + getGuidesMarkdown, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate } from '~/features/docs/GuidesMdx.template' - -export const dynamicParams = false +import { getEmptyArray } from '~/features/helpers.fn' type Params = { slug?: string[] } @@ -16,10 +15,15 @@ const RealtimeGuidePage = async ({ params }: { params: Params }) => { return } -const generateStaticParams = genGuidesStaticParams('realtime') +const generateStaticParams = + // [Charis 2025-04-21] For some reason, using the IS_PROD export from common + // does not work (its type is a function when this is evaluated?) + process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' + ? genGuidesStaticParams('realtime') + : getEmptyArray const generateMetadata = genGuideMeta((params: { slug?: string[] }) => getGuidesMarkdown(['realtime', ...(params.slug ?? [])]) ) export default RealtimeGuidePage -export { generateStaticParams, generateMetadata } +export { generateMetadata, generateStaticParams } diff --git a/apps/docs/features/helpers.fn.ts b/apps/docs/features/helpers.fn.ts index 8d5113a7030..8f50cc1c243 100644 --- a/apps/docs/features/helpers.fn.ts +++ b/apps/docs/features/helpers.fn.ts @@ -1,5 +1,10 @@ type RecObj = T[K] extends Array ? T : never +const EMPTY_ARRAY = new Array(0) +export function getEmptyArray() { + return EMPTY_ARRAY +} + export function deepFilterRec( arr: Array>, recKey: K, @@ -24,6 +29,7 @@ export function deepFilterRec( ) } -export function pluckPromise(promise: Promise, key: K) { - return promise.then((data) => data[key]) +export async function pluckPromise(promise: Promise, key: K) { + const data = await promise + return data[key] } From 1d2418ee8583b7e07d2929d4a695e125c11d7177 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Thu, 24 Apr 2025 22:26:19 +0200 Subject: [PATCH 07/86] chore: Skip building of `libpg-query` (#35255) * Don't build libpg-query package. * Use the WASM build of libpg-query in ai-commands. * Fix missing mdast type. * fix: wasm loading in ai-commands tests * fix: better polyfill * Make the sql-to-rest page to be client-side only. --------- Co-authored-by: Greg Richardson --- package.json | 3 ++- packages/ai-commands/package.json | 1 + packages/ai-commands/test/sql-util.ts | 2 +- packages/ai-commands/test/util.ts | 2 +- packages/ai-commands/tsconfig.json | 1 + packages/ai-commands/vitest.setup.ts | 24 ++++++++++++++++++++++++ packages/ui-patterns/SqlToRest/index.tsx | 2 +- pnpm-lock.yaml | 14 ++++++++------ 8 files changed, 39 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e9d85ca7e2d..6bb693c1850 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "vinxi>esbuild": "0.25.2", "@tanstack/directive-functions-plugin>vite": "6.2.6", "@tanstack/react-start-plugin>vite": "6.2.6" - } + }, + "neverBuiltDependencies": ["libpg-query"] }, "engines": { "pnpm": ">=9", diff --git a/packages/ai-commands/package.json b/packages/ai-commands/package.json index 0a00ce7590e..fdbc75ef0db 100644 --- a/packages/ai-commands/package.json +++ b/packages/ai-commands/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@ai-sdk/openai": "^0.0.72", "@types/common-tags": "^1.8.4", + "@types/mdast": "^4.0.0", "@types/node": "catalog:", "api-types": "workspace:*", "chalk": "^5.3.0", diff --git a/packages/ai-commands/test/sql-util.ts b/packages/ai-commands/test/sql-util.ts index d9c5eea6c66..6338685d734 100644 --- a/packages/ai-commands/test/sql-util.ts +++ b/packages/ai-commands/test/sql-util.ts @@ -1,4 +1,4 @@ -import { A_Const, A_Expr, ColumnRef, CreatePolicyStmt, Node, parseQuery } from 'libpg-query' +import { A_Const, A_Expr, ColumnRef, CreatePolicyStmt, Node, parseQuery } from 'libpg-query/wasm' export type PolicyInfo = { name: string diff --git a/packages/ai-commands/test/util.ts b/packages/ai-commands/test/util.ts index 6a911fcc032..b73bba0834c 100644 --- a/packages/ai-commands/test/util.ts +++ b/packages/ai-commands/test/util.ts @@ -1,6 +1,6 @@ import chalk from 'chalk' +import type { Code } from 'mdast' import { fromMarkdown } from 'mdast-util-from-markdown' -import type { Code } from 'mdast-util-from-markdown/lib' import { format } from 'sql-formatter' declare global { diff --git a/packages/ai-commands/tsconfig.json b/packages/ai-commands/tsconfig.json index f612f0c8a27..825ae2a5a01 100644 --- a/packages/ai-commands/tsconfig.json +++ b/packages/ai-commands/tsconfig.json @@ -3,6 +3,7 @@ "include": ["."], "exclude": ["dist", "build", "node_modules"], "compilerOptions": { + "moduleResolution": "bundler", "types": ["./test/extensions.d.ts"] } } diff --git a/packages/ai-commands/vitest.setup.ts b/packages/ai-commands/vitest.setup.ts index 1b83069042e..b8e0e2e20d8 100644 --- a/packages/ai-commands/vitest.setup.ts +++ b/packages/ai-commands/vitest.setup.ts @@ -9,3 +9,27 @@ if (!process.env.CI) { statSync(envPath) config({ path: envPath }) } + +// Modify fetch to support wasm file URLs +globalThis.fetch = async (input: string | URL | Request, init?: RequestInit) => { + if (input instanceof Request) { + return fetch(input, init) + } + + const url = new URL(input) + if (url.protocol === 'file:' && url.pathname.endsWith('.wasm')) { + return fetchFileNode(input, 'application/wasm') + } + + return fetch(input, init) +} + +async function fetchFileNode(input: string | URL, type: string) { + const fs = await import('node:fs') + const { Readable } = await import('node:stream') + const { fileURLToPath } = await import('node:url') + const path = fileURLToPath(input) + const nodeStream = fs.createReadStream(path) + const stream = Readable.toWeb(nodeStream) as ReadableStream + return new Response(stream, { headers: { 'Content-Type': type } }) +} diff --git a/packages/ui-patterns/SqlToRest/index.tsx b/packages/ui-patterns/SqlToRest/index.tsx index 1e83262f392..75e91e42d22 100644 --- a/packages/ui-patterns/SqlToRest/index.tsx +++ b/packages/ui-patterns/SqlToRest/index.tsx @@ -25,7 +25,7 @@ function FallbackComponent({ error }: FallbackProps) { } // Lazy load client side to prevent hydration issues when browser produces an error -const SqlToRest = dynamic(() => import('./sql-to-rest')) +const SqlToRest = dynamic(() => import('./sql-to-rest'), { ssr: false }) export default function SqlToRestWithFallback(props: SqlToRestProps) { return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2afa5c9f27..480833f2069 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1496,6 +1496,9 @@ importers: '@types/common-tags': specifier: ^1.8.4 version: 1.8.4 + '@types/mdast': + specifier: ^4.0.0 + version: 4.0.1 '@types/node': specifier: 'catalog:' version: 20.12.11 @@ -1613,7 +1616,7 @@ importers: dependencies: '@mertasan/tailwindcss-variables': specifier: ^2.2.3 - version: 2.7.0(autoprefixer@10.4.16(postcss@8.5.3))(postcss@8.5.3) + version: 2.7.0(autoprefixer@10.4.16(postcss@8.4.38))(postcss@8.4.38) '@radix-ui/colors': specifier: ^0.1.8 version: 0.1.9 @@ -8669,7 +8672,6 @@ packages: resolution: {integrity: sha512-t0q23FIpvHDTtnORW+bDJziGsal5uh9RJTJ1fyH8drd4lICOoXhJ5pLMUZ5C0VQei6dNmwTzzoTRgMkO9JgHEQ==} peerDependencies: eslint: '>= 5' - bundledDependencies: [] eslint-plugin-import@2.29.1: resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} @@ -16596,11 +16598,11 @@ snapshots: '@types/react': 18.3.3 react: 18.3.1 - '@mertasan/tailwindcss-variables@2.7.0(autoprefixer@10.4.16(postcss@8.5.3))(postcss@8.5.3)': + '@mertasan/tailwindcss-variables@2.7.0(autoprefixer@10.4.16(postcss@8.4.38))(postcss@8.4.38)': dependencies: - autoprefixer: 10.4.16(postcss@8.5.3) + autoprefixer: 10.4.16(postcss@8.4.38) lodash: 4.17.21 - postcss: 8.5.3 + postcss: 8.4.38 '@mjackson/node-fetch-server@0.2.0': {} @@ -20501,7 +20503,7 @@ snapshots: '@types/mdast@4.0.1': dependencies: - '@types/unist': 3.0.3 + '@types/unist': 2.0.8 '@types/mdx-js__react@1.5.6': dependencies: From 8e7533b8c9c2e24dc577bc3cd8ec485a1a19b97a Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 25 Apr 2025 10:40:14 +0700 Subject: [PATCH 08/86] Remove [contain:layout] class in ProjectLayout to fix tabs not following cursor position (#35212) --- apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index acd0d43da17..da348ff170e 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -208,7 +208,7 @@ const ProjectLayout = forwardRef
{showPausedState ? ( From ff653107cf11243178d16480d449f26bee2ddab0 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 25 Apr 2025 13:59:05 +0700 Subject: [PATCH 09/86] Remove isValidating check on custom domain cancel action (#35259) --- .../Settings/General/CustomDomainConfig/CustomDomainVerify.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx index c8cd2f5ca18..9d97d93a157 100644 --- a/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx +++ b/apps/studio/components/interfaces/Settings/General/CustomDomainConfig/CustomDomainVerify.tsx @@ -189,7 +189,6 @@ const CustomDomainVerify = ({ customDomain }: CustomDomainVerifyProps) => { type="default" onClick={onCancelCustomDomain} loading={isDeleting} - disabled={isValidating} className="self-end" > Cancel From 98ae279a2bcd4557b39db3209828faa949323c37 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Fri, 25 Apr 2025 17:11:28 +1000 Subject: [PATCH 10/86] Add's a filter bar component to design system (#35238) * add filter bar component * update filter bar demo * add custom component support * filter bar component * example filter bar in use * remove search filter * reinstate older filter bar * revert * update registry * update example name * clean up example imports * Update FilterBar.tsx --------- Co-authored-by: Jonathan Summers-Muir --- apps/design-system/__registry__/index.tsx | 11 + apps/design-system/config/docs.ts | 5 + .../content/docs/fragments/filter-bar.mdx | 214 ++++ apps/design-system/public/llms.txt | 6 +- .../default/example/filter-bar-demo.tsx | 166 +++ apps/design-system/registry/examples.ts | 6 + .../Settings/Logs/LogsPreviewer.tsx | 75 +- .../Logs/PreviewFilterPanelWithUniversal.tsx | 381 ++++++ packages/ui-patterns/FilterBar/FilterBar.tsx | 1044 +++++++++++++++++ .../ui-patterns/FilterBar/FilterCondition.tsx | 86 ++ .../ui-patterns/FilterBar/FilterGroup.tsx | 199 ++++ packages/ui-patterns/FilterBar/index.ts | 2 + packages/ui-patterns/FilterBar/types.ts | 45 + packages/ui-patterns/index.tsx | 1 + pnpm-lock.yaml | 6 +- 15 files changed, 2212 insertions(+), 35 deletions(-) create mode 100644 apps/design-system/content/docs/fragments/filter-bar.mdx create mode 100644 apps/design-system/registry/default/example/filter-bar-demo.tsx create mode 100644 apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanelWithUniversal.tsx create mode 100644 packages/ui-patterns/FilterBar/FilterBar.tsx create mode 100644 packages/ui-patterns/FilterBar/FilterCondition.tsx create mode 100644 packages/ui-patterns/FilterBar/FilterGroup.tsx create mode 100644 packages/ui-patterns/FilterBar/index.ts create mode 100644 packages/ui-patterns/FilterBar/types.ts diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index d4554da3efa..71d36ee0000 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -720,6 +720,17 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "filter-bar-demo": { + name: "filter-bar-demo", + type: "components:example", + registryDependencies: ["filter-bar"], + component: React.lazy(() => import("@/registry/default/example/filter-bar-demo")), + source: "", + files: ["registry/default/example/filter-bar-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "hover-card-demo": { name: "hover-card-demo", type: "components:example", diff --git a/apps/design-system/config/docs.ts b/apps/design-system/config/docs.ts index 1031738338e..e3bda291b96 100644 --- a/apps/design-system/config/docs.ts +++ b/apps/design-system/config/docs.ts @@ -117,6 +117,11 @@ export const docsConfig: DocsConfig = { href: '/docs/fragments/multi-select', items: [], }, + { + title: 'Filter Bar', + href: '/docs/fragments/filter-bar', + items: [], + }, { title: 'Logs Bar Chart', href: '/docs/fragments/logs-bar-chart', diff --git a/apps/design-system/content/docs/fragments/filter-bar.mdx b/apps/design-system/content/docs/fragments/filter-bar.mdx new file mode 100644 index 00000000000..fed27c7219d --- /dev/null +++ b/apps/design-system/content/docs/fragments/filter-bar.mdx @@ -0,0 +1,214 @@ +--- +title: Filter Bar +description: An advanced filtering component with support for multiple conditions and operators. +component: true +fragment: true +--- + + + +## Usage + +The Filter Bar component provides advanced filtering capabilities with support for multiple conditions, operators, and different field types. It can be used with both static and async options. + +```tsx +const filterProperties = [ + { + label: 'Name', + name: 'name', + type: 'string', + operators: ['=', '!=', 'CONTAINS', 'STARTS WITH', 'ENDS WITH'], + }, + { + label: 'Status', + name: 'status', + type: 'string', + options: ['active', 'inactive', 'pending'], + operators: ['=', '!='], + }, +] + +export function FilterDemo() { + const [filters, setFilters] = useState(initialFilters) + const [freeformText, setFreeformText] = useState('') + + return ( + + ) +} +``` + +## API Reference + +### FilterProperty + +```tsx +interface FilterProperty { + label: string + name: string + type: 'string' | 'number' | 'date' | 'boolean' + operators: string[] + options?: string[] | ((search?: string) => Promise | string[]) +} +``` + +### FilterGroup + +```tsx +interface FilterGroup { + logicalOperator: 'AND' | 'OR' + conditions: (FilterCondition | FilterGroup)[] +} +``` + +### FilterCondition + +```tsx +interface FilterCondition { + propertyName: string + value: string | number | boolean | Date + operator: string +} +``` + +### Component Props + +| Prop | Type | Description | +| -------------------- | ------------------------------ | ---------------------------------------- | +| filterProperties | FilterProperty[] | Array of properties that can be filtered | +| filters | FilterGroup | Current filter state | +| onFilterChange | (filters: FilterGroup) => void | Callback when filters change | +| freeformText | string | Current free-form search text | +| onFreeformTextChange | (text: string) => void | Callback when free-form text changes | +| aiApiUrl | string? | Optional URL for AI-powered filtering | + +## AI Integration + +The Filter Bar component supports AI-powered filtering through an optional API endpoint. When `aiApiUrl` is provided, the component will send natural language queries to be converted into structured filters. + +### API Endpoint + +The AI API endpoint should accept POST requests with the following structure: + +```typescript +// Request body +interface AIFilterRequest { + prompt: string // Natural language query + filterProperties: FilterProperty[] // Available filter properties +} + +// Response body +interface AIFilterResponse { + logicalOperator: 'AND' | 'OR' + conditions: (FilterCondition | FilterGroup)[] +} +``` + +### Example API Implementation + +```typescript +import { generateObject } from 'ai' +import { openai } from '@ai-sdk/openai' +import { z } from 'zod' + +// Define schemas for validation +const FilterProperty = z.object({ + label: z.string(), + name: z.string(), + type: z.enum(['string', 'number', 'date', 'boolean']), + options: z.array(z.string()).optional(), + operators: z.array(z.string()).optional(), +}) + +const FilterCondition = z.object({ + propertyName: z.string(), + value: z.union([z.string(), z.number(), z.boolean(), z.null()]), + operator: z.string(), +}) + +type FilterGroupType = { + logicalOperator: 'AND' | 'OR' + conditions: Array | FilterGroupType> +} + +const FilterGroup: z.ZodType = z.lazy(() => + z.object({ + logicalOperator: z.enum(['AND', 'OR']), + conditions: z.array(z.union([FilterCondition, FilterGroup])), + }) +) + +export async function POST(req: Request) { + const { prompt, filterProperties } = await req.json() + const filterPropertiesString = JSON.stringify(filterProperties) + + try { + const { object } = await generateObject({ + model: openai('gpt-4-mini'), + schema: FilterGroup, + prompt: `Generate a filter group based on the following prompt: "${prompt}". + Use only these filter properties: ${filterPropertiesString}. + Each property has its own set of valid operators defined in the operators field. + Return a filter group with a logical operator ('AND'/'OR') and an array of conditions. + Each condition can be either a filter condition or another filter group. + Filter conditions should have the structure: { propertyName: string, value: string | number | boolean | null, operator: string }. + Ensure that the generated filters use only the provided property names and their corresponding operators.`, + }) + + // Validate that all propertyNames exist in filterProperties + const validatePropertyNames = (group: FilterGroupType): boolean => { + return group.conditions.every((condition) => { + if ('logicalOperator' in condition) { + return validatePropertyNames(condition as FilterGroupType) + } + const property = filterProperties.find( + (p: z.infer) => p.name === condition.propertyName + ) + if (!property) return false + // Validate operator is valid for this property + return property.operators?.includes(condition.operator) ?? false + }) + } + + if (!validatePropertyNames(object)) { + throw new Error('Invalid property names or operators in generated filter') + } + + // Zod will throw an error if the object doesn't match the schema + const validatedFilters = FilterGroup.parse(object) + return Response.json(validatedFilters) + } catch (error: any) { + console.error('Error in AI filtering:', error) + return Response.json({ error: error.message || 'AI filtering failed' }, { status: 500 }) + } +} +``` + +### Usage with AI + +```tsx +export function FilterDemoWithAI() { + const [filters, setFilters] = useState(initialFilters) + + return ( + + ) +} +``` diff --git a/apps/design-system/public/llms.txt b/apps/design-system/public/llms.txt index 6d50fe9e7f0..34725799b8c 100644 --- a/apps/design-system/public/llms.txt +++ b/apps/design-system/public/llms.txt @@ -1,5 +1,5 @@ # Supabase Design System -Last updated: 2025-03-05T12:59:50.911Z +Last updated: 2025-04-24T06:00:07.374Z ## Overview The Design System used in Supabase Studio. @@ -130,6 +130,8 @@ The Design System used in Supabase Studio. - Displays a callout for user attention. - [Assistant Chat](https://supabase-design-system.vercel.app/design-system/docs/fragments/assistant-chat) - Bespoke Assistant chat text area, with support for a commands Popover. +- [Filter Bar](https://supabase-design-system.vercel.app/design-system/docs/fragments/filter-bar) + - An advanced filtering component with support for multiple conditions and operators. - [Form Item Layout](https://supabase-design-system.vercel.app/design-system/docs/fragments/form-item-layout) - A helper component that provides a layout for form items. - [Info Tooltip](https://supabase-design-system.vercel.app/design-system/docs/fragments/info-tooltip) @@ -144,6 +146,8 @@ The Design System used in Supabase Studio. - Multiple selection component. - [Text Confirm Dialog](https://supabase-design-system.vercel.app/design-system/docs/fragments/text-confirm-dialog) - A modal dialog that interrupts the user with important content and expects a response. +- [Table of Contents (TOC)](https://supabase-design-system.vercel.app/design-system/docs/fragments/toc) + - List of page anchors for the current page. - [Icons](https://supabase-design-system.vercel.app/design-system/docs/icons) - Icons system breakdown. Copy values of Icons. - [Introduction](https://supabase-design-system.vercel.app/design-system/docs/index) diff --git a/apps/design-system/registry/default/example/filter-bar-demo.tsx b/apps/design-system/registry/default/example/filter-bar-demo.tsx new file mode 100644 index 00000000000..06ccabdc71e --- /dev/null +++ b/apps/design-system/registry/default/example/filter-bar-demo.tsx @@ -0,0 +1,166 @@ +import { format } from 'date-fns' +import { useState } from 'react' +import { Button, Button_Shadcn_, Calendar, Input_Shadcn_ } from 'ui' +import { CustomOptionProps, FilterBar, FilterGroup } from 'ui-patterns' + +function CustomDatePicker({ onChange, onCancel, search }: CustomOptionProps) { + const [date, setDate] = useState( + search + ? { + from: new Date(search), + to: undefined, + } + : undefined + ) + + return ( +
+ +
+ + +
+
+ ) +} + +function CustomTimePicker({ onChange, onCancel, search }: CustomOptionProps) { + const [time, setTime] = useState(search || '') + + return ( +
+

Select Time

+ setTime(e.target.value)} /> +
+ + Cancel + + onChange(time)}>Apply +
+
+ ) +} + +function CustomRangePicker({ onChange, onCancel, search }: CustomOptionProps) { + const [range, setRange] = useState(search || '') + + return ( +
+

Select Range

+ setRange(e.target.value)} /> +
+ + Cancel + + onChange(range)}>Apply +
+
+ ) +} + +const filterProperties = [ + { + label: 'Name', + name: 'name', + type: 'string' as const, + operators: ['=', '!=', 'CONTAINS', 'STARTS WITH', 'ENDS WITH'], + }, + { + label: 'Status', + name: 'status', + type: 'string' as const, + options: [ + { label: 'Active', value: 'active' }, + { label: 'Inactive', value: 'inactive' }, + { label: 'Pending', value: 'pending' }, + ], + operators: ['=', '!='], + }, + { + label: 'Type', + name: 'type', + type: 'string' as const, + options: async (search?: string) => { + await new Promise((resolve) => setTimeout(resolve, 500)) + const allOptions = ['user', 'admin', 'guest'] + return search + ? allOptions.filter((option) => option.toLowerCase().includes(search.toLowerCase())) + : allOptions + }, + operators: ['=', '!='], + }, + { + label: 'Time period', + name: 'created_at', + type: 'date' as const, + options: [ + { label: 'Today', value: format(new Date(), 'yyyy-MM-dd') }, + { label: 'Yesterday', value: format(new Date(Date.now() - 86400000), 'yyyy-MM-dd') }, + { label: 'Last 7 days', value: format(new Date(Date.now() - 7 * 86400000), 'yyyy-MM-dd') }, + { label: 'Last 30 days', value: format(new Date(Date.now() - 30 * 86400000), 'yyyy-MM-dd') }, + { + label: 'Pick a date...', + component: (props: CustomOptionProps) => , + }, + ], + operators: ['=', '!=', '>', '<', '>=', '<='], + }, + { + label: 'Priority', + name: 'priority', + type: 'number' as const, + options: { + component: (props: CustomOptionProps) => ( +
+ +
+ ), + }, + triggerOnPropertyClick: true, + operators: ['=', '!=', '>', '<', '>=', '<='], + }, +] + +const initialFilters: FilterGroup = { + logicalOperator: 'AND', + conditions: [], +} + +export default function FilterBarDemo() { + const [filters, setFilters] = useState(initialFilters) + const [freeformText, setFreeformText] = useState('') + + return ( +
+ +
+ ) +} diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index 2198f2527af..b79b6322d44 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -467,6 +467,12 @@ export const examples: Registry = [ registryDependencies: ['dropdown-menu', 'radio-group'], files: ['example/dropdown-menu-radio-group.tsx'], }, + { + name: 'filter-bar-demo', + type: 'components:example', + registryDependencies: ['filter-bar'], + files: ['example/filter-bar-demo.tsx'], + }, { name: 'hover-card-demo', type: 'components:example', diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx index 76f8b95560c..cdd3a78d9ce 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx @@ -28,6 +28,8 @@ import { import type { Filters, LogSearchCallback, LogTemplate, QueryType } from './Logs.types' import { maybeShowUpgradePrompt } from './Logs.utils' import UpgradePrompt from './UpgradePrompt' +import { useFlag } from 'hooks/ui/useFlag' +import { PreviewFilterPanelWithUniversal } from './PreviewFilterPanelWithUniversal' /** * Acts as a container component for the entire log display @@ -58,6 +60,8 @@ export const LogsPreviewer = ({ EmptyState, filterPanelClassName, }: PropsWithChildren) => { + const useUniversalFilterBar = useFlag('universalFilterBar') + const router = useRouter() const { db } = useParams() const organization = useSelectedOrganization() @@ -186,39 +190,48 @@ export const LogsPreviewer = ({ } }, [db, isSuccess]) + // Common props shared between both filter panel components to avoid duplication + const filterPanelProps = { + className: filterPanelClassName, + csvData: logData, + isLoading, + newCount, + onRefresh: handleRefresh, + onSearch: handleSearch, + defaultSearchValue: search, + defaultToValue: timestampEnd, + defaultFromValue: timestampStart, + queryUrl: `/project/${projectRef}/logs/explorer?q=${encodeURIComponent( + params.sql || '' + )}&its=${encodeURIComponent(timestampStart)}&ite=${encodeURIComponent(timestampEnd)}`, + onSelectTemplate, + filters, + onFiltersChange: setFilters, + table, + condensedLayout, + isShowingEventChart: showChart, + onToggleEventChart: () => setShowChart(!showChart), + onSelectedDatabaseChange: (id: string) => { + setFilters({ ...filters, database: id !== projectRef ? id : undefined }) + const { db, ...params } = router.query + router.push({ + pathname: router.pathname, + query: id !== projectRef ? { ...router.query, db: id } : params, + }) + }, + selectedDatePickerValue, + setSelectedDatePickerValue, + } + return (
- setShowChart(!showChart)} - onSelectedDatabaseChange={(id: string) => { - setFilters({ ...filters, database: id !== projectRef ? id : undefined }) - const { db, ...params } = router.query - router.push({ - pathname: router.pathname, - query: id !== projectRef ? { ...router.query, db: id } : params, - }) - }} - selectedDatePickerValue={selectedDatePickerValue} - setSelectedDatePickerValue={setSelectedDatePickerValue} - /> + {useUniversalFilterBar ? ( + // Experimental Universal Filter Bar + + ) : ( + // Legacy Filter Panel + + )} {children}
void +} + +function CustomDateRangePicker({ onChange, onCancel }: CustomOptionProps) { + const [dateRange, setDateRange] = useState() + + return ( +
+ +
+ + +
+
+ ) +} + +interface PreviewFilterPanelProps { + defaultSearchValue?: string + defaultToValue?: string + defaultFromValue?: string + templates?: any + isLoading: boolean + newCount: number + onRefresh?: () => void + onSearch?: LogSearchCallback + onExploreClick?: () => void + queryUrl: string + onSelectTemplate: (template: LogTemplate) => void + table: LogsTableName + condensedLayout: Boolean + isShowingEventChart: boolean + onToggleEventChart: () => void + csvData?: unknown[] + onFiltersChange: (filters: Filters) => void + filters: Filters + onSelectedDatabaseChange: (id: string) => void + className?: string + selectedDatePickerValue: DatePickerValue + setSelectedDatePickerValue: (value: DatePickerValue) => void +} + +function useDebounce void>(callback: T, delay: number) { + const timeoutRef = useRef() + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + } + }, []) + + return useCallback( + (...args: Parameters) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + + timeoutRef.current = setTimeout(() => { + callback(...args) + }, delay) + }, + [callback, delay] + ) as T +} + +const PreviewFilterPanelWithUniversal = ({ + isLoading, + newCount, + onRefresh, + onSearch = () => {}, + defaultSearchValue = '', + defaultFromValue, + defaultToValue, + onExploreClick, + queryUrl, + condensedLayout, + isShowingEventChart, + onToggleEventChart, + csvData, + onFiltersChange, + filters, + table, + onSelectedDatabaseChange, + className, + selectedDatePickerValue, + setSelectedDatePickerValue, +}: PreviewFilterPanelProps) => { + const router = useRouter() + const { ref } = useParams() + + const logName = router.pathname.split('/').pop() + + const { data: loadBalancers } = useLoadBalancersQuery({ projectRef: ref }) + + const showDatabaseSelector = + IS_PLATFORM && LOG_ROUTES_WITH_REPLICA_SUPPORT.includes(router.pathname) + + const filterProperties = useMemo(() => { + const tableFilters = FILTER_OPTIONS[table] + if (!tableFilters) return [] + + const properties: FilterProperty[] = [] + + // Add date range filter + properties.push({ + label: 'Time Range', + name: 'timerange', + type: 'string' as const, + operators: ['='], + options: [ + { + label: 'Last 15 minutes', + value: `${dayjs().subtract(15, 'minute').toISOString()}|${dayjs().toISOString()}`, + }, + { + label: 'Last 30 minutes', + value: `${dayjs().subtract(30, 'minute').toISOString()}|${dayjs().toISOString()}`, + }, + { + label: 'Last hour', + value: `${dayjs().subtract(1, 'hour').toISOString()}|${dayjs().toISOString()}`, + }, + { + label: 'Last 3 hours', + value: `${dayjs().subtract(3, 'hour').toISOString()}|${dayjs().toISOString()}`, + }, + { + label: 'Last 24 hours', + value: `${dayjs().subtract(24, 'hour').toISOString()}|${dayjs().toISOString()}`, + }, + { + label: 'Custom Range...', + component: (props: CustomOptionProps) => , + }, + ], + }) + + // Add table-specific filters + Object.entries(tableFilters).forEach(([key, filterSet]) => { + properties.push({ + label: filterSet.label, + name: filterSet.key, + type: 'string' as const, + operators: ['='], + options: filterSet.options.map((option) => ({ + label: option.label, + value: option.key, + description: option.description, + })), + }) + }) + + return properties + }, [table]) + + // Convert Logs.Filters to FilterBar.FilterGroup + const filterBarFilters = useMemo(() => { + const conditions: FilterCondition[] = [] + + // Handle date range - only add from defaultValues if no timerange filter exists + if ((defaultFromValue || defaultToValue) && !filters.timerange) { + conditions.push({ + propertyName: 'timerange', + operator: '=', + value: `${defaultFromValue || ''}|${defaultToValue || ''}`, + }) + } + + // Convert other filters + Object.entries(filters).forEach(([key, value]) => { + if (key === 'search_query') return // Skip search_query as it's handled in freeform text + + if (typeof value === 'object' && value !== null) { + // If the filter object is empty, add a condition with no value + if (Object.keys(value).length === 0) { + conditions.push({ + propertyName: key, + operator: '=', + value: '', + }) + return + } + + // Handle active filters + Object.entries(value as Record).forEach(([subKey, isEnabled]) => { + if (isEnabled) { + conditions.push({ + propertyName: key, + operator: '=', + value: subKey, + }) + } + }) + } else if (value !== undefined && value !== null) { + conditions.push({ + propertyName: key, + operator: '=', + value: String(value), + }) + } + }) + + return { + logicalOperator: 'AND', + conditions, + } as FilterGroup + }, [filters, defaultFromValue, defaultToValue]) + + const handleFilterChange = (filterGroup: FilterGroup) => { + const newFilters: Filters = {} + let hasTimeRange = false + + filterGroup.conditions.forEach((condition) => { + if (!('propertyName' in condition)) return + + const propertyName = condition.propertyName + if (!propertyName) return + + if (propertyName === 'timerange') { + hasTimeRange = true + if (condition.value) { + const value = String(condition.value) + const [from, to] = value.split('|') + newFilters.timerange = { [value]: true } + onSearch('datepicker-change', { from, to }) + } else { + newFilters.timerange = {} + onSearch('datepicker-change', { from: '', to: '' }) + } + } else { + if (!newFilters[propertyName]) { + newFilters[propertyName] = {} + } + + if (!condition.value) { + return + } + + ;(newFilters[propertyName] as Record)[condition.value] = true + } + }) + + // If timerange was completely removed from the filter group + if (!hasTimeRange) { + onSearch('datepicker-change', { from: '', to: '' }) + } + + onFiltersChange(newFilters) + } + + return ( +
+
+
+ onSearch('search-input-change', { query: value })} + /> +
+ +
+
+ } + loading={isLoading} + disabled={isLoading} + onClick={onRefresh} + /> + + + + {/* */} + + {showDatabaseSelector ? ( + <> + + + + + + Open query in Logs Explorer + + + 0 + ? [{ id: `${ref}-all`, name: 'API Load Balancer' }] + : [] + : [] + } + /> + + ) : ( + + )} +
+
+
+ ) +} + +export { PreviewFilterPanelWithUniversal } diff --git a/packages/ui-patterns/FilterBar/FilterBar.tsx b/packages/ui-patterns/FilterBar/FilterBar.tsx new file mode 100644 index 00000000000..f1aa41910af --- /dev/null +++ b/packages/ui-patterns/FilterBar/FilterBar.tsx @@ -0,0 +1,1044 @@ +'use client' + +import React, { useRef, KeyboardEvent, useMemo, useState, useCallback, useEffect } from 'react' +import { Search, Sparkles } from 'lucide-react' +import { + Command_Shadcn_, + CommandEmpty_Shadcn_, + CommandGroup_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, + Dialog, + DialogContent, +} from 'ui' +import { FilterGroup as FilterGroupComponent } from './FilterGroup' +import { + FilterProperty, + FilterCondition, + FilterGroup, + CustomOptionProps, + FilterOption, + CustomOptionObject, + FilterOptionObject, + AsyncOptionsFunction, + SyncOptionsFunction, +} from './types' + +export type ActiveInput = + | { type: 'value'; path: number[] } + | { type: 'operator'; path: number[] } + | { type: 'group'; path: number[] } + | null + +type CommandItem = { + value: string + label: string + icon?: React.ReactNode + isCustom?: boolean + customOption?: (props: CustomOptionProps) => React.ReactElement +} + +function isCustomOptionObject(option: any): option is CustomOptionObject { + return typeof option === 'object' && 'component' in option +} + +function isFilterOptionObject(option: any): option is FilterOptionObject { + return typeof option === 'object' && 'value' in option && 'label' in option +} + +function isAsyncOptionsFunction( + options: FilterProperty['options'] +): options is AsyncOptionsFunction { + if (!options || Array.isArray(options) || isCustomOptionObject(options)) return false + return typeof options === 'function' && options.constructor.name === 'AsyncFunction' +} + +function isSyncOptionsFunction(options: FilterProperty['options']): options is SyncOptionsFunction { + if (!options || Array.isArray(options) || isCustomOptionObject(options)) return false + return typeof options === 'function' +} + +function isGroup(condition: FilterCondition | FilterGroup): condition is FilterGroup { + return 'logicalOperator' in condition +} + +export type FilterBarProps = { + filterProperties: FilterProperty[] + onFilterChange: (filters: FilterGroup) => void + freeformText: string + onFreeformTextChange: (freeformText: string) => void + filters: FilterGroup + aiApiUrl?: string +} + +export function FilterBar({ + filterProperties, + filters: activeFilters, + onFilterChange, + freeformText, + onFreeformTextChange, + aiApiUrl, +}: FilterBarProps) { + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const commandRef = useRef(null) + const [selectedCommandIndex, setSelectedCommandIndex] = useState(0) + const [isCommandMenuVisible, setIsCommandMenuVisible] = useState(false) + const hideTimeoutRef = useRef(null) + const [activeInput, setActiveInput] = useState(null) + const newPathRef = useRef([]) + const [loadingOptions, setLoadingOptions] = useState>({}) + const [propertyOptionsCache, setPropertyOptionsCache] = useState< + Record + >({}) + const loadTimeoutRef = useRef(null) + const [dialogContent, setDialogContent] = useState(null) + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [pendingPath, setPendingPath] = useState(null) + + const findGroupByPath = (group: FilterGroup, path: number[]): FilterGroup | null => { + if (path.length === 0) return group + + const [current, ...rest] = path + const condition = group.conditions[current] + if (!condition) return null + + if (rest.length === 0) { + return isGroup(condition) ? condition : null + } + + if (isGroup(condition)) { + return findGroupByPath(condition, rest) + } + + return null + } + + const findConditionByPath = (group: FilterGroup, path: number[]): FilterCondition | null => { + if (path.length === 0) return null + + const [current, ...rest] = path + const condition = group.conditions[current] + if (!condition) return null + + if (rest.length === 0) { + return isGroup(condition) ? null : condition + } + + if (isGroup(condition)) { + return findConditionByPath(condition, rest) + } + + return null + } + + const handleInputChange = useCallback( + (path: number[], value: string) => { + console.log('handleInputChange called with path:', path, 'value:', value) + const updateNestedValue = ( + group: FilterGroup, + currentPath: number[], + newValue: string + ): FilterGroup => { + if (currentPath.length === 1) { + console.log('Updating value at path level 1:', currentPath[0], 'newValue:', newValue) + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === currentPath[0] ? { ...condition, value: newValue } : condition + ), + } + } + + const [current, ...rest] = currentPath + console.log('Updating nested value at path:', current, 'remaining path:', rest) + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current + ? isGroup(condition) + ? updateNestedValue(condition, rest, newValue) + : condition + : condition + ), + } + } + + const updatedFilters = updateNestedValue(activeFilters, path, value) + console.log('Updated filters:', updatedFilters) + onFilterChange(updatedFilters) + setSelectedCommandIndex(0) + setIsCommandMenuVisible(true) + }, + [activeFilters] + ) + + const handleOperatorChange = useCallback( + (path: number[], value: string) => { + const updateNestedOperator = ( + group: FilterGroup, + currentPath: number[], + newOperator: string + ): FilterGroup => { + if (currentPath.length === 1) { + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === currentPath[0] ? { ...condition, operator: newOperator } : condition + ), + } + } + + const [current, ...rest] = currentPath + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current + ? isGroup(condition) + ? updateNestedOperator(condition, rest, newOperator) + : condition + : condition + ), + } + } + + const updatedFilters = updateNestedOperator(activeFilters, path, value) + onFilterChange(updatedFilters) + setSelectedCommandIndex(0) + setIsCommandMenuVisible(true) + }, + [activeFilters] + ) + + const handleInputFocus = useCallback((path: number[]) => { + setActiveInput({ type: 'value', path }) + setIsCommandMenuVisible(true) + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current) + } + }, []) + + const handleOperatorFocus = useCallback((path: number[]) => { + setActiveInput({ type: 'operator', path }) + setIsCommandMenuVisible(true) + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current) + } + }, []) + + const handleGroupFreeformFocus = useCallback((path: number[]) => { + setActiveInput({ type: 'group', path }) + setIsCommandMenuVisible(true) + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current) + } + }, []) + + const handleInputBlur = useCallback(() => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current) + } + hideTimeoutRef.current = setTimeout(() => { + setIsCommandMenuVisible(false) + setActiveInput(null) + }, 150) + }, []) + + const handleAIFilter = async () => { + if (!activeInput || activeInput.type !== 'group' || !aiApiUrl) return + + setIsLoading(true) + setError(null) + + try { + const response = await fetch(aiApiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + prompt: freeformText, + filterProperties, + currentPath: activeInput.path, + }), + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'AI filtering failed') + } + + const data = await response.json() + console.log(data) + if (!data || !Array.isArray(data.conditions)) { + throw new Error('Invalid response from AI filter') + } + + // Process the nested structure + const processConditions = (conditions: any[]): any[] => { + return conditions.map((condition) => { + if (isGroup(condition)) { + // This is a group + return { + logicalOperator: condition.logicalOperator, + conditions: processConditions(condition.conditions), + } + } else { + // This is a condition + const matchedProperty = filterProperties.find( + (prop) => prop.name === condition.propertyName + ) + if (!matchedProperty) { + throw new Error(`Invalid property: ${condition.propertyName}`) + } + return { + propertyName: matchedProperty.name, + value: condition.value, + operator: condition.operator || '=', + } + } + }) + } + + const processedGroup = { + logicalOperator: data.logicalOperator || 'AND', + conditions: processConditions(data.conditions), + } + + // Update the active filters by replacing the group at the current path + const updateGroupAtPath = ( + group: FilterGroup, + path: number[], + newGroup: FilterGroup + ): FilterGroup => { + if (path.length === 0) { + return newGroup + } + + const [current, ...rest] = path + return { + ...group, + conditions: group.conditions.map((condition, index) => + index === current + ? updateGroupAtPath(condition as FilterGroup, rest, newGroup) + : condition + ), + } + } + + const updatedFilters = updateGroupAtPath(activeFilters, activeInput.path, processedGroup) + onFilterChange(updatedFilters) + + onFreeformTextChange('') + setIsCommandMenuVisible(false) + } catch (error: any) { + console.error('Error in AI filtering:', error) + setError(error.message || 'AI filtering failed. Please try again.') + onFreeformTextChange('') + } finally { + setIsLoading(false) + } + } + + const handleGroupFreeformChange = useCallback((path: number[], value: string) => { + onFreeformTextChange(value) + setSelectedCommandIndex(0) + setIsCommandMenuVisible(true) + }, []) + + const addFilterToGroup = useCallback( + (group: FilterGroup, path: number[], property: FilterProperty): FilterGroup => { + if (path.length === 0) { + return { + ...group, + conditions: [ + ...group.conditions, + { propertyName: property.name, value: null, operator: '=' }, + ], + } + } + + const [current, ...rest] = path + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current ? addFilterToGroup(condition as FilterGroup, rest, property) : condition + ), + } + }, + [] + ) + + const addGroupToGroup = useCallback((group: FilterGroup, path: number[]): FilterGroup => { + if (path.length === 0) { + return { + ...group, + conditions: [...group.conditions, { logicalOperator: 'AND', conditions: [] }], + } + } + + const [current, ...rest] = path + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current ? addGroupToGroup(condition as FilterGroup, rest) : condition + ), + } + }, []) + + const handleCustomOptionSelect = useCallback( + (property: FilterProperty, path: number[]) => { + if (property.options && isCustomOptionObject(property.options)) { + const element = property.options.component({ + onChange: (value: string) => { + handleInputChange(path, value) + setIsDialogOpen(false) + setDialogContent(null) + setPendingPath(null) + }, + onCancel: () => { + setIsDialogOpen(false) + setDialogContent(null) + setPendingPath(null) + }, + search: freeformText, + }) + setDialogContent(element) + setIsDialogOpen(true) + setPendingPath(path) + } + }, + [handleInputChange, freeformText] + ) + + const updateNestedFilter = ( + group: FilterGroup, + path: number[], + updateFn: (condition: FilterCondition) => FilterCondition + ): FilterGroup => { + if (path.length === 1) { + return { + ...group, + conditions: group.conditions.map((condition, index) => + index === path[0] ? updateFn(condition as FilterCondition) : condition + ), + } + } + + const [current, ...rest] = path + return { + ...group, + conditions: group.conditions.map((condition, index) => + index === current && isGroup(condition) + ? updateNestedFilter(condition, rest, updateFn) + : condition + ), + } + } + + const handleCommandSelect = (selectedValue: string) => { + if (selectedValue === 'ai-filter') { + handleAIFilter() + return + } + + if (selectedValue === 'group') { + if (activeInput && activeInput.type === 'group') { + const currentPath = activeInput.path + const group = findGroupByPath(activeFilters, currentPath) + if (!group) return + + const updatedFilters = addGroupToGroup(activeFilters, currentPath) + onFilterChange(updatedFilters) + newPathRef.current = [...currentPath, group.conditions.length] + setTimeout(() => { + setActiveInput({ type: 'group', path: newPathRef.current }) + setIsCommandMenuVisible(true) + }, 0) + onFreeformTextChange('') + } + return + } + + if (activeInput && activeInput.type === 'value') { + const path = activeInput.path + const selectedItem = commandItems.find((item) => item.value === selectedValue) as + | CommandItem + | undefined + + if (selectedItem?.isCustom && selectedItem.customOption) { + const element = selectedItem.customOption({ + onChange: (value: string) => { + console.log('onChange:', path, value) + handleInputChange(path, value) + setIsDialogOpen(false) + setDialogContent(null) + setPendingPath(null) + setTimeout(() => { + setActiveInput({ type: 'group', path: path.slice(0, -1) }) + setIsCommandMenuVisible(true) + }, 0) + }, + onCancel: () => { + setIsDialogOpen(false) + setDialogContent(null) + setPendingPath(null) + setTimeout(() => { + setActiveInput({ type: 'group', path: path.slice(0, -1) }) + setIsCommandMenuVisible(true) + }, 0) + }, + search: freeformText, + }) + setDialogContent(element) + setIsDialogOpen(true) + setPendingPath(path) + } else { + handleInputChange(path, selectedValue) + setTimeout(() => { + setActiveInput({ type: 'group', path: path.slice(0, -1) }) + setIsCommandMenuVisible(true) + }, 0) + } + } else if (activeInput && activeInput.type === 'operator') { + const path = activeInput.path + handleOperatorChange(path, selectedValue) + setActiveInput(null) + } else if (activeInput && activeInput.type === 'group') { + const selectedProperty = filterProperties.find((p) => p.name === selectedValue) + if (selectedProperty) { + const currentPath = activeInput.path + const group = findGroupByPath(activeFilters, currentPath) + if (!group) return + + // Check if the property itself is a custom option object + if ( + selectedProperty.options && + !Array.isArray(selectedProperty.options) && + isCustomOptionObject(selectedProperty.options) + ) { + // Add the filter first + const updatedFilters = addFilterToGroup(activeFilters, currentPath, selectedProperty) + onFilterChange(updatedFilters) + const newPath = [...currentPath, group.conditions.length] + + // Show the custom component immediately + const element = selectedProperty.options.component({ + onChange: (value: string) => { + console.log('Custom component onChange called with value:', value) + console.log('Current active input path:', newPath) + // Update the filter with the selected value using the nested update function + const filterWithValue = updateNestedFilter(updatedFilters, newPath, (condition) => ({ + ...condition, + propertyName: selectedProperty.name, + value: value, + operator: '=', + })) + onFilterChange(filterWithValue) + setIsDialogOpen(false) + setDialogContent(null) + setPendingPath(null) + setActiveInput({ type: 'group', path: currentPath }) + }, + onCancel: () => { + console.log('Custom component onCancel called') + removeFilterByPath(newPath) + setIsDialogOpen(false) + setDialogContent(null) + setPendingPath(null) + setActiveInput({ type: 'group', path: currentPath }) + }, + search: '', + }) + setDialogContent(element) + setIsDialogOpen(true) + setPendingPath(newPath) + setIsCommandMenuVisible(false) + } else { + // Handle normal array options or async options + const updatedFilters = addFilterToGroup(activeFilters, currentPath, selectedProperty) + onFilterChange(updatedFilters) + const newPath = [...currentPath, group.conditions.length] + + setTimeout(() => { + setActiveInput({ type: 'value', path: newPath }) + setIsCommandMenuVisible(true) + }, 0) + } + onFreeformTextChange('') + } else { + setError(`Invalid property: ${selectedValue}`) + } + } + } + + const loadPropertyOptions = useCallback( + async (property: FilterProperty, search: string = '') => { + // Skip if no options or if options is an array or not a function + if ( + !property.options || + Array.isArray(property.options) || + !isAsyncOptionsFunction(property.options) + ) + return + + // Check if we have cached options for this exact search + const cached = propertyOptionsCache[property.name] + if (cached && cached.searchValue === search) return + + // Clear any pending loads + if (loadTimeoutRef.current) { + clearTimeout(loadTimeoutRef.current) + } + + // Debounce the load + loadTimeoutRef.current = setTimeout(async () => { + if (loadingOptions[property.name]) return // Prevent duplicate loads + + try { + setLoadingOptions((prev) => ({ ...prev, [property.name]: true })) + // We can safely assert this is an AsyncOptionsFunction because we checked above + const asyncOptions = property.options as AsyncOptionsFunction + const rawOptions = await asyncOptions(search) + // Convert string options to FilterOptionObject format + const options = rawOptions.map((option: string | FilterOptionObject) => + typeof option === 'string' ? { label: option, value: option } : option + ) + setPropertyOptionsCache((prev) => ({ + ...prev, + [property.name]: { options, searchValue: search }, + })) + } catch (error) { + console.error(`Error loading options for ${property.name}:`, error) + setError(`Failed to load options for ${property.label}`) + } finally { + setLoadingOptions((prev) => ({ ...prev, [property.name]: false })) + } + }, 300) // Debounce time + }, + [loadingOptions, propertyOptionsCache] + ) + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (loadTimeoutRef.current) { + clearTimeout(loadTimeoutRef.current) + } + } + }, []) + + const commandItems = useMemo(() => { + if (activeInput?.type === 'operator') { + const condition = findConditionByPath(activeFilters, activeInput.path) + const property = filterProperties.find((p) => p.name === condition?.propertyName) + const operatorValue = condition?.operator?.toUpperCase() || '' + const availableOperators = property?.operators || ['='] + return availableOperators + .filter((op) => op.toUpperCase().includes(operatorValue)) + .map((op) => ({ value: op, label: op, icon: undefined })) + } + + const inputValue = + activeInput?.type === 'group' + ? freeformText + : activeInput?.type === 'value' + ? findConditionByPath(activeFilters, activeInput.path)?.value?.toString() || '' + : '' + + const items: CommandItem[] = [] + + if (activeInput?.type === 'group') { + items.push( + ...filterProperties + .filter((prop) => prop.label.toLowerCase().includes(inputValue.toLowerCase())) + .map((prop) => ({ + value: prop.name, + label: prop.label, + icon: undefined, + isCustom: false, + })) + ) + + items.push({ + value: 'group', + label: 'New Group', + icon: undefined, + }) + + if (inputValue.trim().length > 0 && aiApiUrl) { + items.push({ + value: 'ai-filter', + label: 'Filter by AI', + icon: , + }) + } + } else if (activeInput?.type === 'value') { + const activeCondition = findConditionByPath(activeFilters, activeInput.path) + const property = filterProperties.find((p) => p.name === activeCondition?.propertyName) + + if (property) { + // If the property itself is a custom option object, show just one item + if (!Array.isArray(property.options) && isCustomOptionObject(property.options)) { + items.push({ + value: 'custom', + label: property.options.label || 'Custom...', + icon: undefined, + isCustom: true, + customOption: property.options.component, + }) + } else if (loadingOptions[property.name]) { + items.push({ + value: 'loading', + label: 'Loading options...', + icon: undefined, + }) + } else if (Array.isArray(property.options)) { + items.push( + ...property.options + .filter((option) => { + if (typeof option === 'string') { + return option.toLowerCase().includes(inputValue.toLowerCase()) + } + if (isFilterOptionObject(option)) { + return option.label.toLowerCase().includes(inputValue.toLowerCase()) + } + if (isCustomOptionObject(option)) { + return option.label?.toLowerCase().includes(inputValue.toLowerCase()) ?? true + } + return true + }) + .map((option) => { + if (typeof option === 'string') { + return { + value: option, + label: option, + icon: undefined, + } as CommandItem + } + if (isFilterOptionObject(option)) { + return { + value: option.value, + label: option.label, + icon: undefined, + } as CommandItem + } + if (isCustomOptionObject(option)) { + return { + value: 'custom', + label: option.label || 'Custom...', + icon: undefined, + isCustom: true, + customOption: option.component, + } as CommandItem + } + return null + }) + .filter((item): item is CommandItem => item !== null) + ) + } else if (propertyOptionsCache[property.name]) { + items.push( + ...propertyOptionsCache[property.name].options.map((option) => { + if (typeof option === 'string') { + return { + value: option, + label: option, + icon: undefined, + } + } + return { + value: option.value, + label: option.label, + icon: undefined, + } + }) + ) + } + + // Load options if not cached or if search value has changed + if ( + typeof property.options === 'function' && + !loadingOptions[property.name] && + (!propertyOptionsCache[property.name] || + propertyOptionsCache[property.name].searchValue !== inputValue) + ) { + loadPropertyOptions(property, inputValue) + } + } + } + + return items + }, [ + activeInput, + freeformText, + activeFilters, + filterProperties, + propertyOptionsCache, + loadingOptions, + loadPropertyOptions, + aiApiUrl, + ]) + + const removeFilterByPath = useCallback( + (path: number[]) => { + const removeFromGroup = (group: FilterGroup, currentPath: number[]): FilterGroup => { + if (currentPath.length === 1) { + return { + ...group, + conditions: group.conditions.filter((_, i) => i !== currentPath[0]), + } + } + + const [current, ...rest] = currentPath + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current ? removeFromGroup(condition as FilterGroup, rest) : condition + ), + } + } + + const updatedFilters = removeFromGroup(activeFilters, path) + onFilterChange(updatedFilters) + }, + [activeFilters] + ) + + const removeGroupByPath = useCallback( + (path: number[]) => { + const removeFromGroup = (group: FilterGroup, currentPath: number[]): FilterGroup => { + if (currentPath.length === 1) { + return { + ...group, + conditions: group.conditions.filter((_, i) => i !== currentPath[0]), + } + } + + const [current, ...rest] = currentPath + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current ? removeFromGroup(condition as FilterGroup, rest) : condition + ), + } + } + + const updatedFilters = removeFromGroup(activeFilters, path) + onFilterChange(updatedFilters) + }, + [activeFilters] + ) + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'ArrowDown') { + e.preventDefault() + setSelectedCommandIndex((prevIndex) => + prevIndex < commandItems.length - 1 ? prevIndex + 1 : prevIndex + ) + } else if (e.key === 'ArrowUp') { + e.preventDefault() + setSelectedCommandIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : 0)) + } else if (e.key === 'Enter') { + e.preventDefault() + if (commandItems[selectedCommandIndex]) { + handleCommandSelect(commandItems[selectedCommandIndex].value) + } + } else if (e.key === 'Backspace') { + // Skip backspace handling for operator inputs + if (activeInput?.type === 'operator') return + + const inputElement = e.target as HTMLInputElement + const isEmpty = inputElement.value === '' + + if (activeInput?.type === 'group' && isEmpty) { + e.preventDefault() + const group = findGroupByPath(activeFilters, activeInput.path) + + if (group && group.conditions.length > 0) { + // Remove the last condition in the group + const lastConditionPath = [...activeInput.path, group.conditions.length - 1] + removeFilterByPath(lastConditionPath) + // Keep focus on the group's input + setActiveInput({ type: 'group', path: activeInput.path }) + } else if (group && group.conditions.length === 0) { + // Remove the empty group + removeGroupByPath(activeInput.path) + + // Focus the parent group's freeform input + if (activeInput.path.length > 0) { + setActiveInput({ + type: 'group', + path: activeInput.path.slice(0, -1), + }) + } else { + setActiveInput(null) + } + } + } else if (activeInput?.type === 'value' && isEmpty) { + const condition = findConditionByPath(activeFilters, activeInput.path) + if (condition && !condition.value) { + e.preventDefault() + removeFilterByPath(activeInput.path) + + // Focus the group's input + setActiveInput({ + type: 'group', + path: activeInput.path.slice(0, -1), + }) + } + } + } else if (e.key === ' ' && activeInput?.type === 'value') { + e.preventDefault() + setActiveInput({ type: 'group', path: [] }) + } else if (e.key === 'ArrowLeft') { + const inputElement = e.target as HTMLInputElement + if (inputElement.selectionStart === 0) { + e.preventDefault() + if (activeInput?.type === 'value') { + const lastIndex = activeInput.path[activeInput.path.length - 1] + if (lastIndex > 0) { + setActiveInput({ + type: 'value', + path: [...activeInput.path.slice(0, -1), lastIndex - 1], + }) + } else { + setActiveInput({ + type: 'operator', + path: activeInput.path, + }) + } + } else if (activeInput?.type === 'group') { + const group = findGroupByPath(activeFilters, activeInput.path) + if (group && group.conditions.length > 0) { + // If the group has conditions, select the last one + const lastConditionPath = [...activeInput.path, group.conditions.length - 1] + setActiveInput({ type: 'value', path: lastConditionPath }) + } else if (activeInput.path.length > 0) { + // If no conditions but has parent, select parent group's input + setActiveInput({ + type: 'group', + path: activeInput.path.slice(0, -1), + }) + } + } + } + } else if (e.key === 'ArrowRight') { + const inputElement = e.target as HTMLInputElement + if (inputElement.selectionStart === inputElement.value.length) { + e.preventDefault() + if (activeInput?.type === 'value') { + const group = findGroupByPath(activeFilters, activeInput.path.slice(0, -1)) + const lastIndex = activeInput.path[activeInput.path.length - 1] + if (group && lastIndex < group.conditions.length - 1) { + setActiveInput({ + type: 'value', + path: [...activeInput.path.slice(0, -1), lastIndex + 1], + }) + } else { + setActiveInput({ + type: 'group', + path: activeInput.path.slice(0, -1), + }) + } + } else if (activeInput?.type === 'operator') { + setActiveInput({ type: 'value', path: activeInput.path }) + } + } + } else if (e.key === 'Escape') { + setIsCommandMenuVisible(false) + setActiveInput(null) + } + } + + const handleLabelClick = useCallback((path: number[]) => { + setActiveInput({ type: 'value', path }) + }, []) + + const handleLogicalOperatorChange = useCallback( + (path: number[]) => { + const updateNestedLogicalOperator = ( + group: FilterGroup, + currentPath: number[] + ): FilterGroup => { + if (currentPath.length === 0) { + return { + ...group, + logicalOperator: group.logicalOperator === 'AND' ? 'OR' : 'AND', + } + } + + const [current, ...rest] = currentPath + return { + ...group, + conditions: group.conditions.map((condition, i) => + i === current + ? isGroup(condition) + ? updateNestedLogicalOperator(condition, rest) + : condition + : condition + ), + } + } + + const updatedFilters = updateNestedLogicalOperator(activeFilters, path) + onFilterChange(updatedFilters) + }, + [activeFilters] + ) + + return ( +
+
+ +
+ +
+
+ {error &&
{error}
} + {isCommandMenuVisible && !isLoading && activeInput !== null && commandItems.length > 0 && ( + + + No results found. + + {commandItems.map((item, index) => ( + handleCommandSelect(item.value)} + className={`text-xs font-mono ${ + index === selectedCommandIndex ? 'bg-surface-400' : '' + }`} + > + {item.icon} + {item.label} + + ))} + + + + )} + setIsDialogOpen(open)}> + + {dialogContent} + + +
+ ) +} diff --git a/packages/ui-patterns/FilterBar/FilterCondition.tsx b/packages/ui-patterns/FilterBar/FilterCondition.tsx new file mode 100644 index 00000000000..f958534f860 --- /dev/null +++ b/packages/ui-patterns/FilterBar/FilterCondition.tsx @@ -0,0 +1,86 @@ +import React, { useRef, useEffect } from 'react' +import { Input_Shadcn_ } from 'ui' +import { FilterCondition as FilterConditionType, FilterProperty } from './types' + +type FilterConditionProps = { + condition: FilterConditionType + filterProperties: FilterProperty[] + id: string + isActive: boolean + isOperatorActive: boolean + isLoading?: boolean + onOperatorChange: (value: string) => void + onValueChange: (value: string) => void + onOperatorFocus: () => void + onValueFocus: () => void + onBlur: () => void + onLabelClick: () => void + onKeyDown: (e: React.KeyboardEvent) => void +} + +export function FilterCondition({ + condition, + filterProperties, + id, + isActive, + isOperatorActive, + isLoading, + onOperatorChange, + onValueChange, + onOperatorFocus, + onValueFocus, + onBlur, + onLabelClick, + onKeyDown, +}: FilterConditionProps) { + const operatorRef = useRef(null) + const valueRef = useRef(null) + const property = filterProperties.find((p) => p.name === condition.propertyName) + + useEffect(() => { + if (isActive && valueRef.current) { + valueRef.current.focus() + } else if (isOperatorActive && operatorRef.current) { + operatorRef.current.focus() + } + }, [isActive, isOperatorActive]) + + if (!property) return null + + return ( +
+ + {property.label} + + onOperatorChange(e.target.value)} + onFocus={onOperatorFocus} + onBlur={onBlur} + className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 font-mono h-6 mr-1 text-foreground-light" + style={{ + width: `${Math.max(condition.operator.length, 1)}ch`, + minWidth: '1ch', + }} + disabled={isLoading} + aria-label={`Operator for ${property.label}`} + /> + onValueChange(e.target.value)} + onFocus={onValueFocus} + onBlur={onBlur} + onKeyDown={onKeyDown} + className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 font-mono h-6" + style={{ + width: `${Math.max(condition.value?.toString().length || 0, 1)}ch`, + minWidth: '1ch', + }} + disabled={isLoading} + aria-label={`Value for ${property.label}`} + /> +
+ ) +} diff --git a/packages/ui-patterns/FilterBar/FilterGroup.tsx b/packages/ui-patterns/FilterBar/FilterGroup.tsx new file mode 100644 index 00000000000..8cd71755e36 --- /dev/null +++ b/packages/ui-patterns/FilterBar/FilterGroup.tsx @@ -0,0 +1,199 @@ +import React, { useState, useRef, useEffect } from 'react' +import { FilterProperty, FilterGroup as FilterGroupType } from './types' +import { ActiveInput } from './FilterBar' +import { FilterCondition } from './FilterCondition' +import { Input_Shadcn_ } from 'ui' + +type FilterGroupProps = { + group: FilterGroupType + path: number[] + isLoading?: boolean + filterProperties: FilterProperty[] + // Active state + activeInput: ActiveInput + onOperatorChange: (path: number[], value: string) => void + onValueChange: (path: number[], value: string) => void + onOperatorFocus: (path: number[]) => void + onValueFocus: (path: number[]) => void + onBlur: () => void + onLabelClick: (path: number[]) => void + onKeyDown: (e: React.KeyboardEvent) => void + // Group specific props + onGroupFreeformChange: (path: number[], value: string) => void + onGroupFreeformFocus: (path: number[]) => void + groupFreeformValue: string + isGroupFreeformActive: boolean + // Logical operator props + onLogicalOperatorChange?: (path: number[]) => void +} + +export function FilterGroup({ + group, + path, + isLoading, + activeInput, + filterProperties, + onOperatorChange, + onValueChange, + onOperatorFocus, + onValueFocus, + onBlur, + onLabelClick, + onKeyDown, + onGroupFreeformChange, + onGroupFreeformFocus, + groupFreeformValue, + isGroupFreeformActive, + onLogicalOperatorChange, +}: FilterGroupProps) { + const [localFreeformValue, setLocalFreeformValue] = useState('') + const freeformInputRef = useRef(null) + const [isHoveringOperator, setIsHoveringOperator] = useState(false) + const isActive = + isGroupFreeformActive && + activeInput?.type === 'group' && + path.length === activeInput.path.length && + path.every((v, i) => v === activeInput.path[i]) + + // Reset local value when group freeform value is cleared + useEffect(() => { + if (groupFreeformValue === '') { + setLocalFreeformValue('') + } + }, [groupFreeformValue]) + + useEffect(() => { + if (isActive && freeformInputRef.current) { + freeformInputRef.current.focus() + } + }, [isActive]) + + const handleFreeformChange = (e: React.ChangeEvent) => { + setLocalFreeformValue(e.target.value) + onGroupFreeformChange(path, e.target.value) + } + + const handleLogicalOperatorClick = () => { + onLogicalOperatorChange?.(path) + } + + const handleLogicalOperatorMouseEnter = () => { + setIsHoveringOperator(true) + } + + const handleLogicalOperatorMouseLeave = () => { + setIsHoveringOperator(false) + } + + const isConditionActive = (conditionPath: number[]) => { + if (!activeInput) return false + return ( + activeInput.type === 'value' && + conditionPath.length === activeInput.path.length && + conditionPath.every((v, i) => v === activeInput.path[i]) + ) + } + + const isOperatorActive = (conditionPath: number[]) => { + if (!activeInput) return false + return ( + activeInput.type === 'operator' && + conditionPath.length === activeInput.path.length && + conditionPath.every((v, i) => v === activeInput.path[i]) + ) + } + + return ( +
0 + ? "before:content-['('] before:text-foreground-muted after:content-[')'] after:text-foreground-muted" + : '' + }`} + > +
+ {group.conditions.map((condition, index) => { + const currentPath = [...path, index] + + return ( + + {index > 0 && ( + + {group.logicalOperator} + + )} + {/* Render condition or nested group */} + {'logicalOperator' in condition ? ( + + ) : ( + onOperatorChange(currentPath, value)} + filterProperties={filterProperties} + onValueChange={(value) => onValueChange(currentPath, value)} + onOperatorFocus={() => onOperatorFocus(currentPath)} + onValueFocus={() => onValueFocus(currentPath)} + onBlur={onBlur} + onLabelClick={() => onLabelClick(currentPath)} + onKeyDown={onKeyDown} + /> + )} + + ) + })} + {/* Add freeform input at the end */} + onGroupFreeformFocus(path)} + onBlur={onBlur} + onKeyDown={onKeyDown} + className="border-none bg-transparent p-0 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 font-mono h-6" + placeholder={ + path.length === 0 && group.conditions.length === 0 ? 'Search or filter...' : '+' + } + disabled={isLoading} + style={{ + width: `${Math.max( + (isActive ? groupFreeformValue : localFreeformValue).length || 1, + path.length === 0 && group.conditions.length === 0 ? 18 : 1 + )}ch`, + minWidth: path.length === 0 && group.conditions.length === 0 ? '18ch' : '1ch', + }} + /> +
+
+ ) +} diff --git a/packages/ui-patterns/FilterBar/index.ts b/packages/ui-patterns/FilterBar/index.ts new file mode 100644 index 00000000000..8f852e36afb --- /dev/null +++ b/packages/ui-patterns/FilterBar/index.ts @@ -0,0 +1,2 @@ +export * from './FilterBar' +export * from './types' diff --git a/packages/ui-patterns/FilterBar/types.ts b/packages/ui-patterns/FilterBar/types.ts new file mode 100644 index 00000000000..84be2d517ea --- /dev/null +++ b/packages/ui-patterns/FilterBar/types.ts @@ -0,0 +1,45 @@ +import React from 'react' + +export type CustomOptionProps = { + onChange: (value: string) => void + onCancel: () => void + search?: string +} + +export type FilterOptionObject = { + label: string + value: string +} + +export type CustomOptionObject = { + label?: string + component: (props: CustomOptionProps) => React.ReactElement +} + +export type FilterOption = string | FilterOptionObject | CustomOptionObject + +export type AsyncOptionsFunction = (search?: string) => Promise<(string | FilterOptionObject)[]> +export type SyncOptionsFunction = (search?: string) => (string | FilterOptionObject)[] + +export type FilterProperty = { + label: string + name: string + type: 'string' | 'number' | 'date' | 'boolean' + options?: FilterOption[] | AsyncOptionsFunction | SyncOptionsFunction | CustomOptionObject + operators?: string[] +} + +export type FilterCondition = { + propertyName: string + value: string | number | null + operator: string +} + +export type FilterGroup = { + logicalOperator: 'AND' | 'OR' + conditions: (FilterCondition | FilterGroup)[] +} + +export function isGroup(condition: FilterCondition | FilterGroup): condition is FilterGroup { + return 'logicalOperator' in condition +} diff --git a/packages/ui-patterns/index.tsx b/packages/ui-patterns/index.tsx index 8bc8556609b..4e3446f5a0f 100644 --- a/packages/ui-patterns/index.tsx +++ b/packages/ui-patterns/index.tsx @@ -22,5 +22,6 @@ export * from './PromoToast' export * from './admonition' export * from './ComputeBadge' export * from './TimestampInfo' +export * from './FilterBar' export * from './Toc' export * from './LogsBarChart' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 480833f2069..8a87b8add2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22895,7 +22895,7 @@ snapshots: debug: 4.4.0(supports-color@8.1.1) enhanced-resolve: 5.17.1 eslint: 8.57.0(supports-color@8.1.1) - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) fast-glob: 3.3.2 get-tsconfig: 4.7.2 @@ -22907,7 +22907,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: @@ -22934,7 +22934,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0(supports-color@8.1.1) eslint-import-resolver-node: 0.3.9(supports-color@8.1.1) - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 From a8f193cc3ce2abe38716cda57876e01bbdefca95 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Fri, 25 Apr 2025 08:56:12 -0230 Subject: [PATCH 11/86] Fix storage explorer footer (#35036) * Fix storage explorer footer * Fix --------- Co-authored-by: Joshen Lim --- .../Storage/StorageExplorer/FileExplorer.tsx | 8 +- .../StorageExplorer/FileExplorerColumn.tsx | 236 +++++++++--------- .../StorageExplorer/StorageExplorer.tsx | 2 +- 3 files changed, 119 insertions(+), 127 deletions(-) diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx index a99314b9b69..9d6612288a4 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx @@ -1,7 +1,8 @@ +import { noop } from 'lodash' import { observer } from 'mobx-react-lite' import { useEffect, useRef } from 'react' -import { noop } from 'lodash' +import { cn } from 'ui' import { CONTEXT_MENU_KEYS, STORAGE_VIEWS } from '../Storage.constants' import type { StorageColumn, StorageItem, StorageItemWithColumn } from '../Storage.types' import ColumnContextMenu from './ColumnContextMenu' @@ -46,7 +47,10 @@ const FileExplorer = ({ return (
diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx index 3bb49eb2f85..95ccbb7455f 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx @@ -1,15 +1,15 @@ import { Transition } from '@headlessui/react' import { get, noop, sum } from 'lodash' +import { Upload } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { useContextMenu } from 'react-contexify' -import { Checkbox } from 'ui' import InfiniteList from 'components/ui/InfiniteList' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { BASE_PATH } from 'lib/constants' import { formatBytes } from 'lib/helpers' -import { Upload } from 'lucide-react' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' +import { Checkbox, cn } from 'ui' import { CONTEXT_MENU_KEYS, STORAGE_ROW_STATUS, @@ -142,145 +142,133 @@ const FileExplorerColumn = ({ ) return ( - <> -
{ - const eventTarget = get(event.target, ['className'], '') - if (typeof eventTarget === 'string' && eventTarget.includes('react-contexify')) return - onSelectColumnEmptySpace(index) - }} - > - {/* Checkbox selection for select all */} - {view === STORAGE_VIEWS.COLUMNS && ( -
event.stopPropagation()} - > - {columnFiles.length > 0 ? ( - <> - -

- Select all {columnFiles.length} files -

- - ) : ( -

No files available for selection

- )} -
- )} - - {/* List Interface Header */} - {view === STORAGE_VIEWS.LIST && ( -
-
+
{ + const eventTarget = get(event.target, ['className'], '') + if (typeof eventTarget === 'string' && eventTarget.includes('react-contexify')) return + onSelectColumnEmptySpace(index) + }} + > + {/* Checkbox selection for select all */} + {view === STORAGE_VIEWS.COLUMNS && ( +
event.stopPropagation()} + > + {columnFiles.length > 0 ? ( + <> -

Name

-
-

Size

-

Type

-

Created at

-

Last modified at

-
- )} +

Select all {columnFiles.length} files

+ + ) : ( +

No files available for selection

+ )} +
+ )} - {/* Shimmering loaders while fetching contents */} - {column.status === STORAGE_ROW_STATUS.LOADING && ( -
+
+ +

Name

+
+

Size

+

Type

+

Created at

+

Last modified at

+
+ )} + + {/* Shimmering loaders while fetching contents */} + {column.status === STORAGE_ROW_STATUS.LOADING && ( +
- - - + > + + + +
+ )} + + {/* Column Interface */} + (index !== 0 && index === columnItems.length ? 85 : 37)} + hasNextPage={column.status !== STORAGE_ROW_STATUS.LOADING && column.hasMoreItems} + isLoadingNextPage={column.isLoadingMoreItems} + onLoadNextPage={() => onColumnLoadMore(index, column)} + /> + + {/* Drag drop upload CTA for when column is empty */} + {!(snap.isSearching && itemSearchString.length > 0) && + column.items.length === 0 && + column.status !== STORAGE_ROW_STATUS.LOADING && ( +
+ storage-placeholder +

Drop your files here

+

+ Or upload them via the "Upload file" button above +

)} - {/* Column Interface */} - (index !== 0 && index === columnItems.length ? 85 : 37)} - hasNextPage={column.status !== STORAGE_ROW_STATUS.LOADING && column.hasMoreItems} - isLoadingNextPage={column.isLoadingMoreItems} - onLoadNextPage={() => onColumnLoadMore(index, column)} - /> + {snap.isSearching && + itemSearchString.length > 0 && + isEmpty && + column.status !== STORAGE_ROW_STATUS.LOADING && ( +
+

No results found in this folder

+

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

+
+ )} - {/* Drag drop upload CTA for when column is empty */} - {!(snap.isSearching && itemSearchString.length > 0) && - column.items.length === 0 && - column.status !== STORAGE_ROW_STATUS.LOADING && ( -
- storage-placeholder -

Drop your files here

-

- Or upload them via the "Upload file" button above -

-
- )} + {/* Drag drop upload CTA for when column has files */} + setIsDraggedOver(false)} + onDrop={() => setIsDraggedOver(false)} + /> - {snap.isSearching && - itemSearchString.length > 0 && - isEmpty && - column.status !== STORAGE_ROW_STATUS.LOADING && ( -
-

No results found in this folder

-

- Your search for "{itemSearchString}" did not return any results -

-
- )} - - {/* Drag drop upload CTA for when column has files */} - setIsDraggedOver(false)} - onDrop={() => setIsDraggedOver(false)} - /> -
{/* List interface footer */} {view === STORAGE_VIEWS.LIST && ( -
+

{formatBytes(columnItemsSize)} for {columnItems.length} items

)} - +
) } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx index e55cb48a772..bc34848e6eb 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx @@ -1,8 +1,8 @@ -import { useParams } from 'common' import { compact, get, isEmpty, uniqBy } from 'lodash' import { observer } from 'mobx-react-lite' import { useEffect, useRef, useState } from 'react' +import { useParams } from 'common' import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query' import type { Bucket } from 'data/storage/buckets-query' import { IS_PLATFORM } from 'lib/constants' From 0a40e0e5e59043549877ab393e34f7ebe5f5a8e1 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Sat, 26 Apr 2025 00:27:22 +1200 Subject: [PATCH 12/86] chore: update self-hosted image versions (#35257) * chore: update image versions for docker/docker-compose.yml * Update docker-compose.yml --------- Co-authored-by: Han Qiao --- docker/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 54e2575df5e..5f0c9e04659 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -162,7 +162,7 @@ services: rest: container_name: supabase-rest - image: postgrest/postgrest:v12.2.10 + image: postgrest/postgrest:v12.2.11 restart: unless-stopped depends_on: db: @@ -231,7 +231,7 @@ services: # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up storage: container_name: supabase-storage - image: supabase/storage-api:v1.22.3 + image: supabase/storage-api:v1.22.7 restart: unless-stopped volumes: - ./volumes/storage:/var/lib/storage:z From 21f6f1bb16ae09791503d97215245d51c13f6617 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 25 Apr 2025 17:30:39 +0200 Subject: [PATCH 13/86] fix: Disable the `convertSqlToSupabaseJs` AI tool (#35280) * Disable the convertSqlToSupabaseJs tool because it relies on sql-to-rest which expects binaries of libpg-query. * Comment out the code instead of deleting it. --- apps/studio/pages/api/ai/sql/tools.ts | 43 ++++++++++++++------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/apps/studio/pages/api/ai/sql/tools.ts b/apps/studio/pages/api/ai/sql/tools.ts index 8473a7198ff..77fbf78abda 100644 --- a/apps/studio/pages/api/ai/sql/tools.ts +++ b/apps/studio/pages/api/ai/sql/tools.ts @@ -2,7 +2,7 @@ import { tool } from 'ai' import { stripIndent } from 'common-tags' import { z } from 'zod' -import { processSql, renderSupabaseJs } from '@supabase/sql-to-rest' +// import { processSql, renderSupabaseJs } from '@supabase/sql-to-rest' import { getDatabaseFunctions } from 'data/database-functions/database-functions-query' import { getDatabasePolicies } from 'data/database-policies/database-policies-query' import { getEntityDefinitionsSql } from 'data/database/entity-definitions-query' @@ -54,25 +54,28 @@ export const getTools = ({ } }, }), - convertSqlToSupabaseJs: tool({ - description: 'Convert an sql query into supabase-js client code', - parameters: z.object({ - sql: z - .string() - .describe( - 'The sql statement to convert. Only a subset of statements are supported currently. ' - ), - }), - execute: async ({ sql }) => { - try { - const statement = await processSql(sql) - const { code } = await renderSupabaseJs(statement) - return code - } catch (error) { - return `Failed to convert SQL: ${error}` - } - }, - }), + // [Ivan] The tool relies on `libpg-query` binaries which we've removed intentionally because they couldn't build on MacOS. + // Once we figure out a way how to use wasm binaries, we can add it back. + // + // convertSqlToSupabaseJs: tool({ + // description: 'Convert an sql query into supabase-js client code', + // parameters: z.object({ + // sql: z + // .string() + // .describe( + // 'The sql statement to convert. Only a subset of statements are supported currently. ' + // ), + // }), + // execute: async ({ sql }) => { + // try { + // const statement = await processSql(sql) + // const { code } = await renderSupabaseJs(statement) + // return code + // } catch (error) { + // return `Failed to convert SQL: ${error}` + // } + // }, + // }), getRlsKnowledge: tool({ description: 'Get existing policies and examples and instructions on how to write RLS policies', From c136ee4c193f5c6a36e75d6cc4b2f3898902f506 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:02:27 -0400 Subject: [PATCH 14/86] ci (docs): fix docs lint action on forks (#35277) Before: The docs lint action doesn't run properly on forks because it tries to use the branch name to identify the PR for updating, but the branch name is adapted on forks, so it is not `branch-name`, but `forking-account:branch-name`. After: Use the PR number instead to identify the PR instead. --- .github/workflows/docs-lint-v2.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs-lint-v2.yml b/.github/workflows/docs-lint-v2.yml index 6586eb38daf..aa977fcd5c4 100644 --- a/.github/workflows/docs-lint-v2.yml +++ b/.github/workflows/docs-lint-v2.yml @@ -63,7 +63,7 @@ jobs: env: BASE_REF: ${{ github.base_ref }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BRANCH_NAME: ${{ github.head_ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | set -o pipefail run_lints() { @@ -76,6 +76,6 @@ jobs: LINT_EXIT_CODE=$? set -e if [[ $LINT_EXIT_CODE -ne 0 ]]; then - gh pr comment "$BRANCH_NAME" --body "$LINT_RESULTS" + gh pr comment "$PR_NUMBER" --body "$LINT_RESULTS" exit 1 fi From a915f3d7e73bf319a68398c2cceaa301ad48c2be Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 25 Apr 2025 18:37:41 +0200 Subject: [PATCH 15/86] fix: Delay the autofocus of the input in a treeview (#35270) * Remove leftover resizable group which weren't interactive. * Put the input focus in a setTimeout. * Alternative solution * Small fix * Bump threshold to 400ms * Final fix, hopefully. --------- Co-authored-by: Joshen Lim --- .../interfaces/SQLEditor/SQLEditor.tsx | 399 +++++++++--------- apps/studio/lib/constants/index.ts | 1 - apps/studio/state/sql-editor-v2.ts | 7 +- .../ui/src/components/TreeView/TreeView.tsx | 90 ++-- 4 files changed, 254 insertions(+), 243 deletions(-) diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx index e518e86627f..4a10fc3ad7c 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -670,217 +670,208 @@ export const SQLEditor = () => { }} /> - - - - -
- {isLoading ? ( -
- -
- ) : ( - <> - {isDiffOpen && ( -
- { - diffEditorRef.current = editor - setIsDiffEditorMounted(true) - }} - options={{ - fontSize: 13, - renderSideBySide: false, - minimap: { enabled: false }, - wordWrap: 'on', - lineNumbers: 'on', - folding: false, - padding: { top: 4 }, - lineNumbersMinChars: 3, - }} - /> - {showWidget && ( - { - handlePrompt(prompt, { - beforeSelection: promptState.beforeSelection, - selection: promptState.selection || defaultSqlDiff.modified, - afterSelection: promptState.afterSelection, - }) - }} - onAccept={acceptAiHandler} - onReject={discardAiHandler} - onCancel={resetPrompt} - isDiffVisible={true} - isLoading={isCompletionLoading} - startLineNumber={Math.max(0, promptState.startLineNumber)} - endLineNumber={promptState.endLineNumber} - /> - )} -
- )} -
- { - setPromptState((prev) => ({ - ...prev, - isOpen: true, - selection, - beforeSelection, - afterSelection, - startLineNumber, - endLineNumber, - })) - }} - /> - {editorRef.current && promptState.isOpen && !isDiffOpen && ( - { - handlePrompt(prompt, { - beforeSelection: promptState.beforeSelection, - selection: promptState.selection, - afterSelection: promptState.afterSelection, - }) - }} - onCancel={resetPrompt} - isDiffVisible={false} - isLoading={isCompletionLoading} - startLineNumber={Math.max(0, promptState.startLineNumber)} - endLineNumber={promptState.endLineNumber} - /> - )} - - {!promptState.isOpen && !editorRef.current?.getValue() && ( - - Hit {os === 'macos' ? : `CTRL+`}K to edit with the - Assistant - - )} - -
- - )} -
-
- - - - +
+ + +
{isLoading ? (
) : ( - - )} - - - - {results?.rows !== undefined && !isExecuting && ( - - - -

- - {results.rows.length} row{results.rows.length > 1 ? 's' : ''} - - - {results.autoLimit !== undefined && - ` (Limited to only ${results.autoLimit} rows)`} - -

-
- -

- - Results are automatically limited to preserve browser performance, in - particular if your query returns an exceptionally large number of rows. - - - - You may change or remove this limit from the dropdown on the right - -

-
-
- {results.autoLimit !== undefined && ( - - - - - - snapV2.setLimit(Number(val))} - > - {ROWS_PER_PAGE_OPTIONS.map((option) => ( - - {option.label} - - ))} - - - + <> + {isDiffOpen && ( +
+ { + diffEditorRef.current = editor + setIsDiffEditorMounted(true) + }} + options={{ + fontSize: 13, + renderSideBySide: false, + minimap: { enabled: false }, + wordWrap: 'on', + lineNumbers: 'on', + folding: false, + padding: { top: 4 }, + lineNumbersMinChars: 3, + }} + /> + {showWidget && ( + { + handlePrompt(prompt, { + beforeSelection: promptState.beforeSelection, + selection: promptState.selection || defaultSqlDiff.modified, + afterSelection: promptState.afterSelection, + }) + }} + onAccept={acceptAiHandler} + onReject={discardAiHandler} + onCancel={resetPrompt} + isDiffVisible={true} + isLoading={isCompletionLoading} + startLineNumber={Math.max(0, promptState.startLineNumber)} + endLineNumber={promptState.endLineNumber} + /> + )} +
)} -
+
+ { + setPromptState((prev) => ({ + ...prev, + isOpen: true, + selection, + beforeSelection, + afterSelection, + startLineNumber, + endLineNumber, + })) + }} + /> + {editorRef.current && promptState.isOpen && !isDiffOpen && ( + { + handlePrompt(prompt, { + beforeSelection: promptState.beforeSelection, + selection: promptState.selection, + afterSelection: promptState.afterSelection, + }) + }} + onCancel={resetPrompt} + isDiffVisible={false} + isLoading={isCompletionLoading} + startLineNumber={Math.max(0, promptState.startLineNumber)} + endLineNumber={promptState.endLineNumber} + /> + )} + + {!promptState.isOpen && !editorRef.current?.getValue() && ( + + Hit {os === 'macos' ? : `CTRL+`}K to edit with the + Assistant + + )} + +
+ )} -
- - - +
+
+ + + + + {isLoading ? ( +
+ +
+ ) : ( + + )} +
+ +
+ {results?.rows !== undefined && !isExecuting && ( + + + +

+ + {results.rows.length} row{results.rows.length > 1 ? 's' : ''} + + + {results.autoLimit !== undefined && + ` (Limited to only ${results.autoLimit} rows)`} + +

+
+ +

+ + Results are automatically limited to preserve browser performance, in + particular if your query returns an exceptionally large number of rows. + + + + You may change or remove this limit from the dropdown on the right + +

+
+
+ {results.autoLimit !== undefined && ( + + + + + + snapV2.setLimit(Number(val))} + > + {ROWS_PER_PAGE_OPTIONS.map((option) => ( + + {option.label} + + ))} + + + + )} +
+ )} +
+
+
) } diff --git a/apps/studio/lib/constants/index.ts b/apps/studio/lib/constants/index.ts index 01897488056..3ca7d99257d 100644 --- a/apps/studio/lib/constants/index.ts +++ b/apps/studio/lib/constants/index.ts @@ -59,7 +59,6 @@ export const LOCAL_STORAGE_KEYS = { SQL_EDITOR_INTELLISENSE: 'supabase_sql-editor-intellisense-enabled', SQL_EDITOR_SPLIT_SIZE: 'supabase_sql-editor-split-size', - SQL_EDITOR_AI_PANEL_SPLIT_SIZE: 'supabase_sql-editor-ai-panel-split-size', // Key to track which schemas are ok to be sent to AI. The project ref is intentionally put at the end for easier search in the browser console. SQL_EDITOR_AI_SCHEMA: (ref: string) => `supabase_sql-editor-ai-schema-enabled-${ref}`, SQL_EDITOR_AI_OPEN: 'supabase_sql-editor-ai-open', diff --git a/apps/studio/state/sql-editor-v2.ts b/apps/studio/state/sql-editor-v2.ts index 0f85816801b..94046904784 100644 --- a/apps/studio/state/sql-editor-v2.ts +++ b/apps/studio/state/sql-editor-v2.ts @@ -178,14 +178,15 @@ export const sqlEditorState = proxy({ saveFolder: ({ id, name }: { id: string; name: string }) => { let storeFolder = sqlEditorState.folders[id] + const isNewFolder = id === 'new-folder' const hasChanges = storeFolder.folder.name !== name - if (id === 'new-folder' && sqlEditorState.allFolderNames.includes(name)) { + if (isNewFolder && sqlEditorState.allFolderNames.includes(name)) { sqlEditorState.removeFolder(id) - return toast.error('This folder name already exists') + return toast.error('Unable to create new folder: This folder name already exists') } else if (hasChanges && sqlEditorState.allFolderNames.includes(name)) { storeFolder.status = 'idle' - return toast.error('This folder name already exists') + return toast.error('Unable to update folder: This folder name already exists') } const originalFolderName = storeFolder.folder.name.slice() diff --git a/packages/ui/src/components/TreeView/TreeView.tsx b/packages/ui/src/components/TreeView/TreeView.tsx index 7ecc3435566..d8003cf382d 100644 --- a/packages/ui/src/components/TreeView/TreeView.tsx +++ b/packages/ui/src/components/TreeView/TreeView.tsx @@ -2,7 +2,15 @@ import { cva, VariantProps } from 'class-variance-authority' import { ChevronRight, FolderClosed, FolderOpen, Loader2 } from 'lucide-react' -import { ComponentPropsWithoutRef, forwardRef, ReactNode, useEffect, useRef, useState } from 'react' +import { + ComponentPropsWithoutRef, + FocusEvent, + forwardRef, + ReactNode, + useEffect, + useRef, + useState, +} from 'react' import TreeViewPrimitive, { flattenTree } from 'react-accessible-treeview' import { cn } from '../../lib/utils' import { Input } from '../shadcn/ui/input' @@ -90,41 +98,44 @@ const TreeViewItem = forwardRef< ) => { const [localValueState, setLocalValueState] = useState(name) const inputRef = useRef(null) - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (inputRef.current && !inputRef.current.contains(event.target as Node)) { - onEditSubmit?.(localValueState) - } - } - if (isEditing) document.addEventListener('mousedown', handleClickOutside) - return () => { - if (isEditing) document.removeEventListener('mousedown', handleClickOutside) - } - }, [isEditing]) + const timeRef = useRef(0) useEffect(() => { if (isEditing) { - inputRef.current?.focus() + // [Ivan] This component is supposed to focus on its input when it's rendered. The focus doesn't work every time because + // the initial render is triggered by a dropdown menu which at the end of the closing animation, steals focus from the input + // and triggers the blur event (which closes the input). The issue is reported at https://github.com/radix-ui/primitives/issues/3106. - // When editing starts, select text up to the last dot - if (inputRef.current) { + // [Joshen] This is to prevent accidental onBlur callbacks by checking that the onBlur event is being triggered + // within 400ms of the input field being in an edit state. 400ms is just an arbitary value which I think + // represents an "accidental" on blur + timeRef.current = Number(new Date()) + + // The focus will be attempted after a slight delay to ensure that the dropdown closing animations are complete. + setTimeout(() => { const input = inputRef.current - - // Need a slight delay to ensure focus is established - setTimeout(() => { - const fileName = input.value - const lastDotIndex = fileName.lastIndexOf('.') - const startPos = 0 - const endPos = lastDotIndex > 0 ? lastDotIndex : fileName.length - - try { - input.setSelectionRange(startPos, endPos) - } catch (e) { - console.error('Could not set selection range', e) + if (input) { + // If the input is not the active element, focus it + if (document.activeElement !== input) { + input.focus() } - }, 50) - } + + // Need a slight delay to ensure focus is established. When editing starts, select text up to the last dot + // When editing starts, select text up to the last dot + setTimeout(() => { + const fileName = input.value + const lastDotIndex = fileName.lastIndexOf('.') + const startPos = 0 + const endPos = lastDotIndex > 0 ? lastDotIndex : fileName.length + + try { + input.setSelectionRange(startPos, endPos) + } catch (e) { + console.error('Could not set selection range', e) + } + }, 50) + } + }, 200) } else { setLocalValueState(name) } @@ -136,8 +147,16 @@ const TreeViewItem = forwardRef< } }, [isLoading]) - const handleBlur = () => { - onEditSubmit?.(localValueState) + const handleBlur = (e: FocusEvent) => { + const timestamp = Number(new Date()) + const timeDiff = timestamp - timeRef.current + + if (timeDiff < 400) { + e.preventDefault() + inputRef.current?.focus() + } else { + onEditSubmit?.(localValueState) + } } const handleSubmit = (e: React.FormEvent) => { @@ -228,8 +247,9 @@ const TreeViewItem = forwardRef< {name} -
+ { setLocalValueState(e.target.value) @@ -239,7 +259,7 @@ const TreeViewItem = forwardRef< // stop keyboard down bubbling up to TreeView.root // on enter key, send onEditSubmit callback if (e.key === 'Enter') { - onEditSubmit?.(localValueState) + inputRef.current?.blur() } else if (e.key === 'Escape') { setLocalValueState(name) onEditSubmit?.(name) @@ -247,7 +267,7 @@ const TreeViewItem = forwardRef< e.stopPropagation() } }} - className="block w-full text-sm px-2 py-1 h-7 w" + className="block w-full text-sm px-2 py-1 h-7" value={localValueState} />
From d75b7c37bb2ff1b7cf4ad537946a9aedf3729d20 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 25 Apr 2025 23:59:53 +0700 Subject: [PATCH 16/86] Update all links that were pointing to settings/auth to their respective updated pages (#35281) --- .../interfaces/Auth/RateLimits/RateLimits.tsx | 10 +++------- .../components/interfaces/Linter/Linter.utils.tsx | 10 +++++----- .../ProjectSettingsLayout/ProjectSettings.Commands.tsx | 2 +- .../ResourceExhaustionWarningBanner.constants.ts | 2 +- .../ResourceExhaustionWarningBanner.tsx | 4 ++-- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx index 6d484a4026f..7b72ddd9e2a 100644 --- a/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx +++ b/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx @@ -7,6 +7,7 @@ import { toast } from 'sonner' import * as z from 'zod' import { useParams } from 'common' +import { ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' @@ -14,9 +15,6 @@ import { useAuthConfigQuery } from 'data/auth/auth-config-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, Button, Card, CardContent, @@ -25,14 +23,12 @@ import { FormField_Shadcn_, Form_Shadcn_, Input_Shadcn_, - WarningIcon, Tooltip, TooltipContent, TooltipTrigger, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { isSmtpEnabled } from '../SmtpForm/SmtpForm.utils' -import { ScaffoldSection } from 'components/layouts/Scaffold' const RateLimits = () => { const { ref: projectRef } = useParams() @@ -199,7 +195,7 @@ const RateLimits = () => {

@@ -375,7 +371,7 @@ const RateLimits = () => {

Enable anonymous logins to update this rate limit

diff --git a/apps/studio/components/interfaces/Linter/Linter.utils.tsx b/apps/studio/components/interfaces/Linter/Linter.utils.tsx index 872f90b3a85..234092e34fe 100644 --- a/apps/studio/components/interfaces/Linter/Linter.utils.tsx +++ b/apps/studio/components/interfaces/Linter/Linter.utils.tsx @@ -240,7 +240,7 @@ export const lintInfoMap: LintInfo[] = [ name: 'password_requirements_min_length', title: 'Minimum password length not set or inadequate', icon: , - link: ({ projectRef }) => `/project/${projectRef}/settings/auth`, + link: ({ projectRef }) => `/project/${projectRef}/auth/providers?provider=Email`, linkText: 'View settings', docsLink: 'https://supabase.com/docs/guides/platform/going-into-prod#security', category: 'security', @@ -258,7 +258,7 @@ export const lintInfoMap: LintInfo[] = [ name: 'auth_leaked_password_protection', title: 'Leaked Password Protection Disabled', icon: , - link: ({ projectRef }) => `/project/${projectRef}/settings/auth`, + link: ({ projectRef }) => `/project/${projectRef}/auth/providers?provider=Email`, linkText: 'View settings', docsLink: 'https://supabase.com/docs/guides/auth/password-security#password-strength-and-leaked-password-protection', @@ -268,7 +268,7 @@ export const lintInfoMap: LintInfo[] = [ name: 'auth_insufficient_mfa_options', title: 'Insufficient MFA Options', icon: , - link: ({ projectRef }) => `/project/${projectRef}/settings/auth`, + link: ({ projectRef }) => `/project/${projectRef}/auth/mfa`, linkText: 'View settings', docsLink: 'https://supabase.com/docs/guides/auth/auth-mfa', category: 'security', @@ -277,7 +277,7 @@ export const lintInfoMap: LintInfo[] = [ name: 'auth_password_policy_missing', title: 'Password Policy Missing', icon: , - link: ({ projectRef }) => `/project/${projectRef}/settings/auth`, + link: ({ projectRef }) => `/project/${projectRef}/auth/providers?provider=Email`, linkText: 'View settings', docsLink: 'https://supabase.com/docs/guides/auth/password-security', category: 'security', @@ -295,7 +295,7 @@ export const lintInfoMap: LintInfo[] = [ name: 'no_backup_admin', title: 'No Backup Admin Detected', icon: , - link: ({ projectRef }) => `/project/${projectRef}/settings/auth`, + link: ({ projectRef }) => `/project/${projectRef}/auth/mfa`, linkText: 'View settings', docsLink: 'https://supabase.com/docs/guides/auth/auth-mfa', category: 'security', diff --git a/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx b/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx index 20b432d04ad..cd9795b089a 100644 --- a/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx +++ b/apps/studio/components/layouts/ProjectSettingsLayout/ProjectSettings.Commands.tsx @@ -25,7 +25,7 @@ export function useProjectSettingsGotoCommands(options?: CommandOptions) { { id: 'nav-project-settings-auth', name: 'Auth Settings', - route: `/project/${ref}/settings/auth`, + route: `/project/${ref}/auth/providers`, defaultHidden: true, }, { diff --git a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts index 6333c7fae09..398a5160ac8 100644 --- a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts +++ b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants.ts @@ -1,5 +1,5 @@ interface ResourceWarningMessage { - // should match pathnames, ex: ('/', 'project/[ref]/auth', 'project/[ref]/database', '/project/[ref]/settings/auth') + // should match pathnames, ex: ('/', 'project/[ref]/auth', 'project/[ref]/database', '/project/[ref]/settings/api') restrictToRoutes?: string[] bannerContent: { diff --git a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx index 583872abb20..60c1ce45da7 100644 --- a/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx +++ b/apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx @@ -73,8 +73,8 @@ export const ResourceExhaustionWarningBanner = () => { null: '/project/[ref]/settings/[infra-path]', 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', + auth_email_rate_limit: '/project/[ref]/auth/rate-limits', + auth_restricted_email_sending: '/project/[ref]/auth/smtp', default: (metric: string) => `/project/[ref]/settings/[infra-path]#${metric}`, } From 30b6af4ff3a58a2a0187304206ff03b8889bf255 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Fri, 25 Apr 2025 15:22:06 -0400 Subject: [PATCH 17/86] fix (docs): non-platform integrations page (#35285) There were two bugs when trying to run the integrations page locally with NEXT_PUBLIC_IS_PLATFORM=false: 1. The IS_PLATFORM check imported from common was not evaluating correctly to a boolean. This is because I slapped a 'use client' on the entire common package last year -_-""" which caused all its imports to be evaluated to functions when used in server components. I have now moved the 'use client's down to the submodules that actually need it. 2. When the integrations submenu is empty, the navigation menu errors out because it expects all navigation items to either have children or have links. Have updated this to gracefully hide empty headers. --- .../NavigationMenu/NavigationMenuGuideListItems.tsx | 4 ++-- packages/common/feature-flags.tsx | 6 ++++-- packages/common/helpers.ts | 2 ++ packages/common/hooks/index.ts | 2 ++ packages/common/index.tsx | 2 -- packages/common/telemetry.tsx | 2 ++ 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx index cef95a4119e..cc6c052a369 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideListItems.tsx @@ -169,9 +169,9 @@ const Content = (props) => { ) })}
- ) : ( + ) : x.url ? ( - )} + ) : null}
) })} diff --git a/packages/common/feature-flags.tsx b/packages/common/feature-flags.tsx index f79131f2fea..8e85b519fd4 100644 --- a/packages/common/feature-flags.tsx +++ b/packages/common/feature-flags.tsx @@ -1,3 +1,5 @@ +'use client' + import { FlagValues } from '@vercel/flags/react' import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' @@ -123,9 +125,9 @@ export const FeatureFlagProvider = ({ return ( - {/* + {/* [Joshen] Just support configcat flags in Vercel flags for now for simplicity - although I think it should be fairly simply to support PH too + although I think it should be fairly simply to support PH too */} {children} diff --git a/packages/common/helpers.ts b/packages/common/helpers.ts index 749eb556a4d..d48bd3a8b0f 100644 --- a/packages/common/helpers.ts +++ b/packages/common/helpers.ts @@ -1,3 +1,5 @@ +'use client' + import { useSyncExternalStore } from 'react' import type * as React from 'react' diff --git a/packages/common/hooks/index.ts b/packages/common/hooks/index.ts index 0e3a707f73b..5f6e518e4ac 100644 --- a/packages/common/hooks/index.ts +++ b/packages/common/hooks/index.ts @@ -1,3 +1,5 @@ +'use client' + export * from './useAnchorObserver' export * from './useBreakpoint' export * from './useConstant' diff --git a/packages/common/index.tsx b/packages/common/index.tsx index 25a77e6adb4..bb0ac6c5552 100644 --- a/packages/common/index.tsx +++ b/packages/common/index.tsx @@ -1,5 +1,3 @@ -'use client' - export * from './auth' export * from './constants' export * from './database-types' diff --git a/packages/common/telemetry.tsx b/packages/common/telemetry.tsx index 18d46f585cf..29278df8cfa 100644 --- a/packages/common/telemetry.tsx +++ b/packages/common/telemetry.tsx @@ -1,3 +1,5 @@ +'use client' + import { components } from 'api-types' import { useRouter } from 'next/compat/router' import { usePathname } from 'next/navigation' From b26e921878db4e3d356deff6327ca911cdffd26c Mon Sep 17 00:00:00 2001 From: Wen Bo Xie <5532241+w3b6x9@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:41:04 -0400 Subject: [PATCH 18/86] docs: add updating OAuth scopes guide (#35287) --- .../integrations/build-a-supabase-integration/oauth-scopes.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/docs/content/guides/integrations/build-a-supabase-integration/oauth-scopes.mdx b/apps/docs/content/guides/integrations/build-a-supabase-integration/oauth-scopes.mdx index 3ce86288029..ef13b111f2c 100644 --- a/apps/docs/content/guides/integrations/build-a-supabase-integration/oauth-scopes.mdx +++ b/apps/docs/content/guides/integrations/build-a-supabase-integration/oauth-scopes.mdx @@ -15,6 +15,8 @@ Scopes restrict access to the specific [Supabase Management API endpoints](/docs Scopes are set when you [create an OAuth app](/docs/guides/platform/oauth-apps/build-a-supabase-integration#create-an-oauth-app) in the Supabase Dashboard. +You can update scopes of your OAuth app at any time, but existing OAuth app users will need to re-authorize your app via the [OAuth flow](/docs/guides/integrations/build-a-supabase-integration#implementing-the-oauth-20-flow) to apply the new scopes. + ## Available scopes | Name | Type | Description | From 634f86d2d615c33ad15223950a1a5970e703b9e0 Mon Sep 17 00:00:00 2001 From: divit <27228526+delgado3d@users.noreply.github.com> Date: Sat, 26 Apr 2025 12:45:11 +0530 Subject: [PATCH 19/86] feat: disable us east and change default to us west (#35296) --- apps/studio/data/misc/get-default-region-query.ts | 2 +- packages/shared-data/regions.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/studio/data/misc/get-default-region-query.ts b/apps/studio/data/misc/get-default-region-query.ts index af9f135f336..9ec4175bb42 100644 --- a/apps/studio/data/misc/get-default-region-query.ts +++ b/apps/studio/data/misc/get-default-region-query.ts @@ -12,7 +12,7 @@ import type { CloudProvider } from 'shared-data' import { AWS_REGIONS, FLY_REGIONS } from 'shared-data' const RESTRICTED_POOL = [ - 'EAST_US', + 'WEST_US', 'CENTRAL_EU', 'NORTH_EU', 'WEST_EU', diff --git a/packages/shared-data/regions.ts b/packages/shared-data/regions.ts index 50213795744..cb703d33ba2 100644 --- a/packages/shared-data/regions.ts +++ b/packages/shared-data/regions.ts @@ -7,11 +7,11 @@ export const AWS_REGIONS = { displayName: 'West US (North California)', location: [37.774929, -122.419418], }, - EAST_US: { - code: 'us-east-1', - displayName: 'East US (North Virginia)', - location: [37.926868, -78.024902], - }, + // EAST_US: { + // code: 'us-east-1', + // displayName: 'East US (North Virginia)', + // location: [37.926868, -78.024902], + // }, EAST_US_2: { code: 'us-east-2', displayName: 'East US (Ohio)', location: [39.9612, -82.9988] }, CENTRAL_CANADA: { code: 'ca-central-1', From 3f3147002bd07a0b01cb2fa83ee6e4901d178a8b Mon Sep 17 00:00:00 2001 From: Justin <127172022+jl33-ai@users.noreply.github.com> Date: Sun, 27 Apr 2025 23:45:55 +1000 Subject: [PATCH 20/86] Update all languages to use `->>` operator in JSON query examples (#35266) - The docs explain "If you want the data returned as `text`, use the `->>` operator." - Only the SQL example shows this using the `description` field - All language should use `->>` for the description field --- apps/docs/content/guides/database/json.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/docs/content/guides/database/json.mdx b/apps/docs/content/guides/database/json.mdx index 5a067043f46..1619c015cb6 100644 --- a/apps/docs/content/guides/database/json.mdx +++ b/apps/docs/content/guides/database/json.mdx @@ -417,7 +417,7 @@ from books; ```js const { data, error } = await supabase.from('books').select(` title, - description: metadata->description, + description: metadata->>description, price: metadata->price, low_age: metadata->ages->0, high_age: metadata->ages->1 @@ -433,7 +433,7 @@ try await supabase .select( """ title, - description: metadata->description, + description: metadata->>description, price: metadata->price, low_age: metadata->ages->0, high_age: metadata->ages->1 @@ -448,7 +448,7 @@ try await supabase ```kotlin val data = supabase.from("books").select(Columns.raw(""" title, - description: metadata->description, + description: metadata->>description, price: metadata->price, low_age: metadata->ages->0, high_age: metadata->ages->1 @@ -461,7 +461,7 @@ val data = supabase.from("books").select(Columns.raw(""" ```python data = supabase.from_('books').select(""" title, - description: metadata->description, + description: metadata->>description, price: metadata->price, low_age: metadata->ages->0, high_age: metadata->ages->1 From dd03625f557d71c79615d6df7d9c33cd3c704486 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 28 Apr 2025 10:33:37 +0700 Subject: [PATCH 21/86] Chore/refactor triggers to remove mobx (#35254) * Set up create trigger * Hook up update trigger * Deprecate CreateTrigger * Small clean up DeleteTrigger * Update apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx Co-authored-by: Alaister Young --------- Co-authored-by: Alaister Young --- .../Database/Triggers/ChooseFunctionForm.tsx | 30 +- .../Database/Triggers/CreateTrigger.tsx | 792 ------------------ .../Database/Triggers/DeleteTrigger.tsx | 9 +- .../Database/Triggers/TriggerSheet.tsx | 448 ++++++++++ .../Database/Triggers/Triggers.constants.ts | 46 + .../Triggers/TriggersList/TriggersList.tsx | 11 +- .../components/interfaces/Database/index.ts | 3 - .../pages/project/[ref]/database/triggers.tsx | 31 +- 8 files changed, 540 insertions(+), 830 deletions(-) delete mode 100644 apps/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx create mode 100644 apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx create mode 100644 apps/studio/components/interfaces/Database/Triggers/Triggers.constants.ts diff --git a/apps/studio/components/interfaces/Database/Triggers/ChooseFunctionForm.tsx b/apps/studio/components/interfaces/Database/Triggers/ChooseFunctionForm.tsx index 1fcae2033dc..809bb86edd0 100644 --- a/apps/studio/components/interfaces/Database/Triggers/ChooseFunctionForm.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/ChooseFunctionForm.tsx @@ -9,27 +9,33 @@ import { Button, SidePanel } from 'ui' import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState' import InformationBox from 'components/ui/InformationBox' import SqlEditor from 'components/ui/SqlEditor' -import type { DatabaseFunction } from 'data/database-functions/database-functions-query' -import { HelpCircle, Terminal, ChevronDown } from 'lucide-react' +import { + useDatabaseFunctionsQuery, + type DatabaseFunction, +} from 'data/database-functions/database-functions-query' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { ChevronDown, HelpCircle, Terminal } from 'lucide-react' export interface ChooseFunctionFormProps { - triggerFunctions: DatabaseFunction[] visible: boolean - onChange: (id: number) => void setVisible: (value: boolean) => void + onChange: (fn: DatabaseFunction) => void } -const ChooseFunctionForm = ({ - triggerFunctions, - visible, - onChange, - setVisible, -}: ChooseFunctionFormProps) => { +const ChooseFunctionForm = ({ visible, onChange, setVisible }: ChooseFunctionFormProps) => { + const project = useSelectedProject() + + const { data = [] } = useDatabaseFunctionsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const triggerFunctions = data.filter((fn) => fn.return_type === 'trigger') const hasPublicSchemaFunctions = triggerFunctions.length >= 1 const functionSchemas = lodashMap(uniqBy(triggerFunctions, 'schema'), 'schema') - function selectFunction(id: number) { - onChange(id) + const selectFunction = (id: number) => { + const fn = triggerFunctions.find((x) => x.id === id) + if (!!fn) onChange(fn) setVisible(!visible) } diff --git a/apps/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx b/apps/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx deleted file mode 100644 index 8cf605d89f1..00000000000 --- a/apps/studio/components/interfaces/Database/Triggers/CreateTrigger.tsx +++ /dev/null @@ -1,792 +0,0 @@ -import { has, isEmpty, mapValues, union, without } from 'lodash' -import { makeAutoObservable } from 'mobx' -import { observer, useLocalObservable } from 'mobx-react-lite' -import { createContext, useContext, useEffect, useState } from 'react' -import SVG from 'react-inlinesvg' -import { toast } from 'sonner' -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 { - DatabaseFunction, - useDatabaseFunctionsQuery, -} from 'data/database-functions/database-functions-query' -import { useDatabaseTriggerCreateMutation } from 'data/database-triggers/database-trigger-create-mutation' -import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/database-trigger-update-mutation' -import { useTablesQuery } from 'data/tables/tables-query' -import { BASE_PATH } from 'lib/constants' -import { PROTECTED_SCHEMAS } from 'lib/constants/schemas' -import { PauseCircle, PlayCircle, Terminal } from 'lucide-react' -import type { Dictionary } from 'types' -import ChooseFunctionForm from './ChooseFunctionForm' - -class CreateTriggerFormState { - id: number | undefined - originalName: string | undefined - // @ts-ignore - activation: { value: string; error?: string } - // @ts-ignore - enabledMode: { value: string } - // @ts-ignore - events: { value: string[]; error?: string } - // @ts-ignore - functionName: { value: string; error?: string } - // @ts-ignore - functionSchema: { value: string; error?: string } - // @ts-ignore - orientation: { value: string; error?: string } - // @ts-ignore - name: { value: string; error?: string } - // @ts-ignore - schema: { value: string } - // @ts-ignore - table: { value: string } - // @ts-ignore - tableId: { value: number; error?: string } - // @ts-ignore - condition: { value: string } - - constructor() { - makeAutoObservable(this) - this.reset() - } - - get requestBody() { - return { - id: this.id, - activation: this.activation.value, - enabled_mode: this.enabledMode.value, - events: this.events.value, - function_name: this.functionName.value, - function_schema: this.functionSchema.value, - orientation: this.orientation.value, - name: this.name.value, - schema: this.schema.value, - table: this.table.value, - } - } - - reset(trigger?: Dictionary) { - this.id = trigger?.id - this.originalName = trigger?.name - this.activation = { value: trigger?.activation ?? 'BEFORE' } - this.enabledMode = { value: trigger?.enabled_mode ?? 'ORIGIN' } - this.events = { value: trigger?.events ?? [] } - this.functionName = { value: trigger?.function_name ?? '' } - this.functionSchema = { value: trigger?.function_schema ?? '' } - this.orientation = { value: trigger?.orientation ?? 'STATEMENT' } - this.name = { value: trigger?.name ?? '' } - this.schema = { value: trigger?.schema ?? '' } - this.table = { value: trigger?.table ?? '' } - this.tableId = { value: trigger?.table_id ?? '' } - this.condition = { value: trigger?.condition ?? '' } - } - - update(state: Dictionary) { - this.activation = state.activation - this.enabledMode = state.enabledMode - this.events = state.events - this.functionName = state.functionName - this.functionSchema = state.functionSchema - this.orientation = state.orientation - this.name = state.name - this.schema = state.schema - this.table = state.table - this.tableId = state.tableId - } -} - -interface ICreateTriggerStore { - chooseFunctionFormVisible: boolean - formState: CreateTriggerFormState - meta: any - tables: Dictionary[] - onFormChange: ({ key, value }: { key: string; value: any }) => void - onSelectFunction: (fn: DatabaseFunction) => void - setChooseFunctionFormVisible: (value: boolean) => void - setDefaultSelectedTable: () => void - setTables: (value: any[]) => void - validateForm: () => boolean -} - -class CreateTriggerStore implements ICreateTriggerStore { - chooseFunctionFormVisible = false - formState = new CreateTriggerFormState() - meta = null - tables = [] - isDirty = false - - constructor() { - makeAutoObservable(this) - } - - get isEditing() { - return this.formState.id != undefined - } - - setChooseFunctionFormVisible = (value: boolean) => { - this.chooseFunctionFormVisible = value - } - - // set first table as default selection - setDefaultSelectedTable = () => { - if (this.tables?.length != 0) { - this.formState.table.value = (this.tables[0] as any).name - this.formState.schema.value = (this.tables[0] as any).schema - this.formState.tableId.value = (this.tables[0] as any).id - } - } - - setisDirty = (value: boolean) => { - this.isDirty = value - } - - setTables = (value: any[]) => { - this.tables = value - .sort((a, b) => a.schema.localeCompare(b.schema)) - .filter((a) => !PROTECTED_SCHEMAS.includes(a.schema)) as any - this.setDefaultSelectedTable() - } - - onFormChange = ({ key, value }: { key: string; value: any }) => { - this.isDirty = true - if (has(this.formState, key)) { - const temp = (this.formState as any)[key] - // @ts-ignore - this.formState[key] = { ...temp, value, error: undefined } - } else { - // @ts-ignore - this.formState[key] = { value } - } - } - - onSelectFunction = (fn: DatabaseFunction) => { - this.formState.functionName.value = fn.name - this.formState.functionSchema.value = fn.schema - } - - validateForm = () => { - let isValidated = true - const _state = mapValues(this.formState, (x: { value: any }, key: string) => { - switch (key) { - case 'name': { - if (isEmpty(x.value) || hasWhitespace(x.value)) { - isValidated = false - return { ...x, error: 'Invalid trigger name' } - } else { - return x - } - } - case 'activation': { - if (isEmpty(x.value)) { - isValidated = false - return { ...x, error: 'you have an error' } - } else { - return x - } - } - case 'events': { - if (isEmpty(x.value)) { - isValidated = false - return { ...x, error: 'Select at least 1 event' } - } else { - return x - } - } - case 'tableId': { - if (isEmpty(`${x.value}`)) { - isValidated = false - return { ...x, error: 'You must choose a table' } - } else { - return x - } - } - default: - return x - } - }) - if (!isValidated) { - this.formState.update(_state) - } - return isValidated - } -} - -function hasWhitespace(value: string) { - return /\s/.test(value) -} - -const CreateTriggerContext = createContext(null) - -interface CreateTriggerProps { - trigger?: any - visible: boolean - setVisible: (value: boolean) => void -} - -const CreateTrigger = ({ trigger, visible, setVisible }: CreateTriggerProps) => { - const { project } = useProjectContext() - const [isClosingPanel, setIsClosingPanel] = useState(false) - const _localState = useLocalObservable(() => new CreateTriggerStore()) - - useTablesQuery( - { - projectRef: project?.ref, - connectionString: project?.connectionString, - }, - { - onSuccess(tables) { - if (_localState.tables.length <= 0) { - _localState.setTables(tables) - } - }, - } - ) - - const { data } = useDatabaseFunctionsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - const triggerFunctions = (data ?? []).filter((fn) => fn.return_type === 'trigger') - - const { mutate: createDatabaseTrigger, isLoading: isCreating } = - useDatabaseTriggerCreateMutation() - const { mutate: updateDatabaseTrigger, isLoading: isUpdating } = - useDatabaseTriggerUpdateMutation() - - useEffect(() => { - _localState.setisDirty(false) - if (trigger) { - _localState.formState.reset(trigger) - } else { - _localState.formState.reset() - _localState.setDefaultSelectedTable() - } - }, [visible, trigger]) - - async function handleSubmit() { - if (!project) return console.error('Project is required') - - if (_localState.validateForm()) { - const body = _localState.formState.requestBody - if (_localState.isEditing && body.id) { - updateDatabaseTrigger( - { - projectRef: project?.ref, - connectionString: project?.connectionString, - id: body.id, - payload: body, - }, - { - onSuccess: () => { - toast.success(`Successfully updated trigger ${body.name}`) - setVisible(!visible) - }, - onError: (error) => { - toast.error(`Failed to update trigger: ${error.message}`) - }, - } - ) - } else { - createDatabaseTrigger( - { - projectRef: project?.ref, - connectionString: project?.connectionString, - payload: body, - }, - { - onSuccess: () => { - toast.success(`Successfully created trigger ${body.name}`) - setVisible(!visible) - }, - onError: (error) => { - toast.error(`Failed to create trigger: ${error.message}`) - }, - } - ) - } - } - } - - const hasPublicTables = _localState.tables.length >= 1 - - const isClosingSidePanel = () => { - _localState.isDirty ? setIsClosingPanel(true) : setVisible(!visible) - } - - return ( - <> - - {hasPublicTables ? ( -
- -
- {_localState.isEditing ? ( -
- - - {_localState.formState.condition.value && } -
- ) : ( - <> -
- -
- -
-
Conditions to fire trigger
- - - - -
- - - - )} -
- { - const selectedFn = (data ?? []).find((fn) => fn.id === id) - if (selectedFn) _localState.onSelectFunction(selectedFn) - }} - /> -
- setIsClosingPanel(false)} - onConfirm={() => { - setIsClosingPanel(false) - setVisible(!visible) - }} - > -

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

-
-
- ) : ( - - )} -
- - ) -} - -export default observer(CreateTrigger) - -const InputName = observer(({}) => { - const _localState = useContext(CreateTriggerContext) - return ( - - _localState!.onFormChange({ - key: 'name', - value: e.target.value, - }) - } - size="small" - error={_localState!.formState.name.error} - descriptionText="The name is also stored as the actual postgres name of the trigger. Do not use spaces/whitespace." - /> - ) -}) - -const SelectEnabledMode = observer(({}) => { - const _localState = useContext(CreateTriggerContext) - return ( - - _localState!.onFormChange({ - key: 'enabledMode', - value: value, - }) - } - size="small" - descriptionText="Determines if a trigger should or should not fire. Can also be used to disable a trigger, but not delete it." - > - { - return ( -
- ) - }} - value="ORIGIN" - label="Origin" - > - Origin - This is a default behavior -
- { - return ( -
- ) - }} - value="REPLICA" - label="Replica" - > - Replica - - Will only fire if the session is in “replica” mode - -
- { - return ( -
- ) - }} - value="ALWAYS" - label="Always" - > - Always - - Will fire regardless of the current replication role - -
- { - return ( -
- ) - }} - value="DISABLED" - label="Disabled" - > - Disabled - Will not fire -
-
- ) -}) - -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 ( - - _localState!.onFormChange({ - key: 'orientation', - value: value, - }) - } - size="small" - descriptionText="Identifies whether the trigger fires once for each processed row or once for each statement" - > - - Row - fires once for each processed row - - - Statement - fires once for each statement - - - ) -}) - -const ListboxTable = observer(({}) => { - const _localState = useContext(CreateTriggerContext) - - return ( - { - const _table = _localState!.tables.find((x) => x.id === id) - if (_table) { - _localState!.onFormChange({ - key: 'table', - value: _table.name, - }) - _localState!.onFormChange({ - key: 'schema', - value: _table.schema, - }) - _localState!.onFormChange({ - key: 'tableId', - value: id, - }) - } - }} - size="small" - error={_localState!.formState.tableId.error} - descriptionText="This is the table the trigger will watch for changes. You can only select 1 table for a trigger." - > - {_localState!.tables.map((x) => { - return ( - ( -
- - code.replace(/svg/, 'svg class="m-auto text-color-inherit"') - } - /> -
- )} - > -
-

{x.schema}

-

{x.name}

-
-
- ) - })} -
- ) -}) - -const CheckboxEvents = observer(({}) => { - const _localState = useContext(CreateTriggerContext) - return ( - // @ts-ignore - { - const temp = _localState!.formState.events.value - const value = e.target.checked - ? union(temp, [e.target.value]) - : without(temp, e.target.value) - _localState!.onFormChange({ - key: 'events', - value: value, - }) - }} - error={_localState!.formState.events.error} - > - - - - - ) -}) - -const ListboxActivation = observer(({}) => { - const _localState = useContext(CreateTriggerContext) - return ( - { - _localState!.onFormChange({ - key: 'activation', - value: _value, - }) - }} - value={_localState!.formState.activation.value} - layout="horizontal" - size="small" - error={_localState!.formState.activation.error} - > - ( -
- -
- )} - > -
- {'before'} - - Trigger fires before the operation is attempted - -
-
- ( -
- -
- )} - > -
- {'after'} - - Trigger fires after the operation has completed - -
-
-
- ) -}) - -const FunctionForm = observer(({}) => { - const _localState = useContext(CreateTriggerContext) - - return ( -
-
-
Function to trigger
-
-
- {isEmpty(_localState!.formState.functionName.value) ? ( - - ) : ( - - )} -
-
- ) -}) - -const FunctionEmpty = observer(({}) => { - const _localState = useContext(CreateTriggerContext) - return ( - - ) -}) - -const FunctionWithArguments = observer(({}) => { - const _localState = useContext(CreateTriggerContext) - - return ( - <> -
-
-
- -
-
-

{_localState!.formState.functionName.value}

-
- {_localState!.formState.functionSchema.value} -
-
-
- -
- - ) -}) diff --git a/apps/studio/components/interfaces/Database/Triggers/DeleteTrigger.tsx b/apps/studio/components/interfaces/Database/Triggers/DeleteTrigger.tsx index f2131e29369..d57da933a45 100644 --- a/apps/studio/components/interfaces/Database/Triggers/DeleteTrigger.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/DeleteTrigger.tsx @@ -1,3 +1,4 @@ +import { PostgresTrigger } from '@supabase/postgres-meta' import { toast } from 'sonner' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -5,12 +6,12 @@ import { useDatabaseTriggerDeleteMutation } from 'data/database-triggers/databas import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal' interface DeleteTriggerProps { - trigger?: any + trigger?: PostgresTrigger visible: boolean setVisible: (value: boolean) => void } -const DeleteTrigger = ({ trigger, visible, setVisible }: DeleteTriggerProps) => { +export const DeleteTrigger = ({ trigger, visible, setVisible }: DeleteTriggerProps) => { const { project } = useProjectContext() const { id, name, schema } = trigger ?? {} @@ -45,7 +46,7 @@ const DeleteTrigger = ({ trigger, visible, setVisible }: DeleteTriggerProps) => loading={isLoading} confirmLabel={`Delete trigger ${name}`} confirmPlaceholder="Type in name of trigger" - confirmString={name} + confirmString={name ?? ''} text={ <> This will delete your trigger called{' '} @@ -59,5 +60,3 @@ const DeleteTrigger = ({ trigger, visible, setVisible }: DeleteTriggerProps) => /> ) } - -export default DeleteTrigger diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx new file mode 100644 index 00000000000..78b68d7f8ed --- /dev/null +++ b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx @@ -0,0 +1,448 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { Terminal } from 'lucide-react' +import { useEffect, useState } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' +import { toast } from 'sonner' +import * as z from 'zod' + +import { PostgresTrigger } from '@supabase/postgres-meta' +import FormBoxEmpty from 'components/ui/FormBoxEmpty' +import { useDatabaseTriggerCreateMutation } from 'data/database-triggers/database-trigger-create-mutation' +import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/database-trigger-update-mutation' +import { useTablesQuery } from 'data/tables/tables-query' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { PROTECTED_SCHEMAS } from 'lib/constants/schemas' +import { + Button, + Checkbox_Shadcn_, + cn, + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Input_Shadcn_, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Separator, + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetTitle, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import ChooseFunctionForm from './ChooseFunctionForm' +import { + TRIGGER_ENABLED_MODES, + TRIGGER_EVENTS, + TRIGGER_ORIENTATIONS, + TRIGGER_TYPES, +} from './Triggers.constants' + +const formId = 'create-trigger' + +const FormSchema = z.object({ + name: z + .string() + .min(1, 'Please provide a name for your trigger') + .regex(/^\S+$/, 'Name should not contain spaces or whitespaces'), + schema: z.string(), + table: z.string(), + activation: z.string(), + enabled_mode: z.string(), + orientation: z.string(), + function_name: z.string(), + function_schema: z.string(), + events: z.array(z.string()).min(1, 'Please select at least one event'), + + // For UI handling, not to be passed to the final request + tableId: z.string().optional(), +}) + +const defaultValues = { + name: '', + schema: '', + table: '', + activation: 'AFTER', + orientation: 'ROW', + function_name: '', + function_schema: '', + enabled_mode: 'ORIGIN', + events: [], +} + +interface TriggerSheetProps { + selectedTrigger?: PostgresTrigger + open: boolean + setOpen: (val: boolean) => void +} + +export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetProps) => { + const project = useSelectedProject() + + const [showFunctionSelector, setShowFunctionSelector] = useState(false) + + const { mutate: createDatabaseTrigger, isLoading: isCreating } = useDatabaseTriggerCreateMutation( + { + onSuccess: (res) => { + toast.success(`Successfully created trigger ${res.name}`) + setOpen(false) + }, + onError: (error) => { + toast.error(`Failed to create trigger: ${error.message}`) + }, + } + ) + const { mutate: updateDatabaseTrigger, isLoading: isUpdating } = useDatabaseTriggerUpdateMutation( + { + onSuccess: (res) => { + toast.success(`Successfully updated trigger ${res.name}`) + setOpen(false) + }, + onError: (error) => { + toast.error(`Failed to update trigger: ${error.message}`) + }, + } + ) + + const { data = [], isSuccess } = useTablesQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const tables = data + .sort((a, b) => a.schema.localeCompare(b.schema)) + .filter((a) => !PROTECTED_SCHEMAS.includes(a.schema)) + const isEditing = !!selectedTrigger + + const form = useForm>({ + mode: 'onSubmit', + reValidateMode: 'onSubmit', + resolver: zodResolver(FormSchema), + defaultValues, + }) + const { function_name, function_schema } = form.watch() + + const onSubmit: SubmitHandler> = async (values) => { + if (!project) return console.error('Project is required') + const { tableId, ...payload } = values + + if (isEditing) { + updateDatabaseTrigger({ + projectRef: project?.ref, + connectionString: project?.connectionString, + id: selectedTrigger.id, + payload: { name: payload.name, enabled_mode: payload.enabled_mode }, + }) + } else { + createDatabaseTrigger({ + projectRef: project?.ref, + connectionString: project?.connectionString, + payload, + }) + } + } + + useEffect(() => { + if (open && isSuccess) { + form.clearErrors() + + if (isEditing) { + form.reset(selectedTrigger) + } else { + form.reset({ + ...defaultValues, + tableId: tables[0].id.toString(), + table: tables[0].name, + schema: tables[0].schema, + }) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, isSuccess]) + + return ( + <> + + + + Create a new database trigger + + + +
+ ( + + + + + + )} + /> + + {isEditing ? ( + ( + + + + + { + TRIGGER_ENABLED_MODES.find((option) => option.value === field.value) + ?.label + } + + + {TRIGGER_ENABLED_MODES.map((option) => ( + +

{option.label}

+

{option.description}

+
+ ))} +
+
+
+
+ )} + /> + ) : ( + <> + + + ( + + + { + const table = tables.find((x) => x.id.toString() === val) + if (table) { + form.setValue('table', table.name) + form.setValue('schema', table.schema) + } + }} + > + + + + + {tables.map((table) => ( + + {table.schema}. + {table.name} + + ))} + + + + + )} + /> + + ( + + {TRIGGER_EVENTS.map((event) => ( + ( + + + { + return checked + ? field.onChange([...field.value, event.value]) + : field.onChange( + field.value?.filter((value) => value !== event.value) + ) + }} + /> + + + )} + /> + ))} + + )} + /> + + ( + + + + + {TRIGGER_TYPES.find((option) => option.value === field.value)?.label} + + + {TRIGGER_TYPES.map((option) => ( + +

{option.label}

+

{option.description}

+
+ ))} +
+
+
+
+ )} + /> + + ( + + + + + { + TRIGGER_ORIENTATIONS.find((option) => option.value === field.value) + ?.label + } + + + {TRIGGER_ORIENTATIONS.map((option) => ( + +

{option.label}

+

{option.description}

+
+ ))} +
+
+
+
+ )} + /> + + + +
+

Function to trigger

+ {function_name.length === 0 ? ( + + ) : ( +
+
+
+ +
+

+ {function_schema} + .{function_name} +

+
+ +
+ )} +
+ + )} + +
+ + + + + +
+
+ + { + form.setValue('function_name', fn.name) + form.setValue('function_schema', fn.schema) + }} + /> + + ) +} diff --git a/apps/studio/components/interfaces/Database/Triggers/Triggers.constants.ts b/apps/studio/components/interfaces/Database/Triggers/Triggers.constants.ts new file mode 100644 index 00000000000..d0e194b9483 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Triggers/Triggers.constants.ts @@ -0,0 +1,46 @@ +export const TRIGGER_EVENTS = [ + { value: 'INSERT', label: 'Insert', description: 'Any insert operation on the table' }, + { + value: 'UPDATE', + label: 'Update', + description: 'Any update operation of any column on the table', + }, + { value: 'DELETE', label: 'Delete', description: 'Any delete operation of a record' }, +] + +export const TRIGGER_TYPES = [ + { + value: 'BEFORE', + label: 'Before the event', + description: 'Trigger fires before the operation is attempted', + }, + { + value: 'AFTER', + label: 'After the event', + description: 'Trigger fires before the operation has completed', + }, +] + +export const TRIGGER_ORIENTATIONS = [ + { + value: 'ROW', + label: 'Row', + description: 'Fires once for each processed row', + }, + { + value: 'STATEMENT', + label: 'Statement', + description: 'Fires once for each statement', + }, +] + +export const TRIGGER_ENABLED_MODES = [ + { value: 'ORIGIN', label: 'Origin', description: 'This is the default behaviour' }, + { + value: 'REPLICA', + label: 'Replica', + description: 'Will only fire if the session is in "replica" mode', + }, + { value: 'ALWAYS', label: 'Always', description: 'Will fire regardless of the replication role' }, + { value: 'DISABLED', label: 'Disabled', description: 'Will not fire' }, +] diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx index b7cc7a9c27f..0097ecc6907 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx @@ -1,6 +1,7 @@ +import { PostgresTrigger } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' import { noop, partition } from 'lodash' -import { Search } from 'lucide-react' +import { Plus, Search } from 'lucide-react' import { useState } from 'react' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -23,8 +24,8 @@ import TriggerList from './TriggerList' interface TriggersListProps { createTrigger: () => void - editTrigger: (trigger: any) => void - deleteTrigger: (trigger: any) => void + editTrigger: (trigger: PostgresTrigger) => void + deleteTrigger: (trigger: PostgresTrigger) => void } const TriggersList = ({ @@ -111,6 +112,7 @@ const TriggersList = ({
} onClick={() => createTrigger()} className="flex-grow" tooltip={{ @@ -122,8 +124,9 @@ const TriggersList = ({ }, }} > - Create a new trigger + New trigger + { - const [selectedTrigger, setSelectedTrigger] = useState() - const [showCreateTriggerForm, setShowCreateTriggerForm] = useState(false) - const [showDeleteTriggerForm, setShowDeleteTriggerForm] = useState(false) const { setEditorPanel } = useAppStateSnapshot() const isInlineEditorEnabled = useIsInlineEditorEnabled() + const [selectedTrigger, setSelectedTrigger] = useState() + const [showCreateTriggerForm, setShowCreateTriggerForm] = useState(false) + const [showDeleteTriggerForm, setShowDeleteTriggerForm] = useState(false) + const canReadTriggers = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers') const isPermissionsLoaded = usePermissionsLoaded() @@ -42,7 +45,7 @@ execute function function_name();`, } } - const editTrigger = (trigger: any) => { + const editTrigger = (trigger: PostgresTrigger) => { if (isInlineEditorEnabled) { const sql = generateTriggerCreateSQL(trigger) setEditorPanel({ @@ -58,7 +61,7 @@ execute function function_name();`, } } - const deleteTrigger = (trigger: any) => { + const deleteTrigger = (trigger: PostgresTrigger) => { setSelectedTrigger(trigger) setShowDeleteTriggerForm(true) } @@ -85,10 +88,10 @@ execute function function_name();`,
- Date: Mon, 28 Apr 2025 12:54:51 +0700 Subject: [PATCH 22/86] Fix tabs interface not persisting tabs order when closing a tab (#35216) Refactor handleTabClose which fixes tabs not persisting sort order when closing a tab --- apps/studio/state/tabs.ts | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/apps/studio/state/tabs.ts b/apps/studio/state/tabs.ts index 7ddd4ea72ce..c04c828116a 100644 --- a/apps/studio/state/tabs.ts +++ b/apps/studio/state/tabs.ts @@ -291,29 +291,26 @@ export const handleTabClose = ({ if (!ref) return const store = getTabsStore(ref) - // tabs without the one we're closing - const currentTab = store.tabsMap[id] - const currentTabs = Object.values(store.tabsMap).filter((tab) => tab.id !== id) + const tabBeingClosed = store.tabsMap[id] + const tabsAfterClosing = Object.values(store.tabsMap).filter((tab) => tab.id !== id) const nextTabId = !editor ? undefined - : currentTabs.filter((tab) => { + : tabsAfterClosing.filter((tab) => { return editorEntityTypes[editor]?.includes(tab.type) })[0]?.id - delete store.tabsMap[id] - if (currentTab) { - // Update store - // If the tab being removed is logged in the store, update the open tabs - store.openTabs = [...currentTabs.map((tab) => tab.id).filter((id) => id !== currentTab.id)] + const { [id]: value, ...otherTabs } = store.tabsMap + store.tabsMap = otherTabs + + if (tabBeingClosed) { + const updatedOpenTabs = [...store.openTabs].filter((x) => x !== id) + store.openTabs = updatedOpenTabs } - // Check if there is a preview tab and if it matches the tab being closed - if (store.previewTabId) { - if (store.previewTabId === id) { - // remove the preview tab if it matches the tab being closed - store.previewTabId = undefined - } + // Remove the preview tab if it matches the tab being closed + if (store.previewTabId === id) { + store.previewTabId = undefined } // [Joshen] Only navigate away if we're closing the tab that's currently in focus @@ -325,7 +322,7 @@ export const handleTabClose = ({ onClearDashboardHistory() // If no tabs of same type, go to the home of the current section - switch (currentTab?.type) { + switch (tabBeingClosed?.type) { case 'sql': router.push(`/project/${router.query.ref}/sql`) break From 29fc5a7a8208635008cc2734c85c4ca7637ddfbd Mon Sep 17 00:00:00 2001 From: divit <27228526+delgado3d@users.noreply.github.com> Date: Mon, 28 Apr 2025 11:46:05 +0530 Subject: [PATCH 23/86] feat: re enable east US (#35319) --- packages/shared-data/regions.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/shared-data/regions.ts b/packages/shared-data/regions.ts index cb703d33ba2..50213795744 100644 --- a/packages/shared-data/regions.ts +++ b/packages/shared-data/regions.ts @@ -7,11 +7,11 @@ export const AWS_REGIONS = { displayName: 'West US (North California)', location: [37.774929, -122.419418], }, - // EAST_US: { - // code: 'us-east-1', - // displayName: 'East US (North Virginia)', - // location: [37.926868, -78.024902], - // }, + EAST_US: { + code: 'us-east-1', + displayName: 'East US (North Virginia)', + location: [37.926868, -78.024902], + }, EAST_US_2: { code: 'us-east-2', displayName: 'East US (Ohio)', location: [39.9612, -82.9988] }, CENTRAL_CANADA: { code: 'ca-central-1', From 70a8888b0a4f4e258fe3801a797880621a73b9ac Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 28 Apr 2025 13:16:46 +0700 Subject: [PATCH 24/86] Chore/refactor storage explorer store part 1 (#35271) * Use Context for Storage Explorer valtio store + shift views to new store * Shift downloadFile from mobx store to utils * Shift formatTime outside of StorageExplorerStore to StorageExplroer.utils * Clean up * Address comments --- .../layouts/ProjectLayout/ProjectContext.tsx | 13 +- .../layouts/StorageLayout/StorageMenu.tsx | 9 +- .../to-be-cleaned/Storage/Storage.types.ts | 4 +- .../StorageExplorer/ColumnContextMenu.tsx | 8 +- .../Storage/StorageExplorer/FileExplorer.tsx | 14 +- .../StorageExplorer/FileExplorerColumn.tsx | 12 +- .../StorageExplorer/FileExplorerHeader.tsx | 10 +- .../FileExplorerHeaderSelection.tsx | 6 +- .../StorageExplorer/FileExplorerRow.tsx | 13 +- .../StorageExplorer/ItemContextMenu.tsx | 6 +- .../Storage/StorageExplorer/PreviewPane.tsx | 10 +- .../StorageExplorer/StorageExplorer.tsx | 15 +-- .../StorageExplorer/StorageExplorer.utils.ts | 10 -- .../StorageExplorer/StorageExplorer.utils.tsx | 87 +++++++++++++ apps/studio/lib/constants/index.ts | 3 +- .../storageExplorer/StorageExplorerStore.tsx | 120 +++++------------- apps/studio/state/storage-explorer.ts | 13 -- apps/studio/state/storage-explorer.tsx | 97 ++++++++++++++ 18 files changed, 282 insertions(+), 168 deletions(-) delete mode 100644 apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.ts create mode 100644 apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.tsx delete mode 100644 apps/studio/state/storage-explorer.ts create mode 100644 apps/studio/state/storage-explorer.tsx diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx index 819a3668a70..bf3a851352d 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectContext.tsx @@ -5,6 +5,7 @@ import { PROJECT_STATUS } from 'lib/constants' import { AiAssistantStateContextProvider } from 'state/ai-assistant-state' import { DatabaseSelectorStateContextProvider } from 'state/database-selector' import { RoleImpersonationStateContextProvider } from 'state/role-impersonation-state' +import { StorageExplorerStateContextProvider } from 'state/storage-explorer' import { TableEditorStateContextProvider } from 'state/table-editor' export interface ProjectContextType { @@ -45,11 +46,13 @@ export const ProjectContextProvider = ({ key={`ai-assistant-state-${projectRef}`} projectRef={projectRef} > - - - {children} - - + + + + {children} + + + diff --git a/apps/studio/components/layouts/StorageLayout/StorageMenu.tsx b/apps/studio/components/layouts/StorageLayout/StorageMenu.tsx index afd788fd3af..a91a4f3de24 100644 --- a/apps/studio/components/layouts/StorageLayout/StorageMenu.tsx +++ b/apps/studio/components/layouts/StorageLayout/StorageMenu.tsx @@ -51,8 +51,13 @@ const StorageMenu = () => { | 'usage' | 'logs' - const { data, error, isLoading, isError, isSuccess } = useBucketsQuery({ projectRef: ref }) - const buckets = data ?? [] + const { + data: buckets = [], + error, + isLoading, + isError, + isSuccess, + } = useBucketsQuery({ projectRef: ref }) const sortedBuckets = sort === 'alphabetical' ? buckets.sort((a, b) => diff --git a/apps/studio/components/to-be-cleaned/Storage/Storage.types.ts b/apps/studio/components/to-be-cleaned/Storage/Storage.types.ts index e52f7011b57..b29ce672669 100644 --- a/apps/studio/components/to-be-cleaned/Storage/Storage.types.ts +++ b/apps/studio/components/to-be-cleaned/Storage/Storage.types.ts @@ -15,10 +15,12 @@ export interface StorageItem { type: STORAGE_ROW_TYPES status: STORAGE_ROW_STATUS metadata: StorageItemMetadata | null - isCorrupted: boolean created_at: string | null updated_at: string | null last_accessed_at: string | null + // UI specific properties, not from API + isCorrupted: boolean + path?: string } export type StorageItemWithColumn = StorageItem & { columnIndex: number } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx index 3cfb338bee8..3fa51bfcdfd 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ColumnContextMenu.tsx @@ -6,6 +6,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' import { ChevronRight, ChevronsDown, ChevronsUp, Clipboard, Eye, FolderPlus } from 'lucide-react' +import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { STORAGE_ROW_TYPES, STORAGE_SORT_BY, @@ -24,12 +25,13 @@ const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { columns, selectedItems, setSelectedItems, - setView, setSortBy, setSortByOrder, addNewFolderPlaceholder, } = storageExplorerStore + const snap = useStorageExplorerStateSnapshot() + const onSelectCreateFolder = (columnIndex = -1) => { addNewFolderPlaceholder(columnIndex) } @@ -80,10 +82,10 @@ const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { } arrow={} > - setView(STORAGE_VIEWS.COLUMNS)}> + snap.setView(STORAGE_VIEWS.COLUMNS)}> As columns - setView(STORAGE_VIEWS.LIST)}> + snap.setView(STORAGE_VIEWS.LIST)}> As list diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx index 9d6612288a4..dfbd8a9bc1c 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorer.tsx @@ -2,6 +2,7 @@ import { noop } from 'lodash' import { observer } from 'mobx-react-lite' import { useEffect, useRef } from 'react' +import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { cn } from 'ui' import { CONTEXT_MENU_KEYS, STORAGE_VIEWS } from '../Storage.constants' import type { StorageColumn, StorageItem, StorageItemWithColumn } from '../Storage.types' @@ -11,7 +12,6 @@ import FolderContextMenu from './FolderContextMenu' import ItemContextMenu from './ItemContextMenu' export interface FileExplorerProps { - view: string columns: StorageColumn[] openedFolders: StorageItem[] selectedItems: StorageItemWithColumn[] @@ -23,7 +23,6 @@ export interface FileExplorerProps { } const FileExplorer = ({ - view = STORAGE_VIEWS.COLUMNS, columns = [], openedFolders = [], selectedItems = [], @@ -34,6 +33,7 @@ const FileExplorer = ({ onColumnLoadMore = noop, }: FileExplorerProps) => { const fileExplorerRef = useRef(null) + const snap = useStorageExplorerStateSnapshot() useEffect(() => { if (fileExplorerRef) { @@ -49,19 +49,18 @@ const FileExplorer = ({ ref={fileExplorerRef} className={cn( 'file-explorer flex flex-grow overflow-x-auto justify-between h-full w-full relative', - view === STORAGE_VIEWS.LIST && 'flex-col' + snap.view === STORAGE_VIEWS.LIST && 'flex-col' )} > - {view === STORAGE_VIEWS.COLUMNS ? ( + {snap.view === STORAGE_VIEWS.COLUMNS ? (
{columns.map((column, index) => ( ))}
- ) : view === STORAGE_VIEWS.LIST ? ( + ) : snap.view === STORAGE_VIEWS.LIST ? ( <> {columns.length > 0 && ( ) : ( -
Unknown view: {view}
+
Unknown view: {snap.view}
)}
) diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx index 95ccbb7455f..7d768653923 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerColumn.tsx @@ -55,7 +55,6 @@ const DragOverOverlay = ({ isOpen, onDragLeave, onDrop, folderIsEmpty }: any) => export interface FileExplorerColumnProps { index: number - view: STORAGE_VIEWS column: StorageColumn fullWidth?: boolean openedFolders?: StorageItem[] @@ -69,7 +68,6 @@ export interface FileExplorerColumnProps { const FileExplorerColumn = ({ index = 0, - view = STORAGE_VIEWS.COLUMNS, column, fullWidth = false, openedFolders = [], @@ -146,7 +144,7 @@ const FileExplorerColumn = ({ ref={fileExplorerColumnRef} className={cn( fullWidth ? 'w-full' : 'w-64 border-r border-overlay', - view === STORAGE_VIEWS.LIST && 'h-full', + snap.view === STORAGE_VIEWS.LIST && 'h-full', 'hide-scrollbar relative flex flex-shrink-0 flex-col overflow-auto' )} onContextMenu={displayMenu} @@ -159,7 +157,7 @@ const FileExplorerColumn = ({ }} > {/* Checkbox selection for select all */} - {view === STORAGE_VIEWS.COLUMNS && ( + {snap.view === STORAGE_VIEWS.COLUMNS && (
@@ -211,7 +209,7 @@ const FileExplorerColumn = ({ {/* List interface footer */} - {view === STORAGE_VIEWS.LIST && ( + {snap.view === STORAGE_VIEWS.LIST && (

{formatBytes(columnItemsSize)} for {columnItems.length} items diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx index e9b7bf980c9..2574c9f862b 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeader.tsx @@ -162,8 +162,6 @@ const FileExplorerHeader = ({ const storageExplorerStore = useStorageStore() const { - view, - setView, columns, sortBy, setSortBy, @@ -333,7 +331,7 @@ const FileExplorerHeader = ({ ]} /> - ) : view === STORAGE_VIEWS.COLUMNS ? ( + ) : snap.view === STORAGE_VIEWS.COLUMNS ? ( ) : ( @@ -380,10 +378,10 @@ const FileExplorerHeader = ({ {VIEW_OPTIONS.map((option) => ( - setView(option.key)}> + snap.setView(option.key)}>

{option.name}

- {view === option.key && } + {snap.view === option.key && }
))} diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx index 8b6daad9cf2..b69bafc6ac6 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerHeaderSelection.tsx @@ -1,17 +1,19 @@ import { Download, Move, Trash2, X } from 'lucide-react' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' import { Button } from 'ui' +import { downloadFile } from './StorageExplorer.utils' const FileExplorerHeaderSelection = () => { + const { ref: projectRef, bucketId } = useParams() const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') const storageExplorerStore = useStorageStore() const { selectedItems, - downloadFile, downloadSelectedFiles, clearSelectedItems, setSelectedItemsToDelete, @@ -35,7 +37,7 @@ const FileExplorerHeaderSelection = () => { type="primary" onClick={async () => { if (selectedItems.length === 1) { - await downloadFile(selectedItems[0]) + await downloadFile({ projectRef, bucketId, file: selectedItems[0] }) } else { await downloadSelectedFiles(selectedItems) } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx index 92bb5ff9f4f..d1df99ba571 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/FileExplorerRow.tsx @@ -17,6 +17,7 @@ import { import { useContextMenu } from 'react-contexify' import SVG from 'react-inlinesvg' +import { useParams } from 'common' import type { ItemRenderer } from 'components/ui/InfiniteList' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' @@ -46,7 +47,7 @@ import { } from '../Storage.constants' import { StorageItem, StorageItemWithColumn } from '../Storage.types' import FileExplorerRowEditing from './FileExplorerRowEditing' -import { copyPathToFolder } from './StorageExplorer.utils' +import { copyPathToFolder, downloadFile } from './StorageExplorer.utils' import { useCopyUrl } from './useCopyUrl' export const RowIcon = ({ @@ -111,6 +112,7 @@ const FileExplorerRow: ItemRenderer = ({ selectedItems = [], openedFolders = [], }) => { + const { ref: projectRef, bucketId } = useParams() const storageExplorerStore = useStorageStore() const { popColumnAtIndex, @@ -126,7 +128,6 @@ const FileExplorerRow: ItemRenderer = ({ setSelectedItemsToMove, setSelectedFileCustomExpiry, fetchFolderContents, - downloadFile, downloadFolder, selectRangeItems, selectedFilePreview, @@ -260,7 +261,13 @@ const FileExplorerRow: ItemRenderer = ({ { name: 'Download', icon: , - onClick: async () => await downloadFile(itemWithColumnIndex), + onClick: async () => { + await downloadFile({ + projectRef, + bucketId, + file: itemWithColumnIndex, + }) + }, }, { name: 'Separator', icon: undefined, onClick: undefined }, ] diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx index 084290a93e1..b52a350b802 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ItemContextMenu.tsx @@ -4,10 +4,12 @@ import { observer } from 'mobx-react-lite' import { Item, Menu, Separator, Submenu } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' +import { useParams } from 'common' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' import { URL_EXPIRY_DURATION } from '../Storage.constants' import { StorageItemWithColumn } from '../Storage.types' +import { downloadFile } from './StorageExplorer.utils' import { useCopyUrl } from './useCopyUrl' interface ItemContextMenuProps { @@ -15,9 +17,9 @@ interface ItemContextMenuProps { } const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { + const { ref: projectRef, bucketId } = useParams() const storageExplorerStore = useStorageStore() const { - downloadFile, selectedBucket, setSelectedItemsToDelete, setSelectedItemToRename, @@ -39,7 +41,7 @@ const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { case 'move': return setSelectedItemsToMove([item]) case 'download': - return await downloadFile(item) + return await downloadFile({ projectRef, bucketId, file: item }) default: break } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx index 0f89945c81c..1374b522a66 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/PreviewPane.tsx @@ -4,6 +4,7 @@ import { isEmpty } from 'lodash' import { AlertCircle, ChevronDown, Clipboard, Download, Loader, Trash2, X } from 'lucide-react' import SVG from 'react-inlinesvg' +import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' @@ -18,6 +19,7 @@ import { } from 'ui' import { URL_EXPIRY_DURATION } from '../Storage.constants' import { StorageItem } from '../Storage.types' +import { downloadFile } from './StorageExplorer.utils' import { useCopyUrl } from './useCopyUrl' import { useFetchFileUrlQuery } from './useFetchFileUrlQuery' @@ -115,9 +117,9 @@ const PreviewFile = ({ item }: { item: StorageItem }) => { } const PreviewPane = () => { + const { ref: projectRef, bucketId } = useParams() const storageExplorerStore = useStorageStore() const { - downloadFile, selectedBucket, selectedFilePreview: file, closeFilePreview, @@ -128,9 +130,7 @@ const PreviewPane = () => { const canUpdateFiles = useCheckPermissions(PermissionAction.STORAGE_WRITE, '*') - if (!file) { - return null - } + if (!file) return null const width = 450 const isOpen = !isEmpty(file) @@ -211,7 +211,7 @@ const PreviewPane = () => { type="default" icon={} disabled={file.isCorrupted} - onClick={async () => await downloadFile(file)} + onClick={async () => await downloadFile({ projectRef, bucketId, file })} > Download diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx index bc34848e6eb..a6d131379d2 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx @@ -7,6 +7,7 @@ import { useProjectStorageConfigQuery } from 'data/config/project-storage-config import type { Bucket } from 'data/storage/buckets-query' import { IS_PLATFORM } from 'lib/constants' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' +import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants' import ConfirmDeleteModal from './ConfirmDeleteModal' import CustomExpiryModal from './CustomExpiryModal' @@ -35,7 +36,6 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { popOpenedFoldersAtIndex, selectedItemsToMove, clearSelectedItemsToMove, - view, currentBucketName, openBucket, @@ -49,9 +49,9 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { moveFiles, } = storageExplorerStore - const storageExplorerRef = useRef(null) - const { ref } = useParams() + const storageExplorerRef = useRef(null) + const snap = useStorageExplorerStateSnapshot() // [Joshen] This is to ensure that StorageExplorerStore can get the storage file size limit // Will be better once we deprecate the mobx store entirely, which we will get there @@ -68,7 +68,7 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { const fetchContents = async () => { - if (view === STORAGE_VIEWS.LIST) { + if (snap.view === STORAGE_VIEWS.LIST) { const currentFolderIdx = openedFolders.length - 1 const currentFolder = openedFolders[currentFolderIdx] @@ -92,7 +92,7 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { await fetchFolderContents(currentFolder.id, currentFolder.name, currentFolderIdx) } } - } else if (view === STORAGE_VIEWS.COLUMNS) { + } else if (snap.view === STORAGE_VIEWS.COLUMNS) { const paths = openedFolders.map((folder) => folder.name) fetchFoldersByPath(paths, itemSearchString, true) } @@ -102,9 +102,9 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { }, [itemSearchString]) useEffect(() => { - // Load user preferences (view and sort) + // Load user preferences (view, sort, sortBy) loadExplorerPreferences() - }, []) + }, [ref]) useEffect(() => { openBucket(bucket) @@ -196,7 +196,6 @@ const StorageExplorer = ({ bucket }: StorageExplorerProps) => { )}
{ - const folders = openedFolders.slice(0, item.columnIndex).map((folder) => folder.name) - const path = folders.length > 0 ? `${folders.join('/')}/${item.name}` : item.name - copyToClipboard(path) - toast.success(`Copied path to folder "${item.name}"`) -} diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.tsx new file mode 100644 index 00000000000..3d8dbf518ad --- /dev/null +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils.tsx @@ -0,0 +1,87 @@ +import { downloadBucketObject } from 'data/storage/bucket-object-download-mutation' +import { copyToClipboard } from 'lib/helpers' +import { toast } from 'sonner' +import { SONNER_DEFAULT_DURATION } from 'ui' +import { StorageItem, StorageItemWithColumn } from '../Storage.types' + +export const copyPathToFolder = (openedFolders: StorageItem[], item: StorageItemWithColumn) => { + const folders = openedFolders.slice(0, item.columnIndex).map((folder) => folder.name) + const path = folders.length > 0 ? `${folders.join('/')}/${item.name}` : item.name + copyToClipboard(path) + toast.success(`Copied path to folder "${item.name}"`) +} + +export const formatTime = (seconds: number) => { + const days = Math.floor(seconds / (24 * 3600)) + seconds %= 24 * 3600 + const hours = Math.floor(seconds / 3600) + seconds %= 3600 + const minutes = Math.floor(seconds / 60) + seconds = Math.floor(seconds % 60) + + if (days > 0) return `${days}d ` + if (hours > 0) return `${hours}h ` + if (minutes > 0) return `${minutes}m ` + return `${seconds}s` +} + +export const downloadFile = async ({ + projectRef, + bucketId, + file, + showToast = true, + returnBlob = false, +}: { + projectRef?: string + bucketId?: string + file: StorageItemWithColumn + showToast?: boolean + returnBlob?: boolean +}) => { + if (!projectRef) return toast.error('Failed to download: Project ref is required') + if (!bucketId) return toast.error('Failed to download: Bucket ID is required') + if (!file.path) return toast.error('Failed to download: Unable to find path to file') + + const fileName: string = file.name + const fileMimeType = file?.metadata?.mimetype ?? undefined + + const toastId = showToast ? toast.loading(`Retrieving ${fileName}...`) : undefined + + try { + const data = await downloadBucketObject({ + projectRef, + bucketId, + path: file.path, + }) + + const blob = await data.blob() + const newBlob = new Blob([blob], { type: fileMimeType }) + if (returnBlob) return { name: fileName, blob: newBlob } + + const blobUrl = window.URL.createObjectURL(newBlob) + const link = document.createElement('a') + link.href = blobUrl + link.setAttribute('download', `${fileName}`) + document.body.appendChild(link) + link.click() + link.parentNode?.removeChild(link) + window.URL.revokeObjectURL(blob) + if (toastId) { + toast.success(`Downloading ${fileName}`, { + id: toastId, + closeButton: true, + duration: SONNER_DEFAULT_DURATION, + }) + } + return true + } catch (err) { + if (toastId) { + toast.error(`Failed to download ${fileName}`, { + id: toastId, + closeButton: true, + duration: SONNER_DEFAULT_DURATION, + }) + } + return false + } +} diff --git a/apps/studio/lib/constants/index.ts b/apps/studio/lib/constants/index.ts index 3ca7d99257d..942be406eb5 100644 --- a/apps/studio/lib/constants/index.ts +++ b/apps/studio/lib/constants/index.ts @@ -48,14 +48,13 @@ export const LOCAL_STORAGE_KEYS = { UI_PREVIEW_CLS: 'supabase-ui-cls', UI_PREVIEW_INLINE_EDITOR: 'supabase-ui-preview-inline-editor', UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown', - UI_TABLE_EDITOR_TABS: 'supabase-ui-table-editor-tabs', UI_SQL_EDITOR_TABS: 'supabase-ui-sql-editor-tabs', UI_NEW_LAYOUT_PREVIEW: 'supabase-ui-new-layout-preview', - NEW_LAYOUT_NOTICE_ACKNOWLEDGED: 'new-layout-notice-acknowledge', DASHBOARD_HISTORY: (ref: string) => `dashboard-history-${ref}`, + STORAGE_PREFERENCE: (ref: string) => `storage-explorer-${ref}`, SQL_EDITOR_INTELLISENSE: 'supabase_sql-editor-intellisense-enabled', SQL_EDITOR_SPLIT_SIZE: 'supabase_sql-editor-split-size', diff --git a/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx b/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx index 69ba342545c..63fb40418ea 100644 --- a/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx +++ b/apps/studio/localStores/storageExplorer/StorageExplorerStore.tsx @@ -29,6 +29,10 @@ import { StorageItemMetadata, StorageItemWithColumn, } from 'components/to-be-cleaned/Storage/Storage.types' +import { + downloadFile, + formatTime, +} from 'components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.utils' import { convertFromBytes } from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils' import { configKeys } from 'data/config/keys' import { ProjectStorageConfigResponse } from 'data/config/project-storage-config-query' @@ -38,7 +42,8 @@ import { downloadBucketObject } from 'data/storage/bucket-object-download-mutati import { StorageObject, listBucketObjects } from 'data/storage/bucket-objects-list-mutation' import { Bucket } from 'data/storage/buckets-query' import { moveStorageObject } from 'data/storage/object-move-mutation' -import { IS_PLATFORM } from 'lib/constants' +import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' +import { tryParseJson } from 'lib/helpers' import { lookupMime } from 'lib/mime' import Link from 'next/link' import { Button, SONNER_DEFAULT_DURATION, SonnerProgress } from 'ui' @@ -72,7 +77,6 @@ const STORAGE_PROGRESS_INFO_TEXT = "Do not close the browser until it's complete class StorageExplorerStore { projectRef: string = '' - view: STORAGE_VIEWS = STORAGE_VIEWS.COLUMNS sortBy: STORAGE_SORT_BY = STORAGE_SORT_BY.NAME sortByOrder: STORAGE_SORT_BY_ORDER = STORAGE_SORT_BY_ORDER.ASC // selectedBucket will get initialized with a bucket before using @@ -155,7 +159,7 @@ class StorageExplorerStore { } private getLocalStorageKey = () => { - return `supabase-storage-${this.projectRef}` + return LOCAL_STORAGE_KEYS.STORAGE_PREFERENCE(this.projectRef) } private getLatestColumnIndex = () => { @@ -190,12 +194,6 @@ class StorageExplorerStore { this.clearSelectedItems() } - setView = (view: STORAGE_VIEWS) => { - this.view = view - this.closeFilePreview() - this.updateExplorerPreferences() - } - setSortBy = async (sortBy: STORAGE_SORT_BY) => { this.sortBy = sortBy this.closeFilePreview() @@ -415,7 +413,7 @@ class StorageExplorerStore { 1 ? 's' : ''}...`} - progressPrefix={`${remainingTime && !isNaN(remainingTime) && isFinite(remainingTime) && remainingTime !== 0 ? `${this.formatTime(remainingTime)} remaining – ` : ''}`} + progressPrefix={`${remainingTime && !isNaN(remainingTime) && isFinite(remainingTime) && remainingTime !== 0 ? `${formatTime(remainingTime)} remaining – ` : ''}`} action={ toastId && ( - +
{ e.preventDefault() From 4976a8de8202a78eef2093df8fe125099ebf6ff4 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 29 Apr 2025 11:12:10 +0200 Subject: [PATCH 42/86] fix Logs Date Pickers tests (#35351) * fix tests * fix naming * fix names 2 * undo logspreviewer --- .../pages/projects/Logs.Datepickers.test.tsx | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/studio/tests/pages/projects/Logs.Datepickers.test.tsx b/apps/studio/tests/pages/projects/Logs.Datepickers.test.tsx index c4ea241af56..a5bd1aa636c 100644 --- a/apps/studio/tests/pages/projects/Logs.Datepickers.test.tsx +++ b/apps/studio/tests/pages/projects/Logs.Datepickers.test.tsx @@ -94,17 +94,16 @@ test('datepicker onSubmit will return ISO string of selected dates', async () => // open the datepicker userEvent.click(screen.getByText(/13:00/i)) - // Get today's date and tomorrow's date - const today = dayjs() - const tomorrow = today.add(1, 'day') + const day15 = dayjs().date(15) + const day16 = day15.add(1, 'day') - // Find and click on today's date - const todayElement = await screen.findByText(today.format('D')) - userEvent.click(todayElement) + // Find and click on first date + const day15Element = await screen.findByText(day15.format('D')) + userEvent.click(day15Element) - // Find and click on tomorrow's date - const tomorrowElement = await screen.findByText(tomorrow.format('D')) - userEvent.click(tomorrowElement) + // Find and click on second date + const day16Element = await screen.findByText(day16.format('D')) + userEvent.click(day16Element) await userEvent.click(await screen.findByText('Apply')) expect(mockFn).toBeCalled() @@ -112,8 +111,8 @@ test('datepicker onSubmit will return ISO string of selected dates', async () => const call = mockFn.mock.calls[0][0] expect(call).toMatchObject({ - from: dayjs().date(today.date()).hour(13).minute(0).second(0).millisecond(0).toISOString(), - to: dayjs().date(tomorrow.date()).hour(23).minute(59).second(59).millisecond(0).toISOString(), + from: dayjs().date(day15.date()).hour(13).minute(0).second(0).millisecond(0).toISOString(), + to: dayjs().date(day16.date()).hour(23).minute(59).second(59).millisecond(0).toISOString(), }) }) From 34147c3bd221e12dc381daa1f9404593f5047298 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 29 Apr 2025 18:06:24 +0800 Subject: [PATCH 43/86] Fix theme mode radio item large (#35350) * Fix theme mode radio item large * Attempt to fix Logs.DatePickers.test by fixing a date for the test * Revert --- .../Account/Preferences/ThemeSettings.tsx | 4 ++-- .../interfaces/Settings/Logs/Logs.DatePickers.tsx | 15 +++++++-------- .../interfaces/Settings/Logs/LogsQueryPanel.tsx | 8 ++++---- .../pages/projects/Logs.Datepickers.test.tsx | 2 +- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/apps/studio/components/interfaces/Account/Preferences/ThemeSettings.tsx b/apps/studio/components/interfaces/Account/Preferences/ThemeSettings.tsx index 73ae2129770..b05c8c5cf28 100644 --- a/apps/studio/components/interfaces/Account/Preferences/ThemeSettings.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/ThemeSettings.tsx @@ -2,6 +2,7 @@ import { useTheme } from 'next-themes' import { useEffect, useState } from 'react' import SVG from 'react-inlinesvg' +import { DEFAULT_SIDEBAR_BEHAVIOR } from 'components/interfaces/Sidebar' import Panel from 'components/ui/Panel' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { BASE_PATH, LOCAL_STORAGE_KEYS } from 'lib/constants' @@ -19,7 +20,6 @@ import { Theme, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { DEFAULT_SIDEBAR_BEHAVIOR } from 'components/interfaces/Sidebar' export const ThemeSettings = () => { const [mounted, setMounted] = useState(false) @@ -51,7 +51,7 @@ export const ThemeSettings = () => { > {singleThemes.map((theme: Theme) => ( Date: Tue, 29 Apr 2025 12:19:15 -0230 Subject: [PATCH 44/86] Update og image for ui lib (#35357) --- .../public/img/supabase-og-image.png | Bin 72642 -> 195614 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/ui-library/public/img/supabase-og-image.png b/apps/ui-library/public/img/supabase-og-image.png index c2f4d0ea806de10886bca49aa35a59982e3cbd96..cba27d698598a2fd1a93e52bf0fadb8d8d57f147 100644 GIT binary patch literal 195614 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 ur;g)D)jPzHMC>QZiNW0;Pc6T2NRDCd=wh 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 z�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 Y$$5^EOG8WJ z>=UT@jOoYs@3BwB(5OOi{(Lc!%liA>-FtYZ&)?ww_i3!Q`2COn`?QM~L;K≈6IJ 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 Date: Tue, 29 Apr 2025 12:52:08 -0400 Subject: [PATCH 45/86] db (docs): new search functions (#35229) Add some new search functions. Should behave roughly the same as the search functions that are currently in use, but formatted to work better for the new search endpoint(s). --- .../20250423133137_improve_vector_search.sql | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 supabase/migrations/20250423133137_improve_vector_search.sql diff --git a/supabase/migrations/20250423133137_improve_vector_search.sql b/supabase/migrations/20250423133137_improve_vector_search.sql new file mode 100644 index 00000000000..8563164e5bd --- /dev/null +++ b/supabase/migrations/20250423133137_improve_vector_search.sql @@ -0,0 +1,103 @@ +create index if not exists +idx_page_section_embedding +on public.page_section +using hnsw (embedding vector_ip_ops); + +create or replace function match_embedding( + embedding vector(1536), + match_threshold float default 0.78, + max_results int default 30 +) +returns setof page_section +language plpgsql +set search_path = '' +as $$ +#variable_conflict use_variable +begin + return query + select * + from public.page_section + where (page_section.embedding operator(public.<#>) embedding) <= -match_threshold + order by page_section.embedding operator(public.<#>) embedding + limit max_results; +end; +$$; + +create or replace function get_full_content_url( + type text, + path text, + slug text +) +returns text +language sql +set search_path = '' +as $$ + select case + when type = 'github-discussions' + then path + when type = 'partner-integration' + then concat('https://supabase.com', path) + else + concat( + 'https://supabase.com/docs', + path, + case + when slug is null + then '' + else + concat('#', slug) + end + ) + end; +$$; + +create or replace function search_content( + embedding vector(1536), + include_full_content boolean default false, + match_threshold float default 0.78, + max_result int default 30 +) +returns table ( + id bigint, + page_title text, + type text, + href text, + content text, + subsections json[] +) +language sql +set search_path = '' +as $$ + with matched_section as ( + select + *, + row_number() over () as ranking + from public.match_embedding( + embedding, + match_threshold, + max_result + ) + ) + select + page.id, + meta ->> 'title' as page_title, + type, + public.get_full_content_url(type, path, null) as href, + case + when include_full_content + then page.content + else + null + end as content, + array_agg( + json_build_object( + 'title', heading, + 'href', public.get_full_content_url(type, path, slug), + 'content', matched_section.content + ) + ) + from matched_section + join public.page on matched_section.page_id = page.id + group by page.id + order by min(ranking); +$$; From a0f5c798dfee8a187ca29628a39d8b5d3da92930 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:02:36 -0400 Subject: [PATCH 46/86] feat(content api): add dummy api/graphql route (#35141) The first PR in a series to build out the Content API. Adds a dummy GraphQL route with some tests: for now all this does is validate the request body and return a 404. --- apps/docs/app/api/graphql/route.test.ts | 56 +++++++++++++++++++++++++ apps/docs/app/api/graphql/route.ts | 49 ++++++++++++++++++++++ apps/docs/app/api/utils.ts | 29 +++++++++++++ apps/docs/package.json | 1 + pnpm-lock.yaml | 46 ++++++++++++-------- 5 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 apps/docs/app/api/graphql/route.test.ts create mode 100644 apps/docs/app/api/graphql/route.ts create mode 100644 apps/docs/app/api/utils.ts diff --git a/apps/docs/app/api/graphql/route.test.ts b/apps/docs/app/api/graphql/route.test.ts new file mode 100644 index 00000000000..77d12a09ac4 --- /dev/null +++ b/apps/docs/app/api/graphql/route.test.ts @@ -0,0 +1,56 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { POST } from './route' + +describe('GraphQL API endpoint', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.spyOn(console, 'error').mockImplementation(() => {}) + }) + + it('should return 400 if request body is not valid JSON', async () => { + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: 'not json', + }) + + const response = await POST(request) + expect(response.status).toBe(400) + expect(await response.text()).toBe('Invalid request: Request body must be valid JSON') + }) + + it('should return 400 if request body is missing required fields', async () => { + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: JSON.stringify({ variables: {} }), + }) + + const response = await POST(request) + expect(response.status).toBe(400) + expect(await response.text()).toContain('Request body must be a valid GraphQL request object') + }) + + it('should return 400 if query is not a string', async () => { + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: JSON.stringify({ query: 123 }), + }) + + const response = await POST(request) + expect(response.status).toBe(400) + }) + + it('should return 500 for internal server errors', async () => { + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: JSON.stringify({ query: 'query { hello }' }), + }) + + vi.spyOn(request, 'json').mockImplementation(() => { + throw new Error('Unexpected error') + }) + + const response = await POST(request) + expect(response.status).toBe(500) + expect(await response.text()).toBe('Internal Server Error') + }) +}) diff --git a/apps/docs/app/api/graphql/route.ts b/apps/docs/app/api/graphql/route.ts new file mode 100644 index 00000000000..7c5e8c9fdde --- /dev/null +++ b/apps/docs/app/api/graphql/route.ts @@ -0,0 +1,49 @@ +// import { graphql } from 'graphql' +import { NextResponse } from 'next/server' +import { z } from 'zod' + +import { ApiError, InvalidRequestError } from '~/app/api/utils' + +export const runtime = 'edge' + +const graphQLRequestSchema = z.object({ + query: z.string(), + variables: z.record(z.any()).optional(), + operationName: z.string().optional(), +}) + +async function handleGraphQLRequest(request: Request): Promise { + const body = await request.json().catch(() => { + throw new InvalidRequestError('Request body must be valid JSON') + }) + const parsedBody = graphQLRequestSchema.safeParse(body) + if (!parsedBody.success) { + const errorMessage = parsedBody.error?.message + throw new InvalidRequestError( + `Request body must be a valid GraphQL request object${errorMessage ? `: ${errorMessage}` : ''}` + ) + } + + // const { query, variables, operationName } = parsedBody.data + // When endpoint is complete, this should return a response with the GraphQL + // query result + + // For now, just return a 404 response + return new NextResponse('Not Found', { status: 404 }) +} + +export async function POST(request: Request): Promise { + try { + return await handleGraphQLRequest(request) + } catch (error: unknown) { + console.error(error) + + if (error instanceof ApiError) { + return new NextResponse(error.isPrivate() ? 'Internal Server Error' : error.message, { + status: error.statusCode(), + }) + } else { + return new NextResponse('Internal Server Error', { status: 500 }) + } + } +} diff --git a/apps/docs/app/api/utils.ts b/apps/docs/app/api/utils.ts new file mode 100644 index 00000000000..5b3f93ea15b --- /dev/null +++ b/apps/docs/app/api/utils.ts @@ -0,0 +1,29 @@ +import { z } from 'zod' + +export class ApiError extends Error { + constructor(message: string) { + super(message) + } + + isPrivate() { + return true + } + + statusCode() { + return 500 + } +} + +export class InvalidRequestError extends ApiError { + constructor(message: string) { + super(`Invalid request: ${message}`) + } + + isPrivate() { + return false + } + + statusCode() { + return 400 + } +} diff --git a/apps/docs/package.json b/apps/docs/package.json index 4208f151170..dd046d93891 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -55,6 +55,7 @@ "eslint-config-supabase": "workspace:*", "framer-motion": "^11.0.3", "github-slugger": "^2.0.0", + "graphql": "^16.10.0", "gray-matter": "^4.0.3", "hast-util-has-property": "^2.0.1", "hast-util-to-html": "^9.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae1312c5e06..b21aa326339 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -282,6 +282,9 @@ importers: github-slugger: specifier: ^2.0.0 version: 2.0.0 + graphql: + specifier: ^16.10.0 + version: 16.10.0 gray-matter: specifier: ^4.0.3 version: 4.0.3 @@ -564,10 +567,10 @@ importers: version: 0.2.15 '@graphiql/react': specifier: ^0.19.4 - version: 0.19.4(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.8.1))(graphql@16.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 0.19.4(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@graphiql/toolkit': specifier: ^0.9.1 - version: 0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.8.1))(graphql@16.8.1) + version: 0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0) '@gregnr/postgres-meta': specifier: ^0.82.0-dev.2 version: 0.82.0-dev.2(encoding@0.1.13)(supports-color@8.1.1) @@ -979,7 +982,7 @@ importers: version: 2.0.7(eslint@8.57.0(supports-color@8.1.1)) graphql-ws: specifier: 5.14.1 - version: 5.14.1(graphql@16.8.1) + version: 5.14.1(graphql@16.10.0) import-in-the-middle: specifier: ^1.13.1 version: 1.13.1 @@ -8675,6 +8678,7 @@ packages: resolution: {integrity: sha512-t0q23FIpvHDTtnORW+bDJziGsal5uh9RJTJ1fyH8drd4lICOoXhJ5pLMUZ5C0VQei6dNmwTzzoTRgMkO9JgHEQ==} peerDependencies: eslint: '>= 5' + bundledDependencies: [] eslint-plugin-import@2.29.1: resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} @@ -9311,6 +9315,10 @@ packages: peerDependencies: graphql: '>=0.11 <=16' + graphql@16.10.0: + resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + graphql@16.8.1: resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -16075,9 +16083,9 @@ snapshots: '@gar/promisify@1.1.3': {} - '@graphiql/react@0.19.4(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.8.1))(graphql@16.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@graphiql/react@0.19.4(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@graphiql/toolkit': 0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.8.1))(graphql@16.8.1) + '@graphiql/toolkit': 0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0) '@headlessui/react': 1.7.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -16086,11 +16094,11 @@ snapshots: '@types/codemirror': 5.60.10 clsx: 1.2.1 codemirror: 5.65.15 - codemirror-graphql: 2.0.10(@codemirror/language@6.11.0)(codemirror@5.65.15)(graphql@16.8.1) + codemirror-graphql: 2.0.10(@codemirror/language@6.11.0)(codemirror@5.65.15)(graphql@16.10.0) copy-to-clipboard: 3.3.3 framer-motion: 6.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - graphql: 16.8.1 - graphql-language-service: 5.2.0(graphql@16.8.1) + graphql: 16.10.0 + graphql-language-service: 5.2.0(graphql@16.10.0) markdown-it: 12.3.2 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -16102,13 +16110,13 @@ snapshots: - '@types/react-dom' - graphql-ws - '@graphiql/toolkit@0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.8.1))(graphql@16.8.1)': + '@graphiql/toolkit@0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)': dependencies: '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 - graphql: 16.8.1 + graphql: 16.10.0 meros: 1.3.0(@types/node@20.12.11) optionalDependencies: - graphql-ws: 5.14.1(graphql@16.8.1) + graphql-ws: 5.14.1(graphql@16.10.0) transitivePeerDependencies: - '@types/node' @@ -21894,13 +21902,13 @@ snapshots: estree-walker: 3.0.3 periscopic: 3.1.0 - codemirror-graphql@2.0.10(@codemirror/language@6.11.0)(codemirror@5.65.15)(graphql@16.8.1): + codemirror-graphql@2.0.10(@codemirror/language@6.11.0)(codemirror@5.65.15)(graphql@16.10.0): dependencies: '@codemirror/language': 6.11.0 '@types/codemirror': 0.0.90 codemirror: 5.65.15 - graphql: 16.8.1 - graphql-language-service: 5.2.0(graphql@16.8.1) + graphql: 16.10.0 + graphql-language-service: 5.2.0(graphql@16.10.0) codemirror@5.65.15: {} @@ -23719,15 +23727,17 @@ snapshots: graphemer@1.4.0: {} - graphql-language-service@5.2.0(graphql@16.8.1): + graphql-language-service@5.2.0(graphql@16.10.0): dependencies: - graphql: 16.8.1 + graphql: 16.10.0 nullthrows: 1.1.1 vscode-languageserver-types: 3.17.5 - graphql-ws@5.14.1(graphql@16.8.1): + graphql-ws@5.14.1(graphql@16.10.0): dependencies: - graphql: 16.8.1 + graphql: 16.10.0 + + graphql@16.10.0: {} graphql@16.8.1: {} From e41d90926ba0716c4beae489cfe91fa96919aac8 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:27:21 -0400 Subject: [PATCH 47/86] feat (content api): graphql validation (#35191) Continue building out the docs/api/graphql endpoint by adding more validation: -A query depth limiter - A query complexity limiter Also added dummy (empty) root resolver and schema, plus a dev-only GraphiQL page for better DX. Endpoint should still return a 404 for now as there is no resolver. --- apps/docs/app/api/graphql/route.ts | 56 ++++- apps/docs/app/api/graphql/validators.test.ts | 247 +++++++++++++++++++ apps/docs/app/api/graphql/validators.ts | 157 ++++++++++++ apps/docs/app/graphiql/page.tsx | 33 +++ apps/docs/package.json | 4 + apps/docs/resources/rootResolver.ts | 1 + apps/docs/resources/rootSchema.ts | 8 + pnpm-lock.yaml | 189 +++++++++++++- 8 files changed, 688 insertions(+), 7 deletions(-) create mode 100644 apps/docs/app/api/graphql/validators.test.ts create mode 100644 apps/docs/app/api/graphql/validators.ts create mode 100644 apps/docs/app/graphiql/page.tsx create mode 100644 apps/docs/resources/rootResolver.ts create mode 100644 apps/docs/resources/rootSchema.ts diff --git a/apps/docs/app/api/graphql/route.ts b/apps/docs/app/api/graphql/route.ts index 7c5e8c9fdde..7efda7c254a 100644 --- a/apps/docs/app/api/graphql/route.ts +++ b/apps/docs/app/api/graphql/route.ts @@ -1,8 +1,11 @@ -// import { graphql } from 'graphql' +import { type DocumentNode, graphql, GraphQLError, parse, specifiedRules, validate } from 'graphql' +import { createComplexityLimitRule } from 'graphql-validation-complexity' import { NextResponse } from 'next/server' import { z } from 'zod' - import { ApiError, InvalidRequestError } from '~/app/api/utils' +import { rootGraphQLResolver } from '~/resources/rootResolver' +import { rootGraphQLSchema } from '~/resources/rootSchema' +import { createQueryDepthLimiter } from './validators' export const runtime = 'edge' @@ -24,14 +27,57 @@ async function handleGraphQLRequest(request: Request): Promise { ) } - // const { query, variables, operationName } = parsedBody.data - // When endpoint is complete, this should return a response with the GraphQL - // query result + const { query, variables, operationName } = parsedBody.data + const validationErrors = validateGraphQLRequest(query) + if (validationErrors.length > 0) { + return NextResponse.json({ + errors: validationErrors.map((error) => ({ + message: error.message, + locations: error.locations, + path: error.path, + })), + }) + } + + const result = await graphql({ + schema: rootGraphQLSchema, + rootValue: rootGraphQLResolver, + contextValue: { request }, + source: query, + variableValues: variables, + operationName, + }) + // return NextResponse.json(result) // For now, just return a 404 response return new NextResponse('Not Found', { status: 404 }) } +function validateGraphQLRequest(query: string): ReadonlyArray { + const MAX_DEPTH = 9 + + let documentAST: DocumentNode + try { + documentAST = parse(query) + } catch (error: unknown) { + if (error instanceof GraphQLError) { + return [error] + } else { + throw error + } + } + const validationRules = [ + ...specifiedRules, + createQueryDepthLimiter(MAX_DEPTH), + createComplexityLimitRule(1500, { + scalarCost: 1, + objectCost: 2, + listFactor: 10, + }), + ] + return validate(rootGraphQLSchema, documentAST, validationRules) +} + export async function POST(request: Request): Promise { try { return await handleGraphQLRequest(request) diff --git a/apps/docs/app/api/graphql/validators.test.ts b/apps/docs/app/api/graphql/validators.test.ts new file mode 100644 index 00000000000..0659623848d --- /dev/null +++ b/apps/docs/app/api/graphql/validators.test.ts @@ -0,0 +1,247 @@ +import { buildSchema, parse, validate } from 'graphql' +import { describe, expect, test } from 'vitest' +import { createQueryDepthLimiter } from './validators' + +describe('createQueryDepthLimiter', () => { + // Create a test schema + const schema = buildSchema(` + type Post { + id: ID! + title: String + content: String + author: User + comments: [Comment] + } + + type User { + id: ID! + name: String + email: String + posts: [Post] + friends: [User] + } + + type Comment { + id: ID! + text: String + author: User + post: Post + replies: [Comment] + } + + type Query { + post(id: ID!): Post + user(id: ID!): User + posts: [Post] + users: [User] + } + `) + + test('should not report error for queries under max depth', () => { + const query = ` + query { + posts { + title + author { + name + } + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors).toHaveLength(0) + }) + + test('should report error for queries exceeding max depth', () => { + const query = ` + query { + posts { + title + author { + name + posts { + title + } + } + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].message).toContain('Query exceeds maximum depth of 3') + }) + + test('should ignore __typename fields when calculating depth', () => { + const query = ` + query { + posts { + __typename + title + author { + __typename + name + posts { + __typename + } + } + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors).toHaveLength(0) + }) + + test('should handle fragments correctly', () => { + const query = ` + fragment UserDetails on User { + id + name + posts { + title + } + } + + query { + users { + ...UserDetails + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors).toHaveLength(0) + }) + + test('should report error for fragments exceeding max depth', () => { + const query = ` + fragment UserDetails on User { + id + name + posts { + title + author { + name + } + } + } + + query { + users { + ...UserDetails + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].message).toContain( + 'Fragment spread "UserDetails" causes query to exceed maximum depth' + ) + }) + + test('should report error for fragment spreads causing excessive depth', () => { + const query = ` + fragment PostDetails on Post { + title + content + } + + fragment UserDetails on User { + name + posts { + ...PostDetails + } + } + + query { + users { + friends { + ...UserDetails + } + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors.length).toBeGreaterThan(0) + }) + + test('should handle nested fragments correctly', () => { + const query = ` + fragment CommentDetails on Comment { + text + author { + name + } + } + + fragment PostDetails on Post { + title + comments { + ...CommentDetails + } + } + + query { + posts { + ...PostDetails + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors.length).toBeGreaterThan(0) + }) + + test('should handle inline fragments', () => { + const query = ` + query { + posts { + ... on Post { + title + author { + name + } + } + } + } + ` + const queryAST = parse(query) + const maxDepth = 3 + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors).toHaveLength(0) + }) + + test('should detect circular fragment references', () => { + const query = ` + fragment UserPosts on User { + posts { + author { + ...UserPosts + } + } + } + + query { + users { + ...UserPosts + } + } + ` + const queryAST = parse(query) + const maxDepth = 5 + // This should not go into infinite recursion + const errors = validate(schema, queryAST, [createQueryDepthLimiter(maxDepth)]) + expect(errors.length).toBeGreaterThan(0) + }) +}) diff --git a/apps/docs/app/api/graphql/validators.ts b/apps/docs/app/api/graphql/validators.ts new file mode 100644 index 00000000000..f4ca8e68df9 --- /dev/null +++ b/apps/docs/app/api/graphql/validators.ts @@ -0,0 +1,157 @@ +import { + type FragmentDefinitionNode, + GraphQLError, + type SelectionNode, + type ValidationContext, + type ValidationRule, +} from 'graphql' + +/** + * Creates a validator that limits the depth of a GraphQL query. + * + * @param maxDepth The maximum allowed depth + * @returns A validation rule that enforces depth limits + */ +export function createQueryDepthLimiter(maxDepth: number): ValidationRule { + const fragmentDepthMap = new Map() + + return function limitQueryDepth(context) { + return { + Document: { + leave() { + fragmentDepthMap.clear() + }, + }, + Field: { + enter(node, _key, _parent, path, ancestors) { + // Skip __typename and introspection fields + if (node.name.value === '__typename' || node.name.value.startsWith('__')) { + return + } + + // Calculate the depth by counting the ancestors that are field nodes + const fieldAncestors = ancestors.filter( + (ancestor) => ancestor && 'kind' in ancestor && ancestor.kind === 'Field' + ) + const depth = fieldAncestors.length + 1 + + // Check if this field exceeds the maximum maxDepth + if (depth > maxDepth) { + const pathStr = path.join('.') + context.reportError( + new GraphQLError( + `Query exceeds maximum depth of ${maxDepth}. Got depth ${depth} for ${pathStr}.`, + { nodes: node } + ) + ) + } + }, + }, + FragmentDefinition: { + enter(node) { + calculateFragmentDepth(node, context, 0, fragmentDepthMap, maxDepth) + const depth = fragmentDepthMap.get(node.name.value)! + + if (depth > maxDepth) { + context.reportError( + new GraphQLError( + `Fragment "${node.name.value}" exceeds maximum depth of ${maxDepth}.`, + { nodes: node } + ) + ) + } + }, + }, + FragmentSpread: { + enter(node, _key, _parent, _path, ancestors) { + const fragmentName = node.name.value + const fragmentDepth = fragmentDepthMap.get(fragmentName) || 0 + + const fieldAncestors = ancestors.filter( + (ancestor) => ancestor && 'kind' in ancestor && ancestor.kind === 'Field' + ) + const currentDepth = fieldAncestors.length + + const totalDepth = currentDepth + fragmentDepth + + if (totalDepth > maxDepth) { + context.reportError( + new GraphQLError( + `Fragment spread "${fragmentName}" causes query to exceed maximum depth of ${maxDepth}.`, + { nodes: node } + ) + ) + } + }, + }, + } + } +} + +/** + * Helper function to calculate the depth of a fragment + */ +function calculateFragmentDepth( + node: FragmentDefinitionNode, + context: ValidationContext, + currentDepth: number, + visitedFragments: Map, + maxDepth: number +) { + const fragmentName = node.name.value + if (visitedFragments.has(fragmentName)) { + return + } + + let maxFragmentDepth = 0 + // Process all selections in the fragment + function processSelections(selections: ReadonlyArray, depth: number) { + for (const selection of selections) { + if (selection.kind === 'Field') { + // Skip __typename and introspection fields + if (selection.name.value === '__typename' || selection.name.value.startsWith('__')) { + continue + } + + const newDepth = depth + 1 + if (newDepth > maxFragmentDepth) { + maxFragmentDepth = newDepth + } + if (maxFragmentDepth > maxDepth) { + // Do not recurse anymore because we might go on indefinitely + continue + } + + // If there are nested selections, process them + if (selection.selectionSet) { + processSelections(selection.selectionSet.selections, newDepth) + } + } else if (selection.kind === 'FragmentSpread') { + // Process fragment spreads + const spreadName = selection.name.value + const fragment = context.getFragment(spreadName) + if (!fragment) continue + + const fragmentName = fragment.name.value + if (!visitedFragments.has(fragmentName)) { + calculateFragmentDepth(fragment, context, depth, visitedFragments, maxDepth) + } + const fragmentDepth = visitedFragments.get(fragmentName)! + const totalDepth = fragmentDepth + depth + + if (totalDepth > maxFragmentDepth) { + maxFragmentDepth = totalDepth + } + } else if (selection.kind === 'InlineFragment' && selection.selectionSet) { + // Process inline fragments + processSelections(selection.selectionSet.selections, depth) + } + } + } + + if (node.selectionSet) { + processSelections(node.selectionSet.selections, currentDepth) + } + + visitedFragments.set(fragmentName, maxFragmentDepth) +} diff --git a/apps/docs/app/graphiql/page.tsx b/apps/docs/app/graphiql/page.tsx new file mode 100644 index 00000000000..6443554bbbd --- /dev/null +++ b/apps/docs/app/graphiql/page.tsx @@ -0,0 +1,33 @@ +'use client' + +import dynamic from 'next/dynamic' +import { notFound } from 'next/navigation' +import { type ComponentProps } from 'react' +import { IS_DEV } from '~/lib/constants' + +const LazyGraphiQL = dynamic( + async () => { + const { GraphiQL: GraphiQLPrimitive } = await import('graphiql') + const { createGraphiQLFetcher } = await import('@graphiql/toolkit') + await import('graphiql/graphiql.css') + + return function GraphiQL(props: Omit, 'fetcher'>) { + const fetcher = createGraphiQLFetcher({ + url: 'http://localhost:3001/docs/api/graphql', + }) + return + } + }, + { + loading: () =>
Loading GraphiQL...
, + ssr: false, + } +) + +export default function GraphiQLPage() { + if (!IS_DEV) { + notFound() + } + + return +} diff --git a/apps/docs/package.json b/apps/docs/package.json index dd046d93891..b145e54337f 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -56,6 +56,7 @@ "framer-motion": "^11.0.3", "github-slugger": "^2.0.0", "graphql": "^16.10.0", + "graphql-validation-complexity": "^0.4.2", "gray-matter": "^4.0.3", "hast-util-has-property": "^2.0.1", "hast-util-to-html": "^9.0.0", @@ -112,9 +113,11 @@ }, "devDependencies": { "@aws-sdk/client-secrets-manager": "^3.410.0", + "@graphiql/toolkit": "^0.9.1", "@supabase/supa-mdx-lint": "0.2.6-alpha", "@types/common-tags": "^1.8.4", "@types/estree": "1.0.5", + "@types/graphql-validation-complexity": "^0.4.4", "@types/hast": "^2.3.4", "@types/jsrsasign": "^10.5.12", "@types/mdast": "^3.0.0", @@ -131,6 +134,7 @@ "dotenv": "^16.0.3", "ejs": "^3.1.10", "globby": "^13.2.2", + "graphiql": "^3.8.3", "openapi-types": "^12.1.3", "shiki": "^3.2.1", "simple-git": "^3.24.0", diff --git a/apps/docs/resources/rootResolver.ts b/apps/docs/resources/rootResolver.ts new file mode 100644 index 00000000000..c65cd01a5b6 --- /dev/null +++ b/apps/docs/resources/rootResolver.ts @@ -0,0 +1 @@ +export const rootGraphQLResolver = {} diff --git a/apps/docs/resources/rootSchema.ts b/apps/docs/resources/rootSchema.ts new file mode 100644 index 00000000000..7dd25048a82 --- /dev/null +++ b/apps/docs/resources/rootSchema.ts @@ -0,0 +1,8 @@ +import { GraphQLObjectType, GraphQLSchema } from 'graphql' + +export const rootGraphQLSchema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'RootQueryType', + fields: {}, + }), +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b21aa326339..b5b8299a5f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -285,6 +285,9 @@ importers: graphql: specifier: ^16.10.0 version: 16.10.0 + graphql-validation-complexity: + specifier: ^0.4.2 + version: 0.4.2(graphql@16.10.0) gray-matter: specifier: ^4.0.3 version: 4.0.3 @@ -448,6 +451,9 @@ importers: '@aws-sdk/client-secrets-manager': specifier: ^3.410.0 version: 3.468.0 + '@graphiql/toolkit': + specifier: ^0.9.1 + version: 0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0) '@supabase/supa-mdx-lint': specifier: 0.2.6-alpha version: 0.2.6-alpha @@ -457,6 +463,9 @@ importers: '@types/estree': specifier: 1.0.5 version: 1.0.5 + '@types/graphql-validation-complexity': + specifier: ^0.4.4 + version: 0.4.4 '@types/hast': specifier: ^2.3.4 version: 2.3.6 @@ -502,6 +511,9 @@ importers: globby: specifier: ^13.2.2 version: 13.2.2 + graphiql: + specifier: ^3.8.3 + version: 3.8.3(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) openapi-types: specifier: ^12.1.3 version: 12.1.3 @@ -3170,6 +3182,22 @@ packages: react: ^16.8.0 || ^17 || ^18 react-dom: ^16.8.0 || ^17 || ^18 + '@graphiql/react@0.28.2': + resolution: {integrity: sha512-6PE2Ff9dXpyQMHy3oKzCjT738kY2+wdQ4Xce8+1K+G2yMGZ716Fo0i4vW3S6ErHVI4S/C76gFTQlv/pzxU5ylg==} + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + + '@graphiql/toolkit@0.11.1': + resolution: {integrity: sha512-G02te70/oYYna5UhbH6TXwNxeQyWa+ChlPonUrKwC5Ot9ItraGJ9yUU4sS+YRaA+EvkzNoHG79XcW2k1QaAMiw==} + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + graphql-ws: '>= 4.5.0' + peerDependenciesMeta: + graphql-ws: + optional: true + '@graphiql/toolkit@0.9.1': resolution: {integrity: sha512-LVt9pdk0830so50ZnU2Znb2rclcoWznG8r8asqAENzV0U1FM1kuY0sdPpc/rBc9MmmNgnB6A+WZzDhq6dbhTHA==} peerDependencies: @@ -6519,6 +6547,9 @@ packages: '@types/geojson@7946.0.12': resolution: {integrity: sha512-uK2z1ZHJyC0nQRbuovXFt4mzXDwf27vQeUWNhfKGwRcWW429GOhP8HxUHlM6TLH4bzmlv/HlEjpvJh3JfmGsAA==} + '@types/graphql-validation-complexity@0.4.4': + resolution: {integrity: sha512-/0AmF5HYHmLErXRky/be86E4hAVO2LE5ejHdy3kf1rASgzBDG3Yb1yqoHxrZz+Nk72jt2UWCfa6DSfdpHDkuzg==} + '@types/hast@2.3.6': resolution: {integrity: sha512-47rJE80oqPmFdVDCD7IheXBrVdwuBgsYwoczFvKmwfo2Mzsnt+V9OONsYauFmICb6lQPpCuXYJWejBNs4pDJRg==} @@ -7670,6 +7701,13 @@ packages: codemirror: ^5.65.3 graphql: ^15.5.0 || ^16.0.0 + codemirror-graphql@2.2.0: + resolution: {integrity: sha512-egIiewf5zEH5LLSkJpJDpYxO1OkNruD0gTWiBrS1JmXk7yjt5WPw7jSmDRkWJx8JheHONltaJPNPWdTUT5LRIQ==} + peerDependencies: + '@codemirror/language': 6.0.0 + codemirror: ^5.65.3 + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + codemirror@5.65.15: resolution: {integrity: sha512-YC4EHbbwQeubZzxLl5G4nlbLc1T21QTrKGaOal/Pkm9dVDMZXMH7+ieSPEOZCtO9I68i8/oteJKOxzHC2zR+0g==} @@ -9214,6 +9252,10 @@ packages: get-tsconfig@4.7.2: resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + get-value@3.0.1: + resolution: {integrity: sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==} + engines: {node: '>=6.0'} + giget@2.0.0: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} hasBin: true @@ -9303,12 +9345,30 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphiql@3.8.3: + resolution: {integrity: sha512-cuPDYtXVKV86Pu5PHBX642Odi/uVEE2y1Jxq5rGO/Qy1G2lRp7ZZ7a/T30RzxhuLSWo9zUbzq0P3U49//H0Ugw==} + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + graphql-language-service@5.2.0: resolution: {integrity: sha512-o/ZgTS0pBxWm3hSF4+6GwiV1//DxzoLWEbS38+jqpzzy1d/QXBidwQuVYTOksclbtOJZ3KR/tZ8fi/tI6VpVMg==} hasBin: true peerDependencies: graphql: ^15.5.0 || ^16.0.0 + graphql-language-service@5.3.0: + resolution: {integrity: sha512-gCQIIy7lM9CB1KPLEb+DNZLczA9zuTLEOJE2hEQZTFYInogdmMDRa6RAkvM4LL0LcgcS+3uPs6KtHlcjCqRbUg==} + hasBin: true + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2 + + graphql-validation-complexity@0.4.2: + resolution: {integrity: sha512-4tzmN/70a06c2JH5fvISkoLX6oBDpqK22cvr2comge3HZHtBLD3n5Sl6MnQYMVhQqKGlpZWcCgD00MnyKNzYYg==} + peerDependencies: + graphql: '>=0.9.5' + graphql-ws@5.14.1: resolution: {integrity: sha512-aqkls1espsygP1PfkAuuLIV96IbztQ6EaADse97pw8wRIMT3+AL/OYfS8V2iCRkc0gzckitoDRGCQEdnySggiA==} engines: {node: '>=10'} @@ -10285,6 +10345,9 @@ packages: linkify-it@3.0.3: resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + list-item@1.1.1: resolution: {integrity: sha512-S3D0WZ4J6hyM8o5SNKWaMYB1ALSacPZ2nHGEuCjmHZ+dc03gFeNZoNDcqfcnO4vDhTZmNrqrpYZCdXsRh22bzw==} engines: {node: '>=0.10.0'} @@ -10482,6 +10545,10 @@ packages: resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==} hasBin: true + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + markdown-link@0.1.1: resolution: {integrity: sha512-TurLymbyLyo+kAUUAV9ggR9EPcDjP/ctlv9QAFiqUH7c+t6FlsbivPo9OKTU8xdOx9oNd2drW/Fi5RRElQbUqA==} engines: {node: '>=0.10.0'} @@ -10630,6 +10697,9 @@ packages: mdurl@1.0.1: resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + mdx-bundler@10.0.2: resolution: {integrity: sha512-0wF0zoCv+Ms4G+eSlk/jaKYoJHc0oXBaOma3kYlFJiKq9H8h41Dd66ioDBGF4noy80Pf7KTBQlyHfEpTqVml7A==} engines: {node: '>=18', npm: '>=6'} @@ -12150,6 +12220,10 @@ packages: psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -12222,6 +12296,11 @@ packages: react: ^16.8.5 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-compiler-runtime@19.0.0-beta-37ed2a7-20241206: + resolution: {integrity: sha512-9e6rCpVylr9EnVocgYAjft7+2v01BDpajeHKRoO+oc9pKcAMTpstHtHvE/TSVbyf4FvzCGjfKcfHM9XGTXI6Tw==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-contexify@5.0.0: resolution: {integrity: sha512-2FIp7lxJ6dtfGr8EZ4uVV5p5TQjd0n2h/JU7PrejNIMiCeZWvSVPFh4lj1ZvjXosglBvP7q5JQQ8yUCdSaMSaw==} engines: {node: '>=10'} @@ -13916,6 +13995,9 @@ packages: uc.micro@1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} @@ -16110,6 +16192,45 @@ snapshots: - '@types/react-dom' - graphql-ws + '@graphiql/react@0.28.2(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@graphiql/toolkit': 0.11.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0) + '@headlessui/react': 1.7.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': 1.1.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/codemirror': 5.60.10 + clsx: 1.2.1 + codemirror: 5.65.15 + codemirror-graphql: 2.2.0(@codemirror/language@6.11.0)(codemirror@5.65.15)(graphql@16.10.0) + copy-to-clipboard: 3.3.3 + framer-motion: 6.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + get-value: 3.0.1 + graphql: 16.10.0 + graphql-language-service: 5.3.0(graphql@16.10.0) + markdown-it: 14.1.0 + react: 18.3.1 + react-compiler-runtime: 19.0.0-beta-37ed2a7-20241206(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + set-value: 4.1.0 + transitivePeerDependencies: + - '@codemirror/language' + - '@types/node' + - '@types/react' + - '@types/react-dom' + - graphql-ws + + '@graphiql/toolkit@0.11.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)': + dependencies: + '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 + graphql: 16.10.0 + meros: 1.3.0(@types/node@20.12.11) + optionalDependencies: + graphql-ws: 5.14.1(graphql@16.10.0) + transitivePeerDependencies: + - '@types/node' + '@graphiql/toolkit@0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)': dependencies: '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 @@ -20467,6 +20588,10 @@ snapshots: '@types/geojson@7946.0.12': {} + '@types/graphql-validation-complexity@0.4.4': + dependencies: + graphql: 16.10.0 + '@types/hast@2.3.6': dependencies: '@types/unist': 2.0.8 @@ -21910,6 +22035,14 @@ snapshots: graphql: 16.10.0 graphql-language-service: 5.2.0(graphql@16.10.0) + codemirror-graphql@2.2.0(@codemirror/language@6.11.0)(codemirror@5.65.15)(graphql@16.10.0): + dependencies: + '@codemirror/language': 6.11.0 + '@types/codemirror': 0.0.90 + codemirror: 5.65.15 + graphql: 16.10.0 + graphql-language-service: 5.3.0(graphql@16.10.0) + codemirror@5.65.15: {} coffee-script@1.12.7: {} @@ -23599,6 +23732,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-value@3.0.1: + dependencies: + isobject: 3.0.1 + giget@2.0.0: dependencies: citty: 0.1.6 @@ -23727,12 +23864,37 @@ snapshots: graphemer@1.4.0: {} + graphiql@3.8.3(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@graphiql/react': 0.28.2(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + graphql: 16.10.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@codemirror/language' + - '@types/node' + - '@types/react' + - '@types/react-dom' + - graphql-ws + graphql-language-service@5.2.0(graphql@16.10.0): dependencies: graphql: 16.10.0 nullthrows: 1.1.1 vscode-languageserver-types: 3.17.5 + graphql-language-service@5.3.0(graphql@16.10.0): + dependencies: + debounce-promise: 3.1.2 + graphql: 16.10.0 + nullthrows: 1.1.1 + vscode-languageserver-types: 3.17.5 + + graphql-validation-complexity@0.4.2(graphql@16.10.0): + dependencies: + graphql: 16.10.0 + warning: 4.0.3 + graphql-ws@5.14.1(graphql@16.10.0): dependencies: graphql: 16.10.0 @@ -24865,6 +25027,10 @@ snapshots: dependencies: uc.micro: 1.0.6 + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + list-item@1.1.1: dependencies: expand-range: 1.8.2 @@ -25107,6 +25273,15 @@ snapshots: mdurl: 1.0.1 uc.micro: 1.0.6 + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-link@0.1.1: {} markdown-table@3.0.3: {} @@ -25490,6 +25665,8 @@ snapshots: mdurl@1.0.1: {} + mdurl@2.0.0: {} + mdx-bundler@10.0.2(esbuild@0.25.2)(supports-color@8.1.1): dependencies: '@babel/runtime': 7.26.10 @@ -26281,7 +26458,7 @@ snapshots: '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 '@types/statuses': 2.0.5 - graphql: 16.8.1 + graphql: 16.10.0 headers-polyfill: 4.0.3 is-node-process: 1.2.0 outvariant: 1.4.3 @@ -26307,7 +26484,7 @@ snapshots: '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 '@types/statuses': 2.0.5 - graphql: 16.8.1 + graphql: 16.10.0 headers-polyfill: 4.0.3 is-node-process: 1.2.0 outvariant: 1.4.3 @@ -27563,6 +27740,8 @@ snapshots: psl@1.9.0: {} + punycode.js@2.3.1: {} + punycode@2.3.0: {} punycode@2.3.1: @@ -27636,6 +27815,10 @@ snapshots: transitivePeerDependencies: - react-native + react-compiler-runtime@19.0.0-beta-37ed2a7-20241206(react@18.3.1): + dependencies: + react: 18.3.1 + react-contexify@5.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: clsx: 1.2.1 @@ -29768,6 +29951,8 @@ snapshots: uc.micro@1.0.6: {} + uc.micro@2.1.0: {} + ufo@1.5.4: {} uglify-js@3.17.4: From f3583a05bc33ca7302830d1285d2381c71356f94 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:39:46 -0400 Subject: [PATCH 48/86] fix(docs): branch for wrappers docs (#35340) Wrappers docs should not be pulled from the main branch, which contains unreleased content, but instead from the latest release tag. --- .../extensions/wrappers/[[...slug]]/page.tsx | 87 +++++++++++++++++-- apps/docs/lib/octokit.ts | 2 +- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx index 0bbbee54595..f5c6712dfcc 100644 --- a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -4,29 +4,96 @@ import { readFile } from 'node:fs/promises' import { join, relative } from 'node:path' import rehypeSlug from 'rehype-slug' import emoji from 'remark-emoji' - +import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' import { genGuideMeta, genGuidesStaticParams, removeRedundantH1, } from '~/features/docs/GuidesMdx.utils' -import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template' +import { REVALIDATION_TAGS } from '~/features/helpers.fetch' import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs' import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' -import { REVALIDATION_TAGS } from '~/features/helpers.fetch' +import { octokit } from '~/lib/octokit' export const dynamicParams = false // We fetch these docs at build time from an external repo const org = 'supabase' const repo = 'wrappers' -const branch = 'main' const docsDir = 'docs/catalog' const externalSite = 'https://supabase.github.io/wrappers' +type TagQueryResponse = { + repository: { + releases: { + nodes: + | { + tagName: string + }[] + | null + pageInfo: { + hasNextPage: boolean + endCursor: string | null + } + } + } +} + +const tagQuery = ` + query TagQuery($owner: String!, $name: String!, $after: String) { + repository(owner: $owner, name: $name) { + releases( + orderBy: { + field: CREATED_AT, + direction: DESC + }, + first: 5, + after: $after + ) { + nodes { + tagName + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + ` + +async function getLatestRelease(after: string | null = null) { + try { + const { + repository: { + releases: { + nodes, + pageInfo: { hasNextPage, endCursor }, + }, + }, + } = await octokit().graphql(tagQuery, { + owner: org, + name: repo, + after, + request: { + fetch: (url: RequestInfo | URL, options?: RequestInit) => + fetch(url, { ...options, next: { tags: [REVALIDATION_TAGS.WRAPPERS] } }), + }, + }) + + return ( + nodes?.find((node) => node?.tagName?.match(/^v\d+\.\d+\.\d+/)).tagName ?? + (hasNextPage && endCursor ? await getLatestRelease(endCursor) : null) + ) + } catch (error) { + console.error(`Error fetching release tags for wrappers federated pages: ${error}`) + return null + } +} + // Each external docs page is mapped to a local page const pageMap = [ { @@ -193,8 +260,14 @@ const getContent = async (params: Params) => { isExternal = true let remoteFile: string ;({ remoteFile, meta } = federatedPage) - const repoPath = `${org}/${repo}/${branch}/${docsDir}/${remoteFile}` - editLink = `${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}` + + const tag = await getLatestRelease() + if (!tag) { + throw new Error('No latest release found for federated wrappers pages') + } + + const repoPath = `${org}/${repo}/${tag}/${docsDir}/${remoteFile}` + editLink = `${org}/${repo}/blob/${tag}/${docsDir}/${remoteFile}` const response = await fetch(`https://raw.githubusercontent.com/${repoPath}`, { next: { tags: [REVALIDATION_TAGS.WRAPPERS] }, @@ -259,4 +332,4 @@ const generateStaticParams = async () => { const generateMetadata = genGuideMeta(getContent) export default WrappersDocs -export { generateStaticParams, generateMetadata } +export { generateMetadata, generateStaticParams } diff --git a/apps/docs/lib/octokit.ts b/apps/docs/lib/octokit.ts index bfe5a0f1b88..edad471ae79 100644 --- a/apps/docs/lib/octokit.ts +++ b/apps/docs/lib/octokit.ts @@ -8,7 +8,7 @@ import { fetchRevalidatePerDay } from '~/features/helpers.fetch' let octokitInstance: Octokit -function octokit() { +export function octokit() { if (!octokitInstance) { // https://github.com/gr2m/universal-github-app-jwt?tab=readme-ov-file#converting-pkcs1-to-pkcs8 const privateKeyPkcs8 = crypto From ca1cd24a079700a7badf0e5b321219979d55284b Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:51:07 -0400 Subject: [PATCH 49/86] fix (content api): return graphql-spec compliant error (#35202) GraphQL errors were occasionally returned as normal HTTP status code errors, switched to using only GraphQL-spec compliant errors for the /docs/api/graphql endpoint. --- apps/docs/app/api/graphql/route.test.ts | 27 +++++++++++++++---------- apps/docs/app/api/graphql/route.ts | 25 +++++++++++------------ apps/docs/app/api/utils.ts | 15 ++++++++++++-- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/apps/docs/app/api/graphql/route.test.ts b/apps/docs/app/api/graphql/route.test.ts index 77d12a09ac4..c60bd1d9a62 100644 --- a/apps/docs/app/api/graphql/route.test.ts +++ b/apps/docs/app/api/graphql/route.test.ts @@ -7,39 +7,44 @@ describe('GraphQL API endpoint', () => { vi.spyOn(console, 'error').mockImplementation(() => {}) }) - it('should return 400 if request body is not valid JSON', async () => { + it('should return error if request body is not valid JSON', async () => { const request = new Request('http://localhost/api/graphql', { method: 'POST', body: 'not json', }) const response = await POST(request) - expect(response.status).toBe(400) - expect(await response.text()).toBe('Invalid request: Request body must be valid JSON') + const json = await response.json() + expect(json.errors[0].message).toBe('Invalid request: Request body must be valid JSON') }) - it('should return 400 if request body is missing required fields', async () => { + it('should return error if request body is missing required fields', async () => { const request = new Request('http://localhost/api/graphql', { method: 'POST', body: JSON.stringify({ variables: {} }), }) const response = await POST(request) - expect(response.status).toBe(400) - expect(await response.text()).toContain('Request body must be a valid GraphQL request object') + const json = await response.json() + expect(json.errors[0].message).toContain( + 'Invalid request: Request body must be valid GraphQL request object' + ) }) - it('should return 400 if query is not a string', async () => { + it('should return error if query is not a string', async () => { const request = new Request('http://localhost/api/graphql', { method: 'POST', body: JSON.stringify({ query: 123 }), }) const response = await POST(request) - expect(response.status).toBe(400) + const json = await response.json() + expect(json.errors[0].message).toContain( + 'Invalid request: Request body must be valid GraphQL request object' + ) }) - it('should return 500 for internal server errors', async () => { + it('should return error for internal server errors', async () => { const request = new Request('http://localhost/api/graphql', { method: 'POST', body: JSON.stringify({ query: 'query { hello }' }), @@ -50,7 +55,7 @@ describe('GraphQL API endpoint', () => { }) const response = await POST(request) - expect(response.status).toBe(500) - expect(await response.text()).toBe('Internal Server Error') + const json = await response.json() + expect(json.errors[0].message).toBe('Internal Server Error') }) }) diff --git a/apps/docs/app/api/graphql/route.ts b/apps/docs/app/api/graphql/route.ts index 7efda7c254a..2655de05625 100644 --- a/apps/docs/app/api/graphql/route.ts +++ b/apps/docs/app/api/graphql/route.ts @@ -2,7 +2,7 @@ import { type DocumentNode, graphql, GraphQLError, parse, specifiedRules, valida import { createComplexityLimitRule } from 'graphql-validation-complexity' import { NextResponse } from 'next/server' import { z } from 'zod' -import { ApiError, InvalidRequestError } from '~/app/api/utils' +import { ApiError, convertZodToInvalidRequestError, InvalidRequestError } from '~/app/api/utils' import { rootGraphQLResolver } from '~/resources/rootResolver' import { rootGraphQLSchema } from '~/resources/rootSchema' import { createQueryDepthLimiter } from './validators' @@ -16,14 +16,14 @@ const graphQLRequestSchema = z.object({ }) async function handleGraphQLRequest(request: Request): Promise { - const body = await request.json().catch(() => { - throw new InvalidRequestError('Request body must be valid JSON') + const body = await request.json().catch((error) => { + throw new InvalidRequestError('Request body must be valid JSON', error) }) const parsedBody = graphQLRequestSchema.safeParse(body) if (!parsedBody.success) { - const errorMessage = parsedBody.error?.message - throw new InvalidRequestError( - `Request body must be a valid GraphQL request object${errorMessage ? `: ${errorMessage}` : ''}` + throw convertZodToInvalidRequestError( + parsedBody.error, + 'Request body must be valid GraphQL request object' ) } @@ -47,10 +47,7 @@ async function handleGraphQLRequest(request: Request): Promise { variableValues: variables, operationName, }) - // return NextResponse.json(result) - - // For now, just return a 404 response - return new NextResponse('Not Found', { status: 404 }) + return NextResponse.json(result) } function validateGraphQLRequest(query: string): ReadonlyArray { @@ -85,11 +82,13 @@ export async function POST(request: Request): Promise { console.error(error) if (error instanceof ApiError) { - return new NextResponse(error.isPrivate() ? 'Internal Server Error' : error.message, { - status: error.statusCode(), + return NextResponse.json({ + errors: [{ message: error.isPrivate() ? 'Internal Server Error' : error.message }], }) } else { - return new NextResponse('Internal Server Error', { status: 500 }) + return NextResponse.json({ + errors: [{ message: 'Internal Server Error' }], + }) } } } diff --git a/apps/docs/app/api/utils.ts b/apps/docs/app/api/utils.ts index 5b3f93ea15b..12e23bb605e 100644 --- a/apps/docs/app/api/utils.ts +++ b/apps/docs/app/api/utils.ts @@ -1,4 +1,4 @@ -import { z } from 'zod' +import { z, type ZodError } from 'zod' export class ApiError extends Error { constructor(message: string) { @@ -15,7 +15,7 @@ export class ApiError extends Error { } export class InvalidRequestError extends ApiError { - constructor(message: string) { + constructor(message: string, source?: unknown) { super(`Invalid request: ${message}`) } @@ -27,3 +27,14 @@ export class InvalidRequestError extends ApiError { return 400 } } + +export function convertZodToInvalidRequestError( + error: ZodError, + prelude?: string +): InvalidRequestError { + const issue = error.issues[0] + const pathStr = issue.path.join('.') + const message = `${prelude ? `${prelude}: ` : ''}${issue.message} at key "${pathStr}"` + + return new InvalidRequestError(message, error) +} From 9e8107c07055346a3c9dbe6205fe34bf51d8494d Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 29 Apr 2025 16:00:34 -0400 Subject: [PATCH 50/86] fix (content api): local dev introspection (#35279) * feat (content api): add top-level schema query Adds a top-level schema query that returns the stringified GraphQL schema for this endpoint. Useful for LLMs to access the stringified schema for prompting and tool calls. Query shape: ``` query Search { schema } ``` Query response: ``` { "data": { "schema": "schema {\n query: RootQueryType\n}\n\ntype RootQueryType {\n \"\"\"Get the GraphQL schema for this endpoint\"\"\"\n schema: String\n}" } } ``` Towards DOCS-214 * test (content api): add schema snapshot Add schema snapshot test to prevent unintentional changes to public API for this endpoint * fixup! feat (content api): add top-level schema query * fix (content api): local dev introspection The validation rules that are used for security in production are preventing GraphiQL from introspecting the schema in dev. Since this DX feature is incredibly useful, I'm turning off the complexity and depth validations for dev GraphiQL only. Local curls, etc., will still have production-standard validation so it can be tested locally. --- .../graphql/__snapshots__/route.test.ts.snap | 12 ++++++ apps/docs/app/api/graphql/route.test.ts | 25 ++++++++++- apps/docs/app/api/graphql/route.ts | 43 ++++++++++++------- apps/docs/resources/rootResolver.ts | 1 - apps/docs/resources/rootSchema.ts | 27 +++++++++++- 5 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap delete mode 100644 apps/docs/resources/rootResolver.ts diff --git a/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap b/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap new file mode 100644 index 00000000000..92aa9a51c06 --- /dev/null +++ b/apps/docs/app/api/graphql/__snapshots__/route.test.ts.snap @@ -0,0 +1,12 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`/api/graphql schema snapshot > should match snapshot 1`] = ` +"schema { + query: RootQueryType +} + +type RootQueryType { + """Get the GraphQL schema for this endpoint""" + schema: String! +}" +`; diff --git a/apps/docs/app/api/graphql/route.test.ts b/apps/docs/app/api/graphql/route.test.ts index c60bd1d9a62..f77a823659e 100644 --- a/apps/docs/app/api/graphql/route.test.ts +++ b/apps/docs/app/api/graphql/route.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { POST } from './route' -describe('GraphQL API endpoint', () => { +describe('/api/graphql basic error statuses', () => { beforeEach(() => { vi.clearAllMocks() vi.spyOn(console, 'error').mockImplementation(() => {}) @@ -59,3 +59,26 @@ describe('GraphQL API endpoint', () => { expect(json.errors[0].message).toBe('Internal Server Error') }) }) + +describe('/api/graphql schema snapshot', () => { + it('should match snapshot', async () => { + const schemaQuery = ` + query { + schema + } + ` + const request = new Request('http://localhost/api/graphql', { + method: 'POST', + body: JSON.stringify({ query: schemaQuery }), + }) + + const response = await POST(request) + const json = await response.json() + expect(json.errors).toBeUndefined() + + const { + data: { schema }, + } = json + expect(schema).toMatchSnapshot() + }) +}) diff --git a/apps/docs/app/api/graphql/route.ts b/apps/docs/app/api/graphql/route.ts index 2655de05625..c786b1c92fa 100644 --- a/apps/docs/app/api/graphql/route.ts +++ b/apps/docs/app/api/graphql/route.ts @@ -3,12 +3,34 @@ import { createComplexityLimitRule } from 'graphql-validation-complexity' import { NextResponse } from 'next/server' import { z } from 'zod' import { ApiError, convertZodToInvalidRequestError, InvalidRequestError } from '~/app/api/utils' -import { rootGraphQLResolver } from '~/resources/rootResolver' +import { BASE_PATH, IS_DEV } from '~/lib/constants' import { rootGraphQLSchema } from '~/resources/rootSchema' import { createQueryDepthLimiter } from './validators' export const runtime = 'edge' +const MAX_DEPTH = 9 + +const validationRules = [ + ...specifiedRules, + createQueryDepthLimiter(MAX_DEPTH), + createComplexityLimitRule(1500, { + scalarCost: 1, + objectCost: 2, + listFactor: 10, + }), +] + +function isDevGraphiQL(request: Request) { + const origin = request.headers.get('Origin') + const referrer = request.headers.get('Referer') + return ( + IS_DEV && + origin.startsWith('http://localhost') && + referrer === `${origin}${BASE_PATH ?? ''}/graphiql` + ) +} + const graphQLRequestSchema = z.object({ query: z.string(), variables: z.record(z.any()).optional(), @@ -28,7 +50,7 @@ async function handleGraphQLRequest(request: Request): Promise { } const { query, variables, operationName } = parsedBody.data - const validationErrors = validateGraphQLRequest(query) + const validationErrors = validateGraphQLRequest(query, isDevGraphiQL(request)) if (validationErrors.length > 0) { return NextResponse.json({ errors: validationErrors.map((error) => ({ @@ -41,7 +63,6 @@ async function handleGraphQLRequest(request: Request): Promise { const result = await graphql({ schema: rootGraphQLSchema, - rootValue: rootGraphQLResolver, contextValue: { request }, source: query, variableValues: variables, @@ -50,9 +71,7 @@ async function handleGraphQLRequest(request: Request): Promise { return NextResponse.json(result) } -function validateGraphQLRequest(query: string): ReadonlyArray { - const MAX_DEPTH = 9 - +function validateGraphQLRequest(query: string, isDevGraphiQL = false): ReadonlyArray { let documentAST: DocumentNode try { documentAST = parse(query) @@ -63,16 +82,8 @@ function validateGraphQLRequest(query: string): ReadonlyArray { throw error } } - const validationRules = [ - ...specifiedRules, - createQueryDepthLimiter(MAX_DEPTH), - createComplexityLimitRule(1500, { - scalarCost: 1, - objectCost: 2, - listFactor: 10, - }), - ] - return validate(rootGraphQLSchema, documentAST, validationRules) + const rules = isDevGraphiQL ? specifiedRules : validationRules + return validate(rootGraphQLSchema, documentAST, rules) } export async function POST(request: Request): Promise { diff --git a/apps/docs/resources/rootResolver.ts b/apps/docs/resources/rootResolver.ts deleted file mode 100644 index c65cd01a5b6..00000000000 --- a/apps/docs/resources/rootResolver.ts +++ /dev/null @@ -1 +0,0 @@ -export const rootGraphQLResolver = {} diff --git a/apps/docs/resources/rootSchema.ts b/apps/docs/resources/rootSchema.ts index 7dd25048a82..05c5d6d0f1b 100644 --- a/apps/docs/resources/rootSchema.ts +++ b/apps/docs/resources/rootSchema.ts @@ -1,8 +1,31 @@ -import { GraphQLObjectType, GraphQLSchema } from 'graphql' +import { + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + printSchema, +} from 'graphql' + +const GRAPHQL_FIELD_INTROSPECT = 'schema' + +async function resolveIntrospect() { + const schema = printSchema(rootGraphQLSchema) + return schema +} + +const introspectRoot = { + [GRAPHQL_FIELD_INTROSPECT]: { + description: 'Get the GraphQL schema for this endpoint', + type: new GraphQLNonNull(GraphQLString), + resolve: resolveIntrospect, + }, +} export const rootGraphQLSchema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQueryType', - fields: {}, + fields: { + ...introspectRoot, + }, }), }) From 65d0a3cd9b1492cf4a490d5eb6ebd0b1c21edae8 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 29 Apr 2025 20:09:42 -0230 Subject: [PATCH 51/86] Remove logo (#35366) --- apps/www/components/logos.tsx | 5 ----- apps/www/public/images/logos/publicity/krea.svg | 10 ---------- 2 files changed, 15 deletions(-) delete mode 100644 apps/www/public/images/logos/publicity/krea.svg diff --git a/apps/www/components/logos.tsx b/apps/www/components/logos.tsx index 12f0e8b9440..b2ef09c79c7 100644 --- a/apps/www/components/logos.tsx +++ b/apps/www/components/logos.tsx @@ -78,11 +78,6 @@ const logos = [ alt: 'humata', name: 'humata', }, - { - image: `/images/logos/publicity/krea.svg`, - alt: 'krea', - name: 'krea', - }, { image: `/images/logos/publicity/udio.svg`, alt: 'udio', diff --git a/apps/www/public/images/logos/publicity/krea.svg b/apps/www/public/images/logos/publicity/krea.svg deleted file mode 100644 index 76079718c47..00000000000 --- a/apps/www/public/images/logos/publicity/krea.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - From f08e5a3c179e36caf8e066ccf8870a1030dff56d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 30 Apr 2025 13:58:20 +0800 Subject: [PATCH 52/86] Tiny update to delete confirmation modal (#35321) --- .../StorageExplorer/ConfirmDeleteModal.tsx | 34 +++++++------------ .../StorageExplorer/StorageExplorer.tsx | 2 +- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ConfirmDeleteModal.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ConfirmDeleteModal.tsx index 6f041a1de0b..1aefdb6456d 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ConfirmDeleteModal.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/ConfirmDeleteModal.tsx @@ -1,6 +1,7 @@ import { noop } from 'lodash' import { useEffect, useState } from 'react' -import { Alert, Button, Modal } from 'ui' + +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { StorageItem } from '../Storage.types' interface ConfirmDeleteModalProps { @@ -10,7 +11,7 @@ interface ConfirmDeleteModalProps { onSelectDelete: () => void } -const ConfirmDeleteModal = ({ +export const ConfirmDeleteModal = ({ visible = false, selectedItemsToDelete = [], onSelectCancel = noop, @@ -42,28 +43,19 @@ const ConfirmDeleteModal = ({ } return ( - {title}} + title={{title}} size="medium" onCancel={onSelectCancel} - customFooter={ -
- - -
- } - > - - - {description} - - -
+ onConfirm={onConfirmDelete} + variant="destructive" + alert={{ + base: { variant: 'destructive' }, + title: 'This action cannot be undone', + description, + }} + /> ) } diff --git a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx index a6d131379d2..020eacb45ab 100644 --- a/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx +++ b/apps/studio/components/to-be-cleaned/Storage/StorageExplorer/StorageExplorer.tsx @@ -9,7 +9,7 @@ import { IS_PLATFORM } from 'lib/constants' import { useStorageStore } from 'localStores/storageExplorer/StorageExplorerStore' import { useStorageExplorerStateSnapshot } from 'state/storage-explorer' import { STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants' -import ConfirmDeleteModal from './ConfirmDeleteModal' +import { ConfirmDeleteModal } from './ConfirmDeleteModal' import CustomExpiryModal from './CustomExpiryModal' import FileExplorer from './FileExplorer' import FileExplorerHeader from './FileExplorerHeader' From a5f9142fd406ca1bd9f79647dd1b9517e1e2b2bd Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Wed, 30 Apr 2025 08:07:59 +0200 Subject: [PATCH 53/86] fix logs datepicker handling global copy events (#35361) --- .../interfaces/Settings/Logs/Logs.DatePickers.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx b/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx index a705ec69390..a113cd1539f 100644 --- a/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/Logs.DatePickers.tsx @@ -10,6 +10,7 @@ import TimeSplitInput from 'components/ui/DatePicker/TimeSplitInput' import { Button, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_, cn } from 'ui' import { LOGS_LARGE_DATE_RANGE_DAYS_THRESHOLD } from './Logs.constants' import type { DatetimeHelper } from './Logs.types' +import { copyToClipboard } from 'lib/helpers' export type DatePickerValue = { to: string @@ -175,12 +176,13 @@ export const LogsDatePicker = ({ onSubmit, helpers, value }: PropsWithChildren

{ - document.addEventListener('paste', handlePaste) - document.addEventListener('copy', handleCopy) + if (open) { + document.addEventListener('paste', handlePaste) + document.addEventListener('copy', handleCopy) + } return () => { document.removeEventListener('paste', handlePaste) document.removeEventListener('copy', handleCopy) } - }, [startDate, endDate]) + }, [open, startDate, endDate]) const isLargeRange = Math.abs(dayjs(startDate).diff(dayjs(endDate), 'days')) > From 21bbc93afa4fbd7269e4d1ee9b78384060ad07aa Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Wed, 30 Apr 2025 14:19:21 +0800 Subject: [PATCH 54/86] Chore/table editor filter sorts logic moved to hooks (#35138) * init * update Popovers to use new hooks * Update Header.tsx * made primitive components for filter and sorts * Delete FilterPopoverWrapper.tsx * Delete SortPopoverWrapper.tsx * remove * Create README.md * Update README.md * fix sort popover issues * Update SupabaseGrid.tsx * move DeleteConfirmationDialogs into context * fix issue with * more stuff for alaister * fix ts and tables pages * First round of clean up * Update README.md * Smol fix * Fix issues identified * Smol fix * Fix updating table name in database/tables not invalidating * Improve SQL editor invalidation logic * Add fix to reopen last opened table when landing on table editor * Smol fix --------- Co-authored-by: Alaister Young Co-authored-by: Joshen Lim --- apps/studio/components/grid/SupabaseGrid.tsx | 67 ++--- .../grid/components/header/Header.tsx | 103 ++------ .../header/filter/FilterPopover.tsx | 143 +---------- .../header/filter/FilterPopoverPrimitive.tsx | 136 ++++++++++ .../components/header/sort/SortPopover.tsx | 164 +----------- .../header/sort/SortPopoverPrimitive.tsx | 236 ++++++++++++++++++ .../grid/components/header/sort/index.ts | 1 - apps/studio/components/grid/hooks/README.md | 101 ++++++++ .../grid/hooks/useSaveTableEditorState.ts | 58 +++++ .../components/grid/hooks/useTableFilter.ts | 32 +++ .../components/grid/hooks/useTableSort.ts | 50 ++++ .../DeleteConfirmationDialogs.tsx | 87 ++----- .../ForeignRowSelector/ForeignRowSelector.tsx | 36 ++- .../SidePanelEditor/SidePanelEditor.utils.tsx | 1 + .../TableGridEditor/TableGridEditor.tsx | 40 ++- .../TableEditorLayout/TableEditorMenu.tsx | 4 +- apps/studio/data/sql/execute-sql-mutation.ts | 91 ++----- apps/studio/data/table-editor/keys.ts | 4 +- .../data/table-editor/table-editor-types.ts | 37 ++- .../table-row-delete-all-mutation.ts | 5 +- .../data/table-rows/table-rows-query.ts | 45 ++-- apps/studio/data/table-rows/utils.ts | 12 +- .../project/[ref]/database/tables/[id].tsx | 16 +- .../project/[ref]/database/tables/index.tsx | 29 ++- .../pages/project/[ref]/editor/[id].tsx | 28 +-- .../pages/project/[ref]/editor/index.tsx | 4 +- apps/studio/state/table-editor-table.tsx | 5 +- apps/studio/state/table-editor.tsx | 1 - 28 files changed, 872 insertions(+), 664 deletions(-) create mode 100644 apps/studio/components/grid/components/header/filter/FilterPopoverPrimitive.tsx create mode 100644 apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx delete mode 100644 apps/studio/components/grid/components/header/sort/index.ts create mode 100644 apps/studio/components/grid/hooks/README.md create mode 100644 apps/studio/components/grid/hooks/useSaveTableEditorState.ts create mode 100644 apps/studio/components/grid/hooks/useTableFilter.ts create mode 100644 apps/studio/components/grid/hooks/useTableSort.ts diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx index d24332ae8f5..d6dfe395600 100644 --- a/apps/studio/components/grid/SupabaseGrid.tsx +++ b/apps/studio/components/grid/SupabaseGrid.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react' +import { PropsWithChildren, useEffect, useRef, useState } from 'react' import { DataGridHandle } from 'react-data-grid' import { DndProvider } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' @@ -7,24 +7,21 @@ import { createPortal } from 'react-dom' import { useParams } from 'common' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useTableRowsQuery } from 'data/table-rows/table-rows-query' -import { useTableEditorFiltersSort } from 'hooks/misc/useTableEditorFiltersSort' import { RoleImpersonationState } from 'lib/role-impersonation' import { EMPTY_ARR } from 'lib/void' import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state' import { useTableEditorStateSnapshot } from 'state/table-editor' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' -import { - filtersToUrlParams, - formatFilterURLParams, - formatSortURLParams, - saveTableEditorStateToLocalStorage, -} from './SupabaseGrid.utils' + import { Shortcuts } from './components/common/Shortcuts' import Footer from './components/footer/Footer' import { Grid } from './components/grid/Grid' import Header, { HeaderProps } from './components/header/Header' import { RowContextMenu } from './components/menu' -import { Filter, GridProps } from './types' +import { GridProps } from './types' + +import { useTableFilter } from './hooks/useTableFilter' +import { useTableSort } from './hooks/useTableSort' export const SupabaseGrid = ({ customHeader, @@ -39,43 +36,15 @@ export const SupabaseGrid = ({ const tableId = _id ? Number(_id) : undefined const { project } = useProjectContext() + const tableEditorSnap = useTableEditorStateSnapshot() const snap = useTableEditorTableStateSnapshot() const gridRef = useRef(null) const [mounted, setMounted] = useState(false) - const { filters: filter, sorts: sort, setParams } = useTableEditorFiltersSort() - - const sorts = formatSortURLParams(snap.table.name, sort as string[] | undefined) - const filters = formatFilterURLParams(filter as string[]) - - const onApplyFilters = useCallback( - (appliedFilters: Filter[]) => { - snap.setEnforceExactCount(false) - // Reset page to 1 when filters change - snap.setPage(1) - - const filters = filtersToUrlParams(appliedFilters) - - setParams((prevParams) => { - return { - ...prevParams, - filter: filters, - } - }) - - if (project?.ref) { - saveTableEditorStateToLocalStorage({ - projectRef: project.ref, - tableName: snap.table.name, - schema: snap.table.schema, - filters: filters, - }) - } - }, - [project?.ref, snap.table.name, snap.table.schema] - ) + const { filters, onApplyFilters } = useTableFilter() + const { sorts, onApplySorts } = useTableSort() const roleImpersonationState = useRoleImpersonationStateSnapshot() @@ -93,15 +62,13 @@ export const SupabaseGrid = ({ { keepPreviousData: true, retryDelay: (retryAttempt, error: any) => { - if (error && error.message?.includes('does not exist')) { - setParams((prevParams) => { - return { - ...prevParams, - ...{ sort: undefined }, - } - }) - } - if (retryAttempt > 3) { + const doesNotExistError = error && error.message?.includes('does not exist') + const tooManyRequestsError = error.message?.includes('Too Many Requests') + const vaultError = error.message?.includes('query vault failed') + + if (doesNotExistError) onApplySorts([]) + + if (retryAttempt > 3 || doesNotExistError || tooManyRequestsError || vaultError) { return Infinity } return 5000 @@ -118,7 +85,7 @@ export const SupabaseGrid = ({ return (

-
+
{children || ( <> diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index f88b172e15f..55847f98d3e 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -3,16 +3,12 @@ import saveAs from 'file-saver' import { ArrowUp, ChevronDown, FileText, Trash } from 'lucide-react' import Link from 'next/link' import Papa from 'papaparse' -import { ReactNode, useCallback, useState } from 'react' +import { ReactNode, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' -import { - filtersToUrlParams, - saveTableEditorStateToLocalStorage, - sortsToUrlParams, -} from 'components/grid/SupabaseGrid.utils' -import type { Filter, Sort } from 'components/grid/types' +import { useTableFilter } from 'components/grid/hooks/useTableFilter' +import { useTableSort } from 'components/grid/hooks/useTableSort' import GridHeaderActions from 'components/interfaces/TableGridEditor/GridHeaderActions' import { formatTableRowsToSQL } from 'components/interfaces/TableGridEditor/TableEntity.utils' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -22,7 +18,6 @@ import { fetchAllTableRows, useTableRowsQuery } from 'data/table-rows/table-rows import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { useTableEditorFiltersSort } from 'hooks/misc/useTableEditorFiltersSort' import { RoleImpersonationState } from 'lib/role-impersonation' import { useRoleImpersonationStateSnapshot, @@ -40,8 +35,8 @@ import { Separator, SonnerProgress, } from 'ui' -import FilterPopover from './filter/FilterPopover' -import { SortPopover } from './sort' +import { FilterPopover } from './filter/FilterPopover' +import { SortPopover } from './sort/SortPopover' // [Joshen] CSV exports require this guard as a fail-safe if the table is // just too large for a browser to keep all the rows in memory before // exporting. Either that or export as multiple CSV sheets with max n rows each @@ -58,27 +53,21 @@ export const MAX_EXPORT_ROW_COUNT_MESSAGE = ( ) export type HeaderProps = { - sorts: Sort[] - filters: Filter[] customHeader: ReactNode } -const Header = ({ sorts, filters, customHeader }: HeaderProps) => { +const Header = ({ customHeader }: HeaderProps) => { const snap = useTableEditorTableStateSnapshot() return (
{customHeader ? ( - <>{customHeader} + customHeader + ) : snap.selectedRows.size > 0 ? ( + ) : ( - <> - {snap.selectedRows.size > 0 ? ( - - ) : ( - - )} - + )}
@@ -93,6 +82,8 @@ const DefaultHeader = () => { const tableEditorSnap = useTableEditorStateSnapshot() const snap = useTableEditorTableStateSnapshot() const org = useSelectedOrganization() + const canCreateColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns') + const { mutate: sendEvent } = useSendEventMutation() const onAddRow = snap.editable && (snap.table.columns ?? []).length > 0 ? tableEditorSnap.onAddRow : undefined @@ -101,68 +92,11 @@ const DefaultHeader = () => { const canAddNew = onAddRow !== undefined || onAddColumn !== undefined - // [Joshen] Using this logic to block both column and row creation/update/delete - const canCreateColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns') - - const { filters, sorts, setParams } = useTableEditorFiltersSort() - - const onApplyFilters = useCallback( - (appliedFilters: Filter[]) => { - snap.setEnforceExactCount(false) - // Reset page to 1 when filters change - snap.setPage(1) - - const newFilters = filtersToUrlParams(appliedFilters) - - setParams((prevParams) => { - return { - filter: newFilters, - sort: prevParams.sort, - } - }) - - if (projectRef) { - saveTableEditorStateToLocalStorage({ - projectRef, - tableName: snap.table.name, - schema: snap.table.schema, - filters: newFilters, - }) - } - }, - [projectRef, snap.table.name, snap.table.schema, setParams] - ) - - const onApplySorts = useCallback( - (appliedSorts: Sort[]) => { - const newSorts = sortsToUrlParams(appliedSorts) - - setParams((prevParams) => { - return { - filter: prevParams.filter, - sort: newSorts, - } - }) - - if (projectRef) { - saveTableEditorStateToLocalStorage({ - projectRef, - tableName: snap.table.name, - schema: snap.table.schema, - sorts: newSorts, - }) - } - }, - [projectRef, snap.table.name, snap.table.schema, setParams] - ) - - const { mutate: sendEvent } = useSendEventMutation() - return (
- - + +
{canAddNew && ( <> @@ -284,11 +218,7 @@ const DefaultHeader = () => { ) } -type RowHeaderProps = { - sorts: Sort[] - filters: Filter[] -} -const RowHeader = ({ sorts, filters }: RowHeaderProps) => { +const RowHeader = () => { const { project } = useProjectContext() const tableEditorSnap = useTableEditorStateSnapshot() const snap = useTableEditorTableStateSnapshot() @@ -296,6 +226,9 @@ const RowHeader = ({ sorts, filters }: RowHeaderProps) => { const roleImpersonationState = useRoleImpersonationStateSnapshot() const isImpersonatingRole = roleImpersonationState.role !== undefined + const { filters } = useTableFilter() + const { sorts } = useTableSort() + const [isExporting, setIsExporting] = useState(false) const { data } = useTableRowsQuery({ diff --git a/apps/studio/components/grid/components/header/filter/FilterPopover.tsx b/apps/studio/components/grid/components/header/filter/FilterPopover.tsx index ccc6b91295c..cab1b6e51b1 100644 --- a/apps/studio/components/grid/components/header/filter/FilterPopover.tsx +++ b/apps/studio/components/grid/components/header/filter/FilterPopover.tsx @@ -1,145 +1,22 @@ -import update from 'immutability-helper' -import { isEqual } from 'lodash' -import { FilterIcon, Plus } from 'lucide-react' -import { KeyboardEvent, useCallback, useMemo, useState } from 'react' +import { useMemo } from 'react' +import { useTableFilter } from 'components/grid/hooks/useTableFilter' import { formatFilterURLParams } from 'components/grid/SupabaseGrid.utils' -import type { Filter } from 'components/grid/types' -import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' -import { - Button, - PopoverContent_Shadcn_, - PopoverSeparator_Shadcn_, - PopoverTrigger_Shadcn_, - Popover_Shadcn_, -} from 'ui' -import FilterRow from './FilterRow' +import { FilterPopoverPrimitive } from './FilterPopoverPrimitive' export interface FilterPopoverProps { - filters: string[] portal?: boolean - onApplyFilters: (filters: Filter[]) => void } -const FilterPopover = ({ filters, portal = true, onApplyFilters }: FilterPopoverProps) => { - const [open, setOpen] = useState(false) +export const FilterPopover = ({ portal = true }: FilterPopoverProps) => { + const { urlFilters, onApplyFilters } = useTableFilter() - const btnText = - (filters || []).length > 0 - ? `Filtered by ${filters.length} rule${filters.length > 1 ? 's' : ''}` - : 'Filter' + // Convert string[] to Filter[] + const filters = useMemo(() => { + return formatFilterURLParams(urlFilters ?? []) + }, [urlFilters]) return ( - - - - - - - - - ) -} - -export default FilterPopover - -interface FilterOverlayProps { - filters: string[] - onApplyFilters: (filter: Filter[]) => void -} - -const FilterOverlay = ({ filters: filtersFromUrl, onApplyFilters }: FilterOverlayProps) => { - const snap = useTableEditorTableStateSnapshot() - - const initialFilters = useMemo( - () => formatFilterURLParams((filtersFromUrl as string[]) ?? []), - [filtersFromUrl] - ) - const [filters, setFilters] = useState(initialFilters) - - function onAddFilter() { - const column = snap.table.columns[0]?.name - - if (column) { - setFilters([ - ...filters, - { - column, - operator: '=', - value: '', - }, - ]) - } - } - - const onChangeFilter = useCallback((index: number, filter: Filter) => { - setFilters((currentFilters) => - update(currentFilters, { - [index]: { - $set: filter, - }, - }) - ) - }, []) - - const onDeleteFilter = useCallback((index: number) => { - setFilters((currentFilters) => - update(currentFilters, { - $splice: [[index, 1]], - }) - ) - }, []) - - const onSelectApplyFilters = () => { - // [Joshen] Trim empty spaces in input for only UUID type columns - const formattedFilters = filters.map((f) => { - const column = snap.table.columns.find((c) => c.name === f.column) - if (column?.format === 'uuid') return { ...f, value: f.value.trim() } - else return f - }) - setFilters(formattedFilters) - onApplyFilters(formattedFilters) - } - - function handleEnterKeyDown(event: KeyboardEvent) { - if (event.key === 'Enter') onSelectApplyFilters() - } - - return ( -
-
- {filters.map((filter, index) => ( - - ))} - {filters.length == 0 && ( -
-
No filters applied to this view
-

Add a column below to filter the view

-
- )} -
- -
- - -
-
+ ) } diff --git a/apps/studio/components/grid/components/header/filter/FilterPopoverPrimitive.tsx b/apps/studio/components/grid/components/header/filter/FilterPopoverPrimitive.tsx new file mode 100644 index 00000000000..8891ff8dcef --- /dev/null +++ b/apps/studio/components/grid/components/header/filter/FilterPopoverPrimitive.tsx @@ -0,0 +1,136 @@ +import { isEqual } from 'lodash' +import { Filter as FilterIcon, Plus } from 'lucide-react' +import { KeyboardEvent, useCallback, useMemo, useState } from 'react' + +import type { Filter } from 'components/grid/types' +import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' +import { + Button, + PopoverContent_Shadcn_, + PopoverSeparator_Shadcn_, + PopoverTrigger_Shadcn_, + Popover_Shadcn_, +} from 'ui' +import FilterRow from './FilterRow' + +export interface FilterPopoverPrimitiveProps { + buttonText?: string + filters: Filter[] + onApplyFilters: (filters: Filter[]) => void + portal?: boolean +} + +export const FilterPopoverPrimitive = ({ + buttonText, + filters, + onApplyFilters, + portal = true, +}: FilterPopoverPrimitiveProps) => { + const [open, setOpen] = useState(false) + const snap = useTableEditorTableStateSnapshot() + + // Internal state management + const [localFilters, setLocalFilters] = useState(filters) + + // Update local state when filters prop changes + useMemo(() => { + setLocalFilters(filters) + }, [filters]) + + const displayButtonText = + buttonText ?? + (filters.length > 0 + ? `Filtered by ${filters.length} rule${filters.length > 1 ? 's' : ''}` + : 'Filter') + + const onAddFilter = () => { + const column = snap.table.columns[0]?.name + if (column) { + setLocalFilters([ + ...localFilters, + { + column, + operator: '=', + value: '', + }, + ]) + } + } + + const onChangeFilter = useCallback((index: number, filter: Filter) => { + setLocalFilters((currentFilters) => [ + ...currentFilters.slice(0, index), + filter, + ...currentFilters.slice(index + 1), + ]) + }, []) + + const onDeleteFilter = useCallback((index: number) => { + setLocalFilters((currentFilters) => [ + ...currentFilters.slice(0, index), + ...currentFilters.slice(index + 1), + ]) + }, []) + + const onSelectApplyFilters = () => { + // [Joshen] Trim empty spaces in input for only UUID type columns + const formattedFilters = localFilters.map((f) => { + const column = snap.table.columns.find((c) => c.name === f.column) + if (column?.format === 'uuid') return { ...f, value: f.value.trim() } + else return f + }) + setLocalFilters(formattedFilters) + onApplyFilters(formattedFilters) + } + + function handleEnterKeyDown(event: KeyboardEvent) { + if (event.key === 'Enter') onSelectApplyFilters() + } + + return ( + + + + + +
+
+ {localFilters.map((filter, index) => ( + + ))} + {localFilters.length == 0 && ( +
+
No filters applied to this view
+

+ Add a column below to filter the view +

+
+ )} +
+ +
+ + +
+
+
+
+ ) +} diff --git a/apps/studio/components/grid/components/header/sort/SortPopover.tsx b/apps/studio/components/grid/components/header/sort/SortPopover.tsx index cbd90ecb274..f7cc7e20400 100644 --- a/apps/studio/components/grid/components/header/sort/SortPopover.tsx +++ b/apps/studio/components/grid/components/header/sort/SortPopover.tsx @@ -1,163 +1,23 @@ -import update from 'immutability-helper' -import { isEqual } from 'lodash' -import { ChevronDown, List } from 'lucide-react' -import { useCallback, useMemo, useState } from 'react' +import { useMemo } from 'react' import { formatSortURLParams } from 'components/grid/SupabaseGrid.utils' -import { DropdownControl } from 'components/grid/components/common/DropdownControl' -import type { Sort } from 'components/grid/types' +import { useTableSort } from 'components/grid/hooks/useTableSort' import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' -import { - Button, - PopoverContent_Shadcn_, - PopoverSeparator_Shadcn_, - PopoverTrigger_Shadcn_, - Popover_Shadcn_, -} from 'ui' -import SortRow from './SortRow' +import { SortPopoverPrimitive } from './SortPopoverPrimitive' export interface SortPopoverProps { - sorts: string[] portal?: boolean - onApplySorts: (sorts: Sort[]) => void } -const SortPopover = ({ sorts, portal = true, onApplySorts }: SortPopoverProps) => { - const [open, setOpen] = useState(false) +export const SortPopover = ({ portal = true }: SortPopoverProps) => { + const { urlSorts, onApplySorts } = useTableSort() + const tableState = useTableEditorTableStateSnapshot() + const tableName = tableState?.table?.name || '' - const btnText = - (sorts || []).length > 0 - ? `Sorted by ${sorts.length} rule${sorts.length > 1 ? 's' : ''}` - : 'Sort' + // Convert string[] to Sort[] + const sorts = useMemo(() => { + return tableName && urlSorts ? formatSortURLParams(tableName, urlSorts) : [] + }, [tableName, urlSorts]) - return ( - - - - - - - - - ) -} - -export default SortPopover - -export interface SortOverlayProps { - sorts: string[] - onApplySorts: (sorts: Sort[]) => void -} - -const SortOverlay = ({ sorts: sortsFromUrl, onApplySorts }: SortOverlayProps) => { - const snap = useTableEditorTableStateSnapshot() - - const initialSorts = useMemo( - () => formatSortURLParams(snap.table.name, sortsFromUrl ?? []), - [snap.table.name, sortsFromUrl] - ) - const [sorts, setSorts] = useState(initialSorts) - - const columns = snap.table.columns.filter((x) => { - // exclude json/jsonb columns from sorting. Sorting by json fields in PG is only possible if you provide key from - // the JSON object. - if (x.dataType === 'json' || x.dataType === 'jsonb') { - return false - } - const found = sorts.find((y) => y.column == x.name) - return !found - }) - - const dropdownOptions = - columns?.map((x) => { - return { value: x.name, label: x.name } - }) || [] - - function onAddSort(columnName: string | number) { - setSorts([...sorts, { table: snap.table.name, column: columnName as string, ascending: true }]) - } - - const onDeleteSort = useCallback((column: string) => { - setSorts((currentSorts) => currentSorts.filter((sort) => sort.column !== column)) - }, []) - - const onToggleSort = useCallback((column: string, ascending: boolean) => { - setSorts((currentSorts) => { - const idx = currentSorts.findIndex((x) => x.column === column) - - return update(currentSorts, { - [idx]: { - $merge: { ascending }, - }, - }) - }) - }, []) - - const onDragSort = useCallback((dragIndex: number, hoverIndex: number) => { - setSorts((currentSort) => - update(currentSort, { - $splice: [ - [dragIndex, 1], - [hoverIndex, 0, currentSort[dragIndex]], - ], - }) - ) - }, []) - - return ( -
- {sorts.map((sort, index) => ( - - ))} - {sorts.length === 0 && ( -
-
No sorts applied to this view
-

Add a column below to sort the view

-
- )} - - -
- {columns && columns.length > 0 ? ( - - - - ) : ( -

All columns have been added

- )} -
- -
-
-
- ) + return } diff --git a/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx b/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx new file mode 100644 index 00000000000..7cba3696a96 --- /dev/null +++ b/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx @@ -0,0 +1,236 @@ +import { isEqual } from 'lodash' +import { ChevronDown, List } from 'lucide-react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' + +import type { Sort } from 'components/grid/types' +import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' +import { + Button, + PopoverContent_Shadcn_, + PopoverSeparator_Shadcn_, + PopoverTrigger_Shadcn_, + Popover_Shadcn_, +} from 'ui' +import { DropdownControl } from '../../common/DropdownControl' +import SortRow from './SortRow' + +export interface SortPopoverPrimitiveProps { + buttonText?: string + sorts: Sort[] + onApplySorts: (sorts: Sort[]) => void + portal?: boolean +} + +/** + * SortPopoverPrimitive - A component for sorting table columns + * + * This component maintains a draft state of sorts while editing, then applies + * them to the parent component when "Apply" is clicked. + * + * To avoid issues with drag-and-drop reconciliation, we use a special sync mechanism + * that properly detects external vs. internal updates. + */ +export const SortPopoverPrimitive = ({ + buttonText, + sorts, + onApplySorts, + portal = true, +}: SortPopoverPrimitiveProps) => { + const [open, setOpen] = useState(false) + const snap = useTableEditorTableStateSnapshot() + + // Local state for draft sorts + const [localSorts, setLocalSorts] = useState(sorts) + + // Track the last props we received for comparison + const lastSortsRef = useRef(sorts) + // Track if we're in the middle of applying our own changes + const isApplyingRef = useRef(false) + + // Sync with props when they change, but in a smarter way + useEffect(() => { + // If we're in the middle of applying changes, don't sync from props + if (isApplyingRef.current) { + isApplyingRef.current = false + return + } + + // If the props changed unexpectedly (not due to our own actions) + // then we should update our local state + if (!isEqual(sorts, lastSortsRef.current)) { + setLocalSorts(sorts) + lastSortsRef.current = sorts + } + }, [sorts]) + + // Fix: Use localSorts for button text, not sorts + const displayButtonText = + buttonText ?? + (localSorts.length > 0 + ? `Sorted by ${localSorts.length} rule${localSorts.length > 1 ? 's' : ''}` + : 'Sort') + + // Filter available columns to exclude columns already in sorts + const columns = useMemo(() => { + if (!snap?.table?.columns) return [] + return snap.table.columns.filter((x) => { + if (x.dataType === 'json' || x.dataType === 'jsonb') return false + const found = localSorts.find((y) => y.column == x.name) + return !found + }) + }, [snap?.table?.columns, localSorts]) + + // Format the columns for the dropdown + const dropdownOptions = useMemo(() => { + return columns?.map((x) => ({ value: x.name, label: x.name })) || [] + }, [columns]) + + // Add a new sort + const onAddSort = (columnName: string | number) => { + const currentTableName = snap.table?.name + if (currentTableName) { + setLocalSorts([ + ...localSorts, + { table: currentTableName, column: columnName as string, ascending: true }, + ]) + } + } + + // Remove a sort by column name + const onDeleteSort = useCallback((column: string) => { + setLocalSorts((currentSorts) => currentSorts.filter((sort) => sort.column !== column)) + }, []) + + // Toggle ascending/descending for a column + const onToggleSort = useCallback((column: string, ascending: boolean) => { + setLocalSorts((currentSorts) => { + const index = currentSorts.findIndex((x) => x.column === column) + if (index === -1) return currentSorts + const updatedSort = { ...currentSorts[index], ascending } + return [...currentSorts.slice(0, index), updatedSort, ...currentSorts.slice(index + 1)] + }) + }, []) + + // Handle drag-and-drop reordering + const onDragSort = useCallback((dragIndex: number, hoverIndex: number) => { + setLocalSorts((currentSort) => { + if ( + dragIndex < 0 || + dragIndex >= currentSort.length || + hoverIndex < 0 || + hoverIndex >= currentSort.length + ) { + return currentSort + } + const itemToMove = currentSort[dragIndex] + const remainingItems = [ + ...currentSort.slice(0, dragIndex), + ...currentSort.slice(dragIndex + 1), + ] + return [ + ...remainingItems.slice(0, hoverIndex), + itemToMove, + ...remainingItems.slice(hoverIndex), + ] + }) + }, []) + + // Fix: Compare for meaningful changes (only column order and ascending) + const hasChanges = useMemo(() => { + if (localSorts.length !== sorts.length) return true + + // Compare each sort by relevant properties + return localSorts.some((localSort, index) => { + const propSort = sorts[index] + return ( + !propSort || + localSort.column !== propSort.column || + localSort.ascending !== propSort.ascending + ) + }) + }, [localSorts, sorts]) + + // Apply the sorts to the parent component + const onSelectApplySorts = () => { + // Mark that we're applying our changes to prevent re-syncing + isApplyingRef.current = true + + // Update our last sorts ref to the current local state + lastSortsRef.current = [...localSorts] + + // Create deep copies to avoid reference issues + const sortsCopy = localSorts.map((sort) => ({ ...sort })) + + // Apply the sorts + onApplySorts(sortsCopy) + } + + // Generate stable keys for SortRow components to avoid reconciliation issues + const getSortRowKey = (sort: Sort, index: number) => { + return `sort-${sort.table}-${sort.column}-${index}` + } + + const content = ( +
+ {localSorts.map((sort, index) => ( + + ))} + {localSorts.length === 0 && ( +
+
No sorts applied to this view
+

Add a column below to sort the view

+
+ )} + + +
+ {dropdownOptions && dropdownOptions.length > 0 ? ( + + + + ) : ( +

All columns have been added

+ )} +
+ +
+
+
+ ) + + return ( + + + + + + {content} + + + ) +} diff --git a/apps/studio/components/grid/components/header/sort/index.ts b/apps/studio/components/grid/components/header/sort/index.ts deleted file mode 100644 index 8f4094016a6..00000000000 --- a/apps/studio/components/grid/components/header/sort/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as SortPopover } from './SortPopover' diff --git a/apps/studio/components/grid/hooks/README.md b/apps/studio/components/grid/hooks/README.md new file mode 100644 index 00000000000..559e08b58dc --- /dev/null +++ b/apps/studio/components/grid/hooks/README.md @@ -0,0 +1,101 @@ +# Table Filtering and Sorting Developer notes + +## Overview + +The table filtering and sorting system uses a URL-based state persistence pattern combined with custom hooks that abstract the implementation details from consuming components. This architecture provides several benefits: + +- **Persistent state**: Filters and sorts are stored in URL parameters, enabling bookmarking and sharing +- **Separation of concerns**: Logic is separated from UI components +- **Draft-then-apply pattern**: UI components maintain draft state until explicitly applied + +## Core Hooks + +### `useTableFilter` + +```typescript +// Returns: { filters, urlFilters, onApplyFilters } +``` + +The `useTableFilter` hook manages filter state with these responsibilities: + +- Retrieves raw filter parameters from URL +- Formats filter parameters into usable Filter[] objects +- Provides a callback to apply new filters +- Persists changes to URL parameters +- Triggers side effects through `saveFiltersAndTriggerSideEffects` + +Key design aspects: + +- No direct snapshot interaction, keeping it focused solely on filter management +- Uses URL parameters as the source of truth +- Forwards filter changes to URL and triggers application-specific side effects + +### `useTableSort` + +```typescript +// Returns: { sorts, urlSorts, onApplySorts } +``` + +The `useTableSort` hook manages sort state with these responsibilities: + +- Retrieves raw filter parameters from URL +- Formats sort parameters into usable Sort[] objects (needs table name) +- Provides a callback to apply new sorts +- Persists changes to URL parameters +- Triggers side effects through `saveSortsAndTriggerSideEffects` + +Key design aspects: + +- Handles applying table name to sort objects +- Maintains URL parameters as source of truth +- Forwards sort changes to URL and triggers application-specific side effects + +## Component Implementation + +### FilterPopoverPrimitive and SortPopoverPrimitive + +These components follow a "draft and apply" pattern: + +1. **Local state management**: Both components maintain a local state copy of filters/sorts +2. **Edit operations**: Changes like adding, modifying, or deleting are made to the local state +3. **Apply operations**: Only when the user clicks "Apply" are the changes committed via the callback +4. **Synchronization**: Local state is synchronized with props when external changes occur + +## Data Flow + +1. URL parameters store the raw filter/sort state +2. Hooks read and format these parameters into usable objects +3. UI components receive formatted objects and callbacks +4. Components maintain draft state for editing +5. When "Apply" is clicked, callbacks update URL parameters +6. Side effects are triggered via dedicated save hooks + +## Component Usage + +Components using these hooks should follow this pattern: + +```tsx +function TableComponent() { + // Get filter data and callbacks + const { filters, onApplyFilters } = useTableFilter() + + // Get sort data and callbacks + const { sorts, onApplySorts } = useTableSort() + + return ( + <> + + + + + {/* Table rendering with filters and sorts applied */} + + ) +} +``` + +## Implementation Notes + +- Filter and sort parameters are stored in URL using specific formats +- Conversion utilities (`formatFilterURLParams`, `formatSortURLParams`, etc.) handle translation between URL strings and typed objects +- Side effect hooks manage database persistence and related operations diff --git a/apps/studio/components/grid/hooks/useSaveTableEditorState.ts b/apps/studio/components/grid/hooks/useSaveTableEditorState.ts new file mode 100644 index 00000000000..9157100956c --- /dev/null +++ b/apps/studio/components/grid/hooks/useSaveTableEditorState.ts @@ -0,0 +1,58 @@ +import { useCallback } from 'react' + +import { saveTableEditorStateToLocalStorage } from 'components/grid/SupabaseGrid.utils' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' + +/** + * Hook for saving state and triggering side effects. + */ +export function useSaveTableEditorState() { + const { project } = useProjectContext() + const snap = useTableEditorTableStateSnapshot() + + const saveDataAndTriggerSideEffects = useCallback( + (dataToSave: { filters?: string[]; sorts?: string[] }) => { + const projectRef = project?.ref + + if (!projectRef) { + return console.warn( + '[useSaveTableEditorState] ProjectRef missing, cannot save or trigger side effects.' + ) + } + + try { + snap.setPage(1) + snap.setEnforceExactCount(false) + + const tableName = snap.table?.name + const schema = snap.table?.schema + + if (tableName) { + saveTableEditorStateToLocalStorage({ + projectRef, + tableName, + schema, + ...dataToSave, + }) + } else { + console.warn('[useSaveTableEditorState] Table name missing, skipping localStorage save.') + } + } catch (error) { + console.error('[useSaveTableEditorState] Error during interaction with snapshot:', error) + } + }, + [snap, project] + ) + + const saveFiltersAndTriggerSideEffects = useCallback( + (urlFilters: string[]) => saveDataAndTriggerSideEffects({ filters: urlFilters }), + [saveDataAndTriggerSideEffects] + ) + const saveSortsAndTriggerSideEffects = useCallback( + (urlSorts: string[]) => saveDataAndTriggerSideEffects({ sorts: urlSorts }), + [saveDataAndTriggerSideEffects] + ) + + return { saveFiltersAndTriggerSideEffects, saveSortsAndTriggerSideEffects } +} diff --git a/apps/studio/components/grid/hooks/useTableFilter.ts b/apps/studio/components/grid/hooks/useTableFilter.ts new file mode 100644 index 00000000000..0181bbce9ca --- /dev/null +++ b/apps/studio/components/grid/hooks/useTableFilter.ts @@ -0,0 +1,32 @@ +import { useCallback } from 'react' + +import { filtersToUrlParams, formatFilterURLParams } from 'components/grid/SupabaseGrid.utils' +import type { Filter } from 'components/grid/types' +import { useTableEditorFiltersSort } from 'hooks/misc/useTableEditorFiltersSort' +import { useSaveTableEditorState } from './useSaveTableEditorState' + +/** + * Hook for managing table filter URL parameters and saving. + * NO direct snapshot interaction. + */ +export function useTableFilter() { + const { filters: urlFilters, setParams } = useTableEditorFiltersSort() + const { saveFiltersAndTriggerSideEffects } = useSaveTableEditorState() + + const filters = formatFilterURLParams(urlFilters) + + const onApplyFilters = useCallback( + (appliedFilters: Filter[]) => { + const newUrlFilters = filtersToUrlParams(appliedFilters) + setParams((prevParams) => ({ ...prevParams, filter: newUrlFilters })) + saveFiltersAndTriggerSideEffects(newUrlFilters) + }, + [setParams, saveFiltersAndTriggerSideEffects] + ) + + return { + filters, // Formatted Filter[] object array + urlFilters, // Raw string[] from URL + onApplyFilters, // Callback to apply changes + } +} diff --git a/apps/studio/components/grid/hooks/useTableSort.ts b/apps/studio/components/grid/hooks/useTableSort.ts new file mode 100644 index 00000000000..9cbd1c70a7a --- /dev/null +++ b/apps/studio/components/grid/hooks/useTableSort.ts @@ -0,0 +1,50 @@ +import { useCallback, useMemo } from 'react' + +import { formatSortURLParams, sortsToUrlParams } from 'components/grid/SupabaseGrid.utils' +import type { Sort } from 'components/grid/types' +import { useTableEditorFiltersSort } from 'hooks/misc/useTableEditorFiltersSort' +import { useTableEditorTableStateSnapshot } from 'state/table-editor-table' +import { useSaveTableEditorState } from './useSaveTableEditorState' + +/** + * Hook for managing table sort URL parameters and saving. + * Uses snapshot ONLY to get table name for formatting/mapping. + * Uses useSaveTableEditorState for saving and side effects. + * Does NOT format initial sorts (needs table name externally). + * Does NOT interact with snapshot directly. + */ +export function useTableSort() { + const { sorts: urlSorts, setParams } = useTableEditorFiltersSort() + const snap = useTableEditorTableStateSnapshot() + const { saveSortsAndTriggerSideEffects } = useSaveTableEditorState() + + const tableName = useMemo(() => snap.table?.name || '', [snap]) + + const sorts = useMemo(() => { + return formatSortURLParams(tableName, urlSorts) + }, [tableName, urlSorts]) + + const onApplySorts = useCallback( + (appliedSorts: Sort[]) => { + if (!tableName) { + return console.warn( + '[useTableSort] Table name missing in callback, cannot apply sort correctly.' + ) + } + + const sortsWithTable = appliedSorts.map((sort) => ({ ...sort, table: tableName })) + const newUrlSorts = sortsToUrlParams(sortsWithTable) + + setParams((prevParams) => ({ ...prevParams, sort: newUrlSorts })) + + saveSortsAndTriggerSideEffects(newUrlSorts) + }, + [snap, setParams, saveSortsAndTriggerSideEffects] + ) + + return { + sorts, + urlSorts, + onApplySorts, + } +} diff --git a/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx b/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx index 62bb1fd0627..361f79ea9fe 100644 --- a/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/DeleteConfirmationDialogs.tsx @@ -1,106 +1,50 @@ -import type { PostgresTable } from '@supabase/postgres-meta' import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { toast } from 'sonner' -import { - formatFilterURLParams, - saveTableEditorStateToLocalStorageDebounced, -} from 'components/grid/SupabaseGrid.utils' +import { useTableFilter } from 'components/grid/hooks/useTableFilter' import type { SupaRow } from 'components/grid/types' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useDatabaseColumnDeleteMutation } from 'data/database-columns/database-column-delete-mutation' -import { Entity } from 'data/table-editor/table-editor-types' +import { TableLike } from 'data/table-editor/table-editor-types' import { useTableRowDeleteAllMutation } from 'data/table-rows/table-row-delete-all-mutation' import { useTableRowDeleteMutation } from 'data/table-rows/table-row-delete-mutation' import { useTableRowTruncateMutation } from 'data/table-rows/table-row-truncate-mutation' import { useTableDeleteMutation } from 'data/tables/table-delete-mutation' -import { TablesData, useGetTables } from 'data/tables/tables-query' -import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' -import { noop } from 'lib/void' import { useGetImpersonatedRoleState } from 'state/role-impersonation-state' import { useTableEditorStateSnapshot } from 'state/table-editor' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, Checkbox } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import { useTableEditorFiltersSort } from 'hooks/misc/useTableEditorFiltersSort' export type DeleteConfirmationDialogsProps = { - selectedTable?: Entity | PostgresTable - onAfterDeleteTable?: (tables: TablesData) => void + selectedTable?: TableLike + onTableDeleted?: () => void } const DeleteConfirmationDialogs = ({ selectedTable, - onAfterDeleteTable = noop, + onTableDeleted, }: DeleteConfirmationDialogsProps) => { const { project } = useProjectContext() const snap = useTableEditorStateSnapshot() - const { selectedSchema } = useQuerySchemaState() - - const { filters: filter, setParams } = useTableEditorFiltersSort() - const filters = formatFilterURLParams(filter as string[]) - - const getTables = useGetTables({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) + const { filters, onApplyFilters } = useTableFilter() const removeDeletedColumnFromFiltersAndSorts = ({ - ref, - tableName, - schema, columnName, }: { - ref: string - tableName: string - schema: string + ref?: string + tableName?: string + schema?: string columnName: string }) => { - setParams((prevParams) => { - const existingFilters = (prevParams?.filter ?? []) as string[] - const existingSorts = (prevParams?.sort ?? []) as string[] - - const newFiltersAndSorts = { - filter: existingFilters.filter((filter: string) => { - const [column] = filter.split(':') - if (column !== columnName) return filter - }), - sort: existingSorts.filter((sort: string) => { - const [column] = sort.split(':') - if (column !== columnName) return sort - }), - } - - // Overwrite local storage without the deleted column - saveTableEditorStateToLocalStorageDebounced({ - projectRef: ref, - tableName, - schema, - filters: newFiltersAndSorts.filter, - sorts: newFiltersAndSorts.sort, - }) - - return { - ...prevParams, - ...newFiltersAndSorts, - } - }) + onApplyFilters(filters.filter((filter) => filter.column !== columnName)) } const { mutate: deleteColumn } = useDatabaseColumnDeleteMutation({ onSuccess: () => { if (!(snap.confirmationDialog?.type === 'column')) return const selectedColumnToDelete = snap.confirmationDialog.column - if (!project?.ref) return - if (!selectedTable?.name) return - - removeDeletedColumnFromFiltersAndSorts({ - ref: project?.ref, - tableName: selectedTable?.name, - schema: selectedColumnToDelete.schema, - columnName: selectedColumnToDelete.name, - }) - + removeDeletedColumnFromFiltersAndSorts({ columnName: selectedColumnToDelete.name }) toast.success(`Successfully deleted column "${selectedColumnToDelete.name}"`) }, onError: (error) => { @@ -114,9 +58,8 @@ const DeleteConfirmationDialogs = ({ }) const { mutate: deleteTable } = useTableDeleteMutation({ onSuccess: async () => { - const tables = await getTables(selectedSchema) - onAfterDeleteTable(tables) toast.success(`Successfully deleted table "${selectedTable?.name}"`) + onTableDeleted?.() }, onError: (error) => { toast.error(`Failed to delete ${selectedTable?.name}: ${error.message}`) @@ -231,13 +174,13 @@ const DeleteConfirmationDialogs = ({ truncateRows({ projectRef: project.ref, connectionString: project.connectionString, - table: selectedTable as any, + table: selectedTable, }) } else { deleteAllRows({ projectRef: project.ref, connectionString: project.connectionString, - table: selectedTable as any, + table: selectedTable, filters, roleImpersonationState: getImpersonatedRoleState(), }) @@ -246,7 +189,7 @@ const DeleteConfirmationDialogs = ({ deleteRows({ projectRef: project.ref, connectionString: project.connectionString, - table: selectedTable as any, + table: selectedTable, rows: selectedRowsToDelete as SupaRow[], roleImpersonationState: getImpersonatedRoleState(), }) 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 221e6bfa353..92eab845314 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx @@ -4,15 +4,9 @@ import { DndProvider } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' import { useParams } from 'common' -import { - filtersToUrlParams, - formatFilterURLParams, - formatSortURLParams, - sortsToUrlParams, -} from 'components/grid/SupabaseGrid.utils' import RefreshButton from 'components/grid/components/header/RefreshButton' -import FilterPopover from 'components/grid/components/header/filter/FilterPopover' -import { SortPopover } from 'components/grid/components/header/sort' +import { FilterPopoverPrimitive } from 'components/grid/components/header/filter/FilterPopoverPrimitive' +import { SortPopoverPrimitive } from 'components/grid/components/header/sort/SortPopoverPrimitive' import type { Filter } from 'components/grid/types' import { Sort } from 'components/grid/types' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -65,9 +59,9 @@ const ForeignRowSelector = ({ id: tableId, }) - const [{ sort: sorts, filter: filters }, setParams] = useState<{ - filter: string[] - sort: string[] + const [{ sort: sorts, filter: filters }, setFiltersAndSorts] = useState<{ + filter: Filter[] + sort: Sort[] }>({ filter: [], sort: [] }) const onApplyFilters = (appliedFilters: Filter[]) => { @@ -76,19 +70,19 @@ const ForeignRowSelector = ({ setPage(1) } - setParams((prevParams) => { + setFiltersAndSorts((prevParams) => { return { ...prevParams, - filter: filtersToUrlParams(appliedFilters), + filter: appliedFilters, } }) } const onApplySorts = (appliedSorts: Sort[]) => { - setParams((prevParams) => { + setFiltersAndSorts((prevParams) => { return { ...prevParams, - sort: sortsToUrlParams(appliedSorts), + sort: appliedSorts, } }) } @@ -103,8 +97,8 @@ const ForeignRowSelector = ({ projectRef: project?.ref, connectionString: project?.connectionString, tableId: table?.id, - sorts: formatSortURLParams(table?.name || '', sorts), - filters: formatFilterURLParams(filters), + sorts, + filters, page, limit: rowsPerPage, roleImpersonationState: roleImpersonationState as RoleImpersonationState, @@ -160,13 +154,17 @@ const ForeignRowSelector = ({
- - +
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx index 5b9799e38ea..af57fb9decb 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx @@ -764,6 +764,7 @@ export const updateTable = async ({ queryClient.invalidateQueries(databaseKeys.foreignKeyConstraints(projectRef, table.schema)), queryClient.invalidateQueries(databaseKeys.tableDefinition(projectRef, table.id)), queryClient.invalidateQueries(entityTypeKeys.list(projectRef)), + queryClient.invalidateQueries(tableKeys.list(projectRef, table.schema, true)), ]) // We need to invalidate tableRowsAndCount after tableEditor diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx index df3d071c0cb..2ad8c0c54ca 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx @@ -13,16 +13,19 @@ import { isTableLike, isView, } from 'data/table-editor/table-editor-types' +import { useGetTables } from 'data/tables/tables-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { useUrlState } from 'hooks/ui/useUrlState' import { PROTECTED_SCHEMAS } from 'lib/constants/schemas' import { useAppStateSnapshot } from 'state/app-state' import { TableEditorTableStateContextProvider } from 'state/table-editor-table' -import { handleTabClose, makeActiveTabPermanent, useTabsStore } from 'state/tabs' +import { createTabId, handleTabClose, makeActiveTabPermanent, useTabsStore } from 'state/tabs' import { Button } from 'ui' import { Admonition, GenericSkeletonLoader } from 'ui-patterns' import { useIsTableEditorTabsEnabled } from '../App/FeaturePreview/FeaturePreviewContext' +import DeleteConfirmationDialogs from './DeleteConfirmationDialogs' import SidePanelEditor from './SidePanelEditor/SidePanelEditor' import TableDefinition from './TableDefinition' @@ -40,8 +43,10 @@ export const TableGridEditor = ({ const project = useSelectedProject() const appSnap = useAppStateSnapshot() const { ref: projectRef, id } = useParams() + const tabs = useTabsStore(projectRef) const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled() + const { selectedSchema } = useQuerySchemaState() useLoadTableEditorStateFromLocalStorageIntoUrl({ projectRef, @@ -55,6 +60,17 @@ export const TableGridEditor = ({ const isReadOnly = !canEditTables && !canEditColumns const tabId = !!id ? tabs.openTabs.find((x) => x.endsWith(id)) : undefined + const getTables = useGetTables({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const onClearDashboardHistory = () => { + if (projectRef && editor) { + appSnap.setDashboardHistory(projectRef, editor === 'table' ? 'editor' : editor, undefined) + } + } + const onTableCreated = useCallback( (table: { id: number }) => { router.push(`/project/${projectRef}/editor/${table.id}`) @@ -62,11 +78,21 @@ export const TableGridEditor = ({ [projectRef, router] ) - const onClearDashboardHistory = () => { - if (projectRef && editor) { - appSnap.setDashboardHistory(projectRef, editor === 'table' ? 'editor' : editor, undefined) + const onTableDeleted = useCallback(async () => { + // For simplicity for now, we just open the first table within the same schema + if (isTableEditorTabsEnabled && selectedTable) { + // Close tab + const tabId = createTabId(selectedTable.entity_type, { id: selectedTable.id }) + handleTabClose({ ref: projectRef, id: tabId, router, editor, onClearDashboardHistory }) + } else { + const tables = await getTables(selectedSchema) + if (tables.length > 0) { + router.push(`/project/${projectRef}/editor/${tables[0].id}`) + } else { + router.push(`/project/${projectRef}/editor`) + } } - } + }, [getTables, isTableEditorTabsEnabled, projectRef, router, selectedSchema]) // NOTE: DO NOT PUT HOOKS AFTER THIS LINE if (isLoadingSelectedTable || !projectRef) { @@ -161,6 +187,10 @@ export const TableGridEditor = ({ selectedTable={isTableLike(selectedTable) ? selectedTable : undefined} onTableCreated={onTableCreated} /> +
) diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index df73b7f6ade..3c4ea8c51bb 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -46,11 +46,11 @@ import { TableMenuEmptyState } from './TableMenuEmptyState' const TableEditorMenu = () => { const { ref, id: _id } = useParams() - const isMobile = useBreakpoint() const id = _id ? Number(_id) : undefined const snap = useTableEditorStateSnapshot() - const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled() const { selectedSchema, setSelectedSchema } = useQuerySchemaState() + const isTableEditorTabsEnabled = useIsTableEditorTabsEnabled() + const isMobile = useBreakpoint() const [showModal, setShowModal] = useState(false) const [searchText, setSearchText] = useState('') diff --git a/apps/studio/data/sql/execute-sql-mutation.ts b/apps/studio/data/sql/execute-sql-mutation.ts index 01df31c8bc1..b5ddceaf427 100644 --- a/apps/studio/data/sql/execute-sql-mutation.ts +++ b/apps/studio/data/sql/execute-sql-mutation.ts @@ -1,16 +1,13 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { databaseExtensionsKeys } from 'data/database-extensions/keys' -import { databasePoliciesKeys } from 'data/database-policies/keys' -import { databaseRoleKeys } from 'data/database-roles/keys' -import { databaseTriggerKeys } from 'data/database-triggers/keys' -import { databaseKeys } from 'data/database/keys' -import { entityTypeKeys } from 'data/entity-types/keys' -import { enumeratedTypesKeys } from 'data/enumerated-types/keys' -import { tableKeys } from 'data/tables/keys' import { executeSql, ExecuteSqlData, ExecuteSqlVariables } from './execute-sql-query' +// [Joshen] Intention is that we invalidate all database related keys whenever running a mutation related query +// So we attempt to ignore all the non-related query keys. We could probably look into grouping our query keys better +// actually to not make this too hacky here +const INVALIDATION_KEYS_IGNORE = ['branches', 'settings-v2', 'addons', 'custom-domains', 'content'] + export type QueryResponseError = { code: string message: string @@ -41,9 +38,16 @@ export const useExecuteSqlMutation = ({ const { contextualInvalidation, sql, projectRef } = variables // [Joshen] Default to false for now, only used for SQL editor to dynamically invalidate - if (contextualInvalidation && projectRef) { - const invalidationKeys = inferInvalidationKeys(projectRef, sql) - await Promise.all(invalidationKeys.map((key) => queryClient.invalidateQueries(key))) + const sqlLower = sql.toLowerCase() + const isMutationSQL = + sqlLower.includes('create ') || sqlLower.includes('alter ') || sqlLower.includes('drop ') + if (contextualInvalidation && projectRef && isMutationSQL) { + const databaseRelatedKeys = queryClient + .getQueryCache() + .findAll(['projects', projectRef]) + .map((x) => x.queryKey) + .filter((x) => !INVALIDATION_KEYS_IGNORE.some((a) => x.includes(a))) + await Promise.all(databaseRelatedKeys.map((key) => queryClient.invalidateQueries(key))) } await onSuccess?.(data, variables, context) }, @@ -58,68 +62,3 @@ export const useExecuteSqlMutation = ({ } ) } - -// [Joshen] Can expand this further eventually, but just covering certain easy cases for now -const inferInvalidationKeys = (ref: string, sql: string) => { - const keys = [] - const sqlLower = sql.toLowerCase() - - if ( - sqlLower.includes('create table') || - sqlLower.includes('alter table') || - sqlLower.includes('drop table') - ) { - keys.push(entityTypeKeys.list(ref)) - keys.push(tableKeys.list(ref)) - } - if ( - sqlLower.includes('create schema') || - sqlLower.includes('alter schema') || - sqlLower.includes('drop schema') - ) { - keys.push(databaseKeys.schemas(ref)) - } - if ( - sqlLower.includes('create function') || - sqlLower.includes('alter function') || - sqlLower.includes('drop function') - ) { - keys.push(databaseKeys.databaseFunctions(ref)) - } - if ( - sqlLower.includes('create trigger') || - sqlLower.includes('alter trigger') || - sqlLower.includes('drop trigger') - ) { - keys.push(databaseTriggerKeys.list(ref)) - } - if ( - sqlLower.includes('create policy') || - sqlLower.includes('alter policy') || - sqlLower.includes('drop policy') - ) { - keys.push(databasePoliciesKeys.list(ref)) - } - if ( - sqlLower.includes('create type') || - sqlLower.includes('alter type') || - sqlLower.includes('drop type') - ) { - keys.push(enumeratedTypesKeys.list(ref)) - } - if ( - sqlLower.includes('create role') || - sqlLower.includes('alter role') || - sqlLower.includes('drop role') - ) { - keys.push(databaseRoleKeys.databaseRoles(ref)) - } - if (sqlLower.includes('create index') || sqlLower.includes('drop index')) { - keys.push(databaseKeys.indexes(ref)) - } - if (sqlLower.includes('create extension') || sqlLower.includes('drop extension')) { - keys.push(databaseExtensionsKeys.list(ref)) - } - - return keys -} diff --git a/apps/studio/data/table-editor/keys.ts b/apps/studio/data/table-editor/keys.ts index 6ce556c9754..851eefd9838 100644 --- a/apps/studio/data/table-editor/keys.ts +++ b/apps/studio/data/table-editor/keys.ts @@ -1,4 +1,4 @@ export const tableEditorKeys = { - tableEditor: (projectRef: string | undefined, id: number | undefined) => - ['projects', projectRef, 'table-editor', id] as const, + tableEditor: (projectRef: string | undefined, id?: number) => + ['projects', projectRef, 'table-editor', id].filter(Boolean), } diff --git a/apps/studio/data/table-editor/table-editor-types.ts b/apps/studio/data/table-editor/table-editor-types.ts index 1014acac8cd..b793f43111d 100644 --- a/apps/studio/data/table-editor/table-editor-types.ts +++ b/apps/studio/data/table-editor/table-editor-types.ts @@ -45,6 +45,8 @@ export interface ForeignTable { export type Entity = Table | PartitionedTable | View | MaterializedView | ForeignTable +export type TableLike = Table | PartitionedTable + export function isTable(entity?: Entity): entity is Table { return entity?.entity_type === ENTITY_TYPE.TABLE } @@ -57,7 +59,7 @@ export function isPartitionedTable(entity?: Entity): entity is PartitionedTable * Returns true if the entity is a Table or a PartitionedTable. * Foreign tables are not considered table-like. */ -export function isTableLike(entity?: Entity): entity is Table | PartitionedTable { +export function isTableLike(entity?: Entity): entity is TableLike { return isTable(entity) || isPartitionedTable(entity) } @@ -79,3 +81,36 @@ export function isMaterializedView(entity?: Entity): entity is MaterializedView export function isViewLike(entity?: Entity): entity is View | MaterializedView { return isView(entity) || isMaterializedView(entity) } + +export function postgresTableToEntity(table: PostgresTable): Entity | undefined { + if (table.columns === undefined || table.relationships === undefined) { + console.error( + 'Unable to convert PostgresTable to Entity type: columns and relationships must not be undefined.' + ) + return undefined + } + + const tableRelationships: TableRelationship[] = table.relationships.map((rel) => ({ + deletion_action: 'a', + update_action: 'a', + ...rel, + })) + + return { + id: table.id, + schema: table.schema, + name: table.name, + comment: table.comment, + rls_enabled: table.rls_enabled, + rls_forced: table.rls_forced, + replica_identity: table.replica_identity, + bytes: table.bytes, + size: table.size, + live_rows_estimate: table.live_rows_estimate, + dead_rows_estimate: table.dead_rows_estimate, + columns: table.columns, + relationships: tableRelationships, + primary_keys: table.primary_keys, + entity_type: ENTITY_TYPE.TABLE, + } +} 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 0fc55394fb1..bfd96398af4 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 @@ -2,8 +2,9 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react import { toast } from 'sonner' import { Query } from '@supabase/pg-meta/src/query' -import type { Filter, SupaTable } from 'components/grid/types' +import type { Filter } from 'components/grid/types' import { executeSql } from 'data/sql/execute-sql-query' +import { Entity } from 'data/table-editor/table-editor-types' import { RoleImpersonationState, wrapWithRoleImpersonation } from 'lib/role-impersonation' import { isRoleImpersonationEnabled } from 'state/role-impersonation-state' import type { ResponseError } from 'types' @@ -13,7 +14,7 @@ import { formatFilterValue } from './utils' export type TableRowDeleteAllVariables = { projectRef: string connectionString?: string - table: SupaTable + table: Entity filters: Filter[] roleImpersonationState?: RoleImpersonationState } diff --git a/apps/studio/data/table-rows/table-rows-query.ts b/apps/studio/data/table-rows/table-rows-query.ts index 56825ec17d7..1241ab9e4cf 100644 --- a/apps/studio/data/table-rows/table-rows-query.ts +++ b/apps/studio/data/table-rows/table-rows-query.ts @@ -1,3 +1,5 @@ +import { Query } from '@supabase/pg-meta/src/query' +import { getTableRowsSql } from '@supabase/pg-meta/src/query/table-row-query' import { useQuery, useQueryClient, @@ -5,8 +7,6 @@ import { type UseQueryOptions, } from '@tanstack/react-query' -import { Query } from '@supabase/pg-meta/src/query' -import { getTableRowsSql } from '@supabase/pg-meta/src/query/table-row-query' import { IS_PLATFORM } from 'common' import { parseSupaTable } from 'components/grid/SupabaseGrid.utils' import { Filter, Sort, SupaRow, SupaTable } from 'components/grid/types' @@ -142,9 +142,9 @@ export const fetchAllTableRows = async ({ ) try { - const { result } = await executeWithRetry(async () => + const { result } = await executeWithRetry(async () => { executeSql({ projectRef, connectionString, sql: query }) - ) + }) rows.push(...result) progressCallback?.(rows.length) @@ -153,7 +153,7 @@ export const fetchAllTableRows = async ({ await sleep(THROTTLE_DELAY) } catch (error) { throw new Error( - `Error fetching table rows: ${error instanceof Error ? error.message : 'Unknown error'}` + `Error fetching all table rows: ${error instanceof Error ? error.message : 'Unknown error'}` ) } } @@ -202,23 +202,28 @@ export async function getTableRows( getTableRowsSql({ table: entity, filters, sorts, limit, page }), roleImpersonationState ) - const { result } = await executeSql( - { - projectRef, - connectionString, - sql, - queryKey: ['table-rows', table?.id], - isRoleImpersonationEnabled: isRoleImpersonationEnabled(roleImpersonationState?.role), - }, - signal - ) - const rows = result.map((x: any, index: number) => { - return { idx: index, ...x } - }) as SupaRow[] + try { + const { result } = await executeSql( + { + projectRef, + connectionString, + sql, + queryKey: ['table-rows', table?.id], + isRoleImpersonationEnabled: isRoleImpersonationEnabled(roleImpersonationState?.role), + }, + signal + ) - return { - rows, + const rows = result.map((x: any, index: number) => { + return { idx: index, ...x } + }) as SupaRow[] + + return { rows } + } catch (error) { + throw new Error( + `Error fetching table rows: ${error instanceof Error ? error.message : 'Unknown error'}` + ) } } diff --git a/apps/studio/data/table-rows/utils.ts b/apps/studio/data/table-rows/utils.ts index 1cd131be63a..bb6e78a811f 100644 --- a/apps/studio/data/table-rows/utils.ts +++ b/apps/studio/data/table-rows/utils.ts @@ -1,4 +1,4 @@ -import type { Filter, ServiceError, SupaTable } from 'components/grid/types' +import type { Filter, ServiceError } from 'components/grid/types' import { isNumericalColumn } from 'components/grid/utils/types' import { Entity, isTableLike } from 'data/table-editor/table-editor-types' @@ -6,7 +6,15 @@ import { Entity, isTableLike } from 'data/table-editor/table-editor-types' * temporary fix until we implement a better filter UI * which validate input value base on the column type */ -export function formatFilterValue(table: SupaTable, filter: Filter) { +export function formatFilterValue( + table: { + columns: { + name: string + format: string + }[] + }, + filter: Filter +) { const column = table.columns.find((x) => x.name == filter.column) if (column && isNumericalColumn(column.format)) { const numberValue = Number(filter.value) diff --git a/apps/studio/pages/project/[ref]/database/tables/[id].tsx b/apps/studio/pages/project/[ref]/database/tables/[id].tsx index c4f3da0e276..9c834807105 100644 --- a/apps/studio/pages/project/[ref]/database/tables/[id].tsx +++ b/apps/studio/pages/project/[ref]/database/tables/[id].tsx @@ -11,6 +11,7 @@ import { useTableEditorQuery } from 'data/table-editor/table-editor-query' import { isTableLike } from 'data/table-editor/table-editor-types' import { ChevronRight } from 'lucide-react' import { useTableEditorStateSnapshot } from 'state/table-editor' +import { TableEditorTableStateContextProvider } from 'state/table-editor-table' import type { NextPageWithLayout } from 'types' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' @@ -50,11 +51,16 @@ const DatabaseTables: NextPageWithLayout = () => { - - + {project?.ref !== undefined && selectedTable !== undefined && isTableLike(selectedTable) && ( + + + + + )} ) } diff --git a/apps/studio/pages/project/[ref]/database/tables/index.tsx b/apps/studio/pages/project/[ref]/database/tables/index.tsx index 512e29931cf..c56310f5d36 100644 --- a/apps/studio/pages/project/[ref]/database/tables/index.tsx +++ b/apps/studio/pages/project/[ref]/database/tables/index.tsx @@ -1,6 +1,7 @@ -import type { PostgresTable } from '@supabase/postgres-meta' import { useState } from 'react' +import { PostgresTable } from '@supabase/postgres-meta' +import { useParams } from 'common' import { TableList } from 'components/interfaces/Database' import { SidePanelEditor } from 'components/interfaces/TableGridEditor' import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs' @@ -8,12 +9,15 @@ import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import { FormHeader } from 'components/ui/Forms/FormHeader' +import { Entity, isTableLike, postgresTableToEntity } from 'data/table-editor/table-editor-types' import { useTableEditorStateSnapshot } from 'state/table-editor' +import { TableEditorTableStateContextProvider } from 'state/table-editor-table' import type { NextPageWithLayout } from 'types' const DatabaseTables: NextPageWithLayout = () => { + const { ref: projectRef } = useParams() const snap = useTableEditorStateSnapshot() - const [selectedTableToEdit, setSelectedTableToEdit] = useState() + const [selectedTableToEdit, setSelectedTableToEdit] = useState() return ( <> @@ -24,15 +28,15 @@ const DatabaseTables: NextPageWithLayout = () => { { - setSelectedTableToEdit(table) + setSelectedTableToEdit(postgresTableToEntity(table)) snap.onEditTable() }} onDeleteTable={(table) => { - setSelectedTableToEdit(table) + setSelectedTableToEdit(postgresTableToEntity(table)) snap.onDeleteTable() }} onDuplicateTable={(table) => { - setSelectedTableToEdit(table) + setSelectedTableToEdit(postgresTableToEntity(table)) snap.onDuplicateTable() }} /> @@ -40,8 +44,19 @@ const DatabaseTables: NextPageWithLayout = () => { - - + {projectRef !== undefined && + selectedTableToEdit !== undefined && + isTableLike(selectedTableToEdit) && ( + + + + )} + + ) } diff --git a/apps/studio/pages/project/[ref]/editor/[id].tsx b/apps/studio/pages/project/[ref]/editor/[id].tsx index c1f16f54964..da2077944bf 100644 --- a/apps/studio/pages/project/[ref]/editor/[id].tsx +++ b/apps/studio/pages/project/[ref]/editor/[id].tsx @@ -1,9 +1,7 @@ -import { useRouter } from 'next/router' -import { useCallback, useEffect } from 'react' +import { useEffect } from 'react' import { useParams } from 'common' import { useIsTableEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/DeleteConfirmationDialogs' import { TableGridEditor } from 'components/interfaces/TableGridEditor/TableGridEditor' import DefaultLayout from 'components/layouts/DefaultLayout' import { EditorBaseLayout } from 'components/layouts/editors/EditorBaseLayout' @@ -11,7 +9,6 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte import TableEditorLayout from 'components/layouts/TableEditorLayout/TableEditorLayout' import TableEditorMenu from 'components/layouts/TableEditorLayout/TableEditorMenu' import { useTableEditorQuery } from 'data/table-editor/table-editor-query' -import { TablesData } from 'data/tables/tables-query' import { addTab, createTabId, getTabsStore } from 'state/tabs' import type { NextPageWithLayout } from 'types' @@ -27,19 +24,6 @@ const TableEditorPage: NextPageWithLayout = () => { id, }) - const router = useRouter() - const onAfterDeleteTable = useCallback( - (tables: TablesData) => { - // For simplicity for now, we just open the first table within the same schema - if (tables.length > 0) { - router.push(`/project/${projectRef}/editor/${tables[0].id}`) - } else { - router.push(`/project/${projectRef}/editor`) - } - }, - [router, projectRef] - ) - /** * Effect: Creates or updates tab when table is loaded * Runs when: @@ -70,15 +54,7 @@ const TableEditorPage: NextPageWithLayout = () => { } }, [selectedTable, id, projectRef, isTableEditorTabsEnabled]) - return ( - <> - - - - ) + return } TableEditorPage.getLayout = (page) => ( diff --git a/apps/studio/pages/project/[ref]/editor/index.tsx b/apps/studio/pages/project/[ref]/editor/index.tsx index 2f3bebfa86f..e1a129c3808 100644 --- a/apps/studio/pages/project/[ref]/editor/index.tsx +++ b/apps/studio/pages/project/[ref]/editor/index.tsx @@ -25,8 +25,8 @@ const TableEditorPage: NextPageWithLayout = () => { } useEffect(() => { + const lastOpenedTab = appSnap.dashboardHistory.editor if (isTableEditorTabsEnabled) { - const lastOpenedTab = appSnap.dashboardHistory.editor const lastTabId = tabStore.openTabs.find((id) => editorEntityTypes.table.includes(tabStore.tabsMap[id]?.type) ) @@ -37,6 +37,8 @@ const TableEditorPage: NextPageWithLayout = () => { const lastTab = tabStore.tabsMap[lastTabId] if (lastTab) router.push(`/project/${projectRef}/editor/${lastTab.metadata?.tableId}`) } + } else if (lastOpenedTab) { + router.push(`/project/${projectRef}/editor/${appSnap.dashboardHistory.editor}`) } }, [isTableEditorTabsEnabled]) diff --git a/apps/studio/state/table-editor-table.tsx b/apps/studio/state/table-editor-table.tsx index f35aaccdaf3..60c133eecea 100644 --- a/apps/studio/state/table-editor-table.tsx +++ b/apps/studio/state/table-editor-table.tsx @@ -17,14 +17,15 @@ import { useTableEditorStateSnapshot } from './table-editor' export const createTableEditorTableState = ({ projectRef, table: originalTable, - editable, + editable = true, onAddColumn, onExpandJSONEditor, onExpandTextEditor, }: { projectRef: string table: Entity - editable: boolean + /** If set to true, render an additional "+" column to support adding a new column in the grid editor */ + editable?: boolean onAddColumn: () => void onExpandJSONEditor: (column: string, row: SupaRow) => void onExpandTextEditor: (column: string, row: SupaRow) => void diff --git a/apps/studio/state/table-editor.tsx b/apps/studio/state/table-editor.tsx index 6230e27716d..be940f4135e 100644 --- a/apps/studio/state/table-editor.tsx +++ b/apps/studio/state/table-editor.tsx @@ -1,7 +1,6 @@ import type { PostgresColumn } from '@supabase/postgres-meta' import { PropsWithChildren, createContext, useContext, useRef } from 'react' import { proxy, useSnapshot } from 'valtio' -import { proxySet } from 'valtio/utils' import type { SupaRow } from 'components/grid/types' import { ForeignKey } from 'components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.types' From 915af35334c0c27e5771a4bec52c855fa14243b7 Mon Sep 17 00:00:00 2001 From: Daniel Gabriel <56942108+revogabe@users.noreply.github.com> Date: Wed, 30 Apr 2025 03:52:23 -0300 Subject: [PATCH 55/86] fix(ui): enable portal for popover components (#35370) --- apps/studio/components/interfaces/Database/Tables/TableList.tsx | 2 +- apps/studio/components/ui/FilterPopover.tsx | 2 +- apps/studio/components/ui/SchemaSelector.tsx | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Tables/TableList.tsx b/apps/studio/components/interfaces/Database/Tables/TableList.tsx index 798720c3dcd..ab2bc61582b 100644 --- a/apps/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/apps/studio/components/interfaces/Database/Tables/TableList.tsx @@ -221,7 +221,7 @@ const TableList = ({ icon={} /> - +

Show entity types

diff --git a/apps/studio/components/ui/FilterPopover.tsx b/apps/studio/components/ui/FilterPopover.tsx index d3eed31644a..186d4c3c652 100644 --- a/apps/studio/components/ui/FilterPopover.tsx +++ b/apps/studio/components/ui/FilterPopover.tsx @@ -83,7 +83,7 @@ export const FilterPopover = >({
- +
{title ?? `Select ${name.toLowerCase()}`} diff --git a/apps/studio/components/ui/SchemaSelector.tsx b/apps/studio/components/ui/SchemaSelector.tsx index df05ba0ceba..a8103d0d931 100644 --- a/apps/studio/components/ui/SchemaSelector.tsx +++ b/apps/studio/components/ui/SchemaSelector.tsx @@ -126,6 +126,7 @@ const SchemaSelector = ({ className="p-0 min-w-[200px]" side="bottom" align="start" + portal={true} sameWidthAsTrigger > From 14562e9c8abe793bd428eba3a1da196bf03f3e41 Mon Sep 17 00:00:00 2001 From: angelico Date: Wed, 30 Apr 2025 15:29:48 +0800 Subject: [PATCH 56/86] chore: init useIsOrioleDbInAws (#35325) * chore: check for cloud provider in useIsOrioleDb * chore: introduce useIsOrioleDbInAwsNew instead * chore: consolidate to using useIsOrioleDb * chore: selectively include useIsOrioleDbInAwsNew in conditionals * fix: prettier * fix: incorrect cloud provider * fix: update conditional instead * fix: prettier * Simplify logic * Smol fix --------- Co-authored-by: Joshen Lim --- .../PostgresVersionSelector.tsx | 2 +- .../interfaces/Settings/Addons/Addons.tsx | 5 ++-- .../Infrastructure/InfrastructureInfo.tsx | 25 +++++++++---------- apps/studio/data/database/backups-query.ts | 9 ++++--- apps/studio/hooks/misc/useSelectedProject.ts | 11 +++++++- .../project/[ref]/database/backups/pitr.tsx | 8 +++--- .../[ref]/database/backups/scheduled.tsx | 8 +++--- 7 files changed, 40 insertions(+), 28 deletions(-) diff --git a/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx index dd5bb1a399c..cfdcb09cfee 100644 --- a/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/PostgresVersionSelector.tsx @@ -76,7 +76,7 @@ export const PostgresVersionSelector = ({ const defaultValue = gaVersion ? formatValue(gaVersion) : formatValue(availableVersions[0]) form.setValue('postgresVersionSelection', defaultValue) } - }, [isSuccess, form]) + }, [isSuccess, availableVersions, form]) return ( diff --git a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx index 16cc9ddf870..9d426018192 100644 --- a/apps/studio/components/interfaces/Settings/Addons/Addons.tsx +++ b/apps/studio/components/interfaces/Settings/Addons/Addons.tsx @@ -32,7 +32,7 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import type { ProjectAddonVariantMeta } from 'data/subscriptions/types' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { useIsOrioleDb, useProjectByRef } from 'hooks/misc/useSelectedProject' +import { useIsOrioleDb, useIsOrioleDbInAws, useProjectByRef } from 'hooks/misc/useSelectedProject' import { useFlag } from 'hooks/ui/useFlag' import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils' import { BASE_PATH, INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS } from 'lib/constants' @@ -54,6 +54,7 @@ const Addons = () => { const isBranch = parentProject !== undefined const isProjectActive = useIsProjectActive() const isOrioleDb = useIsOrioleDb() + const isOrioleDbInAws = useIsOrioleDbInAws() const { data: settings } = useProjectSettingsV2Query({ projectRef }) const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: selectedOrg?.slug }) @@ -486,7 +487,7 @@ const Addons = () => { - ) : isOrioleDb ? ( + ) : isOrioleDbInAws ? ( { current_app_version_release_channel && current_app_version_release_channel !== 'ga' ? current_app_version_release_channel : undefined - const isOrioleDb = (current_app_version ?? '').includes('orioledb') + const isOrioleDb = useIsOrioleDb() const latestPgVersion = (latest_app_version ?? '').split('supabase-postgres-')[1] const isInactive = project?.status === 'INACTIVE' @@ -147,18 +148,16 @@ const InfrastructureInfo = () => { ), isOrioleDb && ( - <> - - - - OrioleDB - - - - This project uses OrioleDB - - - + + + + OrioleDB + + + + This project uses OrioleDB + + ), isOnLatestVersion && ( diff --git a/apps/studio/data/database/backups-query.ts b/apps/studio/data/database/backups-query.ts index 626ffef968d..7cef5a9462a 100644 --- a/apps/studio/data/database/backups-query.ts +++ b/apps/studio/data/database/backups-query.ts @@ -2,7 +2,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import type { components } from 'data/api' import { get, handleError } from 'data/fetchers' -import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' +import { useIsOrioleDbInAws } from 'hooks/misc/useSelectedProject' import type { ResponseError } from 'types' import { databaseKeys } from './keys' @@ -32,11 +32,14 @@ export const useBackupsQuery = ( { enabled = true, ...options }: UseQueryOptions = {} ) => { // [Joshen] Check for specifically false to account for project not loaded yet - const isOrioleDb = useIsOrioleDb() + const isOrioleDbInAws = useIsOrioleDbInAws() return useQuery( databaseKeys.backups(projectRef), ({ signal }) => getBackups({ projectRef }, signal), - { enabled: enabled && isOrioleDb === false && typeof projectRef !== 'undefined', ...options } + { + enabled: enabled && !isOrioleDbInAws && typeof projectRef !== 'undefined', + ...options, + } ) } diff --git a/apps/studio/hooks/misc/useSelectedProject.ts b/apps/studio/hooks/misc/useSelectedProject.ts index 7fb9b5d60e8..c973f22e083 100644 --- a/apps/studio/hooks/misc/useSelectedProject.ts +++ b/apps/studio/hooks/misc/useSelectedProject.ts @@ -1,7 +1,9 @@ +import { useMemo } from 'react' + import { useIsLoggedIn, useParams } from 'common' import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { ProjectInfo, useProjectsQuery } from 'data/projects/projects-query' -import { useMemo } from 'react' +import { PROVIDERS } from 'lib/constants' export function useSelectedProject({ enabled = true } = {}) { const { ref } = useParams() @@ -37,3 +39,10 @@ export const useIsOrioleDb = () => { const isOrioleDb = project?.dbVersion?.endsWith('orioledb') return isOrioleDb } + +export const useIsOrioleDbInAws = () => { + const project = useSelectedProject() + const isOrioleDbInAws = + project?.dbVersion?.endsWith('orioledb') && project?.cloud_provider === PROVIDERS.AWS.id + return isOrioleDbInAws +} diff --git a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx index 166c05189bb..275ae658474 100644 --- a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -5,6 +5,7 @@ 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' +import DefaultLayout from 'components/layouts/DefaultLayout' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' @@ -16,12 +17,11 @@ import UpgradeToPro from 'components/ui/UpgradeToPro' import { useBackupsQuery } from 'data/database/backups-query' import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { useIsOrioleDb } from 'hooks/misc/useSelectedProject' +import { useIsOrioleDbInAws } from 'hooks/misc/useSelectedProject' import { PROJECT_STATUS } from 'lib/constants' import type { NextPageWithLayout } from 'types' import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_ } from 'ui' import { Admonition } from 'ui-patterns' -import DefaultLayout from 'components/layouts/DefaultLayout' const DatabasePhysicalBackups: NextPageWithLayout = () => { return ( @@ -51,7 +51,7 @@ const PITR = () => { const { ref: projectRef } = useParams() const { project } = useProjectContext() const organization = useSelectedOrganization() - const isOrioleDb = useIsOrioleDb() + const isOrioleDbInAws = useIsOrioleDbInAws() const { data: backups, error, isLoading, isError, isSuccess } = useBackupsQuery({ projectRef }) const plan = organization?.plan?.id @@ -65,7 +65,7 @@ const PITR = () => { return } - if (isOrioleDb) { + if (isOrioleDbInAws) { return ( { const { ref: projectRef } = useParams() const { data: backups, error, isLoading, isError, isSuccess } = useBackupsQuery({ projectRef }) - const isOrioleDb = useIsOrioleDb() + const isOrioleDbInAws = useIsOrioleDbInAws() const isPitrEnabled = backups?.pitr_enabled const isPermissionsLoaded = usePermissionsLoaded() @@ -39,7 +39,7 @@ const DatabaseScheduledBackups: NextPageWithLayout = () => { - {isOrioleDb ? ( + {isOrioleDbInAws ? ( Date: Wed, 30 Apr 2025 21:24:22 +1200 Subject: [PATCH 57/86] chore: add Eduardo to humans.txt (#35365) --- apps/docs/public/humans.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index ef470c4a57e..d69b658b8c8 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -32,6 +32,7 @@ David A. Ventimiglia Deji I Div Arora Divit D +Eduardo Gurgel Egor Romanov Eleftheria Trivyzaki Etienne Stalmans From 4a5e1a8bd4e6931e2b1590309d3863426ce84c52 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 30 Apr 2025 17:47:47 +0800 Subject: [PATCH 58/86] Update query block bar hover behaviour (#35374) * Update query block bar hover behaviour * Smol fix --- .../components/ui/Charts/ComposedChart.tsx | 20 +++++------ .../ui/Charts/ComposedChart.utils.tsx | 7 ++-- .../components/ui/QueryBlock/QueryBlock.tsx | 35 ++++++++++++++----- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/apps/studio/components/ui/Charts/ComposedChart.tsx b/apps/studio/components/ui/Charts/ComposedChart.tsx index 7ebfa5db0f6..1e6c62bbf1f 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.tsx @@ -1,21 +1,21 @@ 'use client' -import { useState, ComponentProps, useEffect } from 'react' import dayjs from 'dayjs' +import { useTheme } from 'next-themes' +import { ComponentProps, useEffect, useState } from 'react' import { Area, Bar, - ComposedChart as RechartComposedChart, - XAxis, - YAxis, CartesianGrid, Line, + ComposedChart as RechartComposedChart, ReferenceArea, Tooltip, + XAxis, + YAxis, } from 'recharts' import { CategoricalChartState } from 'recharts/types/chart/types' import { cn } from 'ui' -import { useTheme } from 'next-themes' import ChartHeader from './ChartHeader' import ChartHighlightActions from './ChartHighlightActions' import { @@ -24,17 +24,17 @@ import { STACKED_CHART_COLORS, updateStackedChartColors, } from './Charts.constants' -import { Datum, CommonChartProps } from './Charts.types' -import { useChartSize, numberFormatter } from './Charts.utils' -import { ChartHighlight } from './useChartHighlight' -import NoDataPlaceholder from './NoDataPlaceholder' -import { MultiAttribute } from './ComposedChartHandler' +import { CommonChartProps, Datum } from './Charts.types' +import { numberFormatter, useChartSize } from './Charts.utils' import { calculateTotalChartAggregate, CustomLabel, CustomTooltip, formatBytes, } from './ComposedChart.utils' +import { MultiAttribute } from './ComposedChartHandler' +import NoDataPlaceholder from './NoDataPlaceholder' +import { ChartHighlight } from './useChartHighlight' export interface BarChartProps extends CommonChartProps { attributes: MultiAttribute[] diff --git a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx index 3a6e83663bc..bacb440bf9b 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.utils.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.utils.tsx @@ -1,12 +1,11 @@ 'use client' -import { useState } from 'react' import dayjs from 'dayjs' +import { useState } from 'react' +import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' import { DateTimeFormats } from './Charts.constants' import { numberFormatter } from './Charts.utils' import { MultiAttribute } from './ComposedChartHandler' -import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' -import { cn } from 'ui' interface CustomIconProps { color: string @@ -232,4 +231,4 @@ const CustomLabel = ({ payload, attributes, showMaxValue, onLabelHover }: Custom ) } -export { CustomTooltip, CustomLabel } +export { CustomLabel, CustomTooltip } diff --git a/apps/studio/components/ui/QueryBlock/QueryBlock.tsx b/apps/studio/components/ui/QueryBlock/QueryBlock.tsx index 9424b39c1cc..b2d5fa639c4 100644 --- a/apps/studio/components/ui/QueryBlock/QueryBlock.tsx +++ b/apps/studio/components/ui/QueryBlock/QueryBlock.tsx @@ -1,6 +1,6 @@ import { Code, Play } from 'lucide-react' import { DragEvent, ReactNode, useEffect, useMemo, useState } from 'react' -import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts' +import { Bar, BarChart, CartesianGrid, Cell, Tooltip, XAxis, YAxis } from 'recharts' import { toast } from 'sonner' import { useParams } from 'common' @@ -12,9 +12,10 @@ import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql- import dayjs from 'dayjs' import { Parameter, parseParameters } from 'lib/sql-parameters' import { Dashboards } from 'types' -import { ChartContainer, ChartTooltip, ChartTooltipContent, cn, CodeBlock, SQL_ICON } from 'ui' +import { ChartContainer, ChartTooltipContent, cn, CodeBlock, SQL_ICON } from 'ui' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { ButtonTooltip } from '../ButtonTooltip' +import { CHART_COLORS } from '../Charts/Charts.constants' import SqlWarningAdmonition from '../SqlWarningAdmonition' import { BlockViewConfiguration } from './BlockViewConfiguration' import { EditQueryButton } from './EditQueryButton' @@ -130,6 +131,7 @@ export const QueryBlock = ({ const [readOnlyError, setReadOnlyError] = useState(false) const [queryError, setQueryError] = useState() const [queryResult, setQueryResult] = useState(results) + const [focusDataIndex, setFocusDataIndex] = useState() const formattedQueryResult = useMemo(() => { // Make sure Y axis values are numbers @@ -379,10 +381,9 @@ export const QueryBlock = ({

Select columns for the X and Y axes

) : ( -
+
{ + if (e.activeTooltipIndex !== focusDataIndex) { + setFocusDataIndex(e.activeTooltipIndex) + } + }} + onMouseLeave={() => setFocusDataIndex(undefined)} > - + - } /> - + } /> + + {chartData?.map((_: any, index: number) => ( + + ))} +
From 5e0629cc09cca0a86f3003ee138a0d1762fc14db Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 30 Apr 2025 18:06:20 +0800 Subject: [PATCH 59/86] Fix local version popover version (#35375) --- .../LayoutHeader/LocalVersionPopover.tsx | 15 +++++++++------ apps/studio/pages/api/cli-release-version.ts | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LocalVersionPopover.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LocalVersionPopover.tsx index 1fdd245e201..6adaa0abf2b 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LocalVersionPopover.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LocalVersionPopover.tsx @@ -28,15 +28,18 @@ import { getSemver, semverGte, semverLte } from './LocalVersionPopover.utils' export const LocalVersionPopover = () => { const { data, isSuccess } = useCLIReleaseVersionQuery() const currentCliVersion = data?.current - const hasLatestCLIVersion = isSuccess && !!data?.latest + const latestCliVersion = data?.latest + const hasLatestCLIVersion = isSuccess && !!latestCliVersion - const current = getSemver(data?.current) - const latest = getSemver(data?.latest) + const current = getSemver(currentCliVersion) + const latest = getSemver(latestCliVersion) const hasUpdate = - !!current && !!latest ? data?.current !== data?.latest && semverLte(current, latest) : false + !!current && !!latest + ? currentCliVersion !== latestCliVersion && semverLte(current, latest) + : false const isBeta = - !!current && !!latest && data?.current !== data?.latest && semverGte(current, latest) + !!current && !!latest && currentCliVersion !== latestCliVersion && semverGte(current, latest) const approximateNextRelease = !!data?.published_at ? dayjs(data?.published_at).utc().add(14, 'day').format('DD MMM YYYY') @@ -180,7 +183,7 @@ export const LocalVersionPopover = () => { {hasLatestCLIVersion && hasUpdate && !isBeta && (

Available version:

-

{data.latest}

+

{latestCliVersion}

)}
diff --git a/apps/studio/pages/api/cli-release-version.ts b/apps/studio/pages/api/cli-release-version.ts index 0c67461cb35..7c6ce96b38a 100644 --- a/apps/studio/pages/api/cli-release-version.ts +++ b/apps/studio/pages/api/cli-release-version.ts @@ -8,7 +8,7 @@ type GitHubRepositoryRelease = { published_at: string } -const current = process.env.CURRENT_CLI_VERSION +const current = `v${process.env.CURRENT_CLI_VERSION}` const handler = async (req: NextApiRequest, res: NextApiResponse) => { try { From a343e864a02b3feb765d41b2fbb2825e17650fca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:33:11 +0000 Subject: [PATCH 60/86] build(deps-dev): bump react-router from 7.4.0 to 7.5.2 (#35263) * build(deps-dev): bump react-router from 7.4.0 to 7.5.2 Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.4.0 to 7.5.2. - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.5.2/packages/react-router) --- updated-dependencies: - dependency-name: react-router dependency-version: 7.5.2 dependency-type: direct:development ... Signed-off-by: dependabot[bot] * Rerun the version bump. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ivan Vasilov --- apps/ui-library/package.json | 2 +- pnpm-lock.yaml | 35 +++++++++++++++++------------------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/apps/ui-library/package.json b/apps/ui-library/package.json index 8157c99e4e7..8523c160c0f 100644 --- a/apps/ui-library/package.json +++ b/apps/ui-library/package.json @@ -70,7 +70,7 @@ "mdast-util-toc": "^6.1.1", "postcss": "^8", "react-dropzone": "^14.3.8", - "react-router": "^7.4.0", + "react-router": "^7.5.2", "rimraf": "^4.1.3", "shadcn": "2.4.0-canary.16", "shiki": "^1.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5b8299a5f0..9b294548e12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -747,7 +747,7 @@ importers: version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: ^2.4.1 - version: 2.4.1(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 2.4.1(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) openai: specifier: ^4.20.1 version: 4.71.1(encoding@0.1.13)(zod@3.23.8) @@ -1057,7 +1057,7 @@ importers: version: 1.1.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-router/fs-routes': specifier: ^7.4.0 - version: 7.4.0(@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5))(typescript@5.5.2) + version: 7.4.0(@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5))(typescript@5.5.2) '@supabase/postgrest-js': specifier: '*' version: 1.19.2 @@ -1148,7 +1148,7 @@ importers: devDependencies: '@react-router/dev': specifier: ^7.1.5 - version: 7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5) + version: 7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5) '@shikijs/compat': specifier: ^1.1.7 version: 1.6.0 @@ -1192,8 +1192,8 @@ importers: specifier: ^14.3.8 version: 14.3.8(react@18.3.1) react-router: - specifier: ^7.4.0 - version: 7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.5.2 + version: 7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rimraf: specifier: ^4.1.3 version: 4.4.1 @@ -12518,8 +12518,8 @@ packages: peerDependencies: react: '>= 16.3' - react-router@7.4.0: - resolution: {integrity: sha512-Y2g5ObjkvX3VFeVt+0CIPuYd9PpgqCslG7ASSIdN73LwA1nNWzcMLaoMRJfP3prZFI92svxFwbn7XkLJ+UPQ6A==} + react-router@7.5.3: + resolution: {integrity: sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -18651,7 +18651,7 @@ snapshots: dependencies: react: 18.3.1 - '@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5)': + '@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5)': dependencies: '@babel/core': 7.26.10(supports-color@8.1.1) '@babel/generator': 7.27.0 @@ -18662,7 +18662,7 @@ snapshots: '@babel/traverse': 7.27.0(supports-color@8.1.1) '@babel/types': 7.27.0 '@npmcli/package-json': 4.0.1 - '@react-router/node': 7.4.0(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.5.2) + '@react-router/node': 7.4.0(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.5.2) arg: 5.0.2 babel-dead-code-elimination: 1.0.9(supports-color@8.1.1) chokidar: 4.0.3 @@ -18676,7 +18676,7 @@ snapshots: picocolors: 1.1.1 prettier: 2.8.8 react-refresh: 0.14.2 - react-router: 7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: 7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) semver: 7.7.1 set-cookie-parser: 2.7.1 valibot: 0.41.0(typescript@5.5.2) @@ -18700,17 +18700,17 @@ snapshots: - tsx - yaml - '@react-router/fs-routes@7.4.0(@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5))(typescript@5.5.2)': + '@react-router/fs-routes@7.4.0(@react-router/dev@7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5))(typescript@5.5.2)': dependencies: - '@react-router/dev': 7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5) + '@react-router/dev': 7.4.0(@types/node@22.13.14)(jiti@2.4.2)(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.72.0)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.19.3)(typescript@5.5.2)(vite@6.2.6(@types/node@22.13.14)(jiti@2.4.2)(sass@1.72.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.4.5))(yaml@2.4.5) minimatch: 9.0.5 optionalDependencies: typescript: 5.5.2 - '@react-router/node@7.4.0(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.5.2)': + '@react-router/node@7.4.0(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(typescript@5.5.2)': dependencies: '@mjackson/node-fetch-server': 0.2.0 - react-router: 7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: 7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) source-map-support: 0.5.21 stream-slice: 0.1.2 undici: 6.21.2 @@ -26945,13 +26945,13 @@ snapshots: mitt: 3.0.1 next: 14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0) - nuqs@2.4.1(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + nuqs@2.4.1(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): dependencies: mitt: 3.0.1 react: 18.3.1 optionalDependencies: next: 15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0) - react-router: 7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: 7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nwsapi@2.2.20: optional: true @@ -28074,9 +28074,8 @@ snapshots: transitivePeerDependencies: - react-dom - react-router@7.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router@7.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@types/cookie': 0.6.0 cookie: 1.0.2 react: 18.3.1 set-cookie-parser: 2.7.1 From 4466b9e262c887b9d6a35684996af3ca4fccae63 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:14:49 -0400 Subject: [PATCH 61/86] revert(ui-patterns): admonition link underline (#35387) Admonition link underline was added when admonitions were not "prose", but admonitions are now "prose" again so it is redundant --- packages/ui-patterns/admonition.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui-patterns/admonition.tsx b/packages/ui-patterns/admonition.tsx index 5c546acbedd..6c046091cc0 100644 --- a/packages/ui-patterns/admonition.tsx +++ b/packages/ui-patterns/admonition.tsx @@ -103,7 +103,6 @@ export const Admonition = forwardRef< {...props} className={cn( 'mb-2', - '[&_a]:underline', admonitionSVG({ type: typeMapped }), admonitionBase({ type: typeMapped }), props.className From c35fe03ab6972a2658de619169e2c54392b8f20e Mon Sep 17 00:00:00 2001 From: Prashant Sridharan <914007+CoolAssPuppy@users.noreply.github.com> Date: Wed, 30 Apr 2025 18:45:56 +0100 Subject: [PATCH 62/86] Added Euka case study (#35385) --- apps/www/_customers/bree.mdx | 14 +-- apps/www/_customers/euka.mdx | 112 ++++++++++++++++++ apps/www/data/CustomerStories.ts | 11 ++ apps/www/public/customers-rss.xml | 9 +- .../images/blog/avatars/kevin-wang-euka.jpeg | Bin 0 -> 52131 bytes .../public/images/customers/logos/euka.png | Bin 0 -> 3373 bytes .../images/customers/logos/light/euka.png | Bin 0 -> 2422 bytes 7 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 apps/www/_customers/euka.mdx create mode 100644 apps/www/public/images/blog/avatars/kevin-wang-euka.jpeg create mode 100644 apps/www/public/images/customers/logos/euka.png create mode 100644 apps/www/public/images/customers/logos/light/euka.png diff --git a/apps/www/_customers/bree.mdx b/apps/www/_customers/bree.mdx index 411ea8e9e03..8e70581f263 100644 --- a/apps/www/_customers/bree.mdx +++ b/apps/www/_customers/bree.mdx @@ -30,7 +30,7 @@ region: 'North America' supabase_products: ['database', 'auth', 'functions', 'vector'] --- - + We hit scalability limits with Fauna, and Supabase was a step up across the board. The developer experience, ecosystem, support for AI. It’s the complete package. @@ -41,7 +41,7 @@ supabase_products: ['database', 'auth', 'functions', 'vector'] Bree had originally built their infrastructure on Fauna, a NoSQL database with a proprietary query language (FQL). The limitations quickly surfaced. - + Fauna’s custom language, FQL, introduced friction in our development process. It took time to learn and made it harder for our team to iterate and ship at the speed we needed. @@ -54,7 +54,7 @@ And when Fauna announced its end of life, it only added urgency. Bree needed a r Bree had long been aware of Supabase. Supabase had earned its reputation among builders and indie developers for its ease of use and instant setup. But what sold Bree was how well Supabase aligned with their long-term technical goals. - + Builders around the world use Supabase for exciting projects. It is so intuitive that developers can start building immediately and ship production-ready features in a day. @@ -65,7 +65,7 @@ After a thorough evaluation of vendors, Bree chose Supabase for several reasons: - A tightly integrated suite of tools: [Auth](/auth), [Edge Functions](/edge-functions), [Vectors](/modules/vector), and more. - A rich ecosystems of partners and plugins - + It’s almost the default database for AI-first development. It’s integrated with Lovable, with V0, and with Cursor—which we use heavily. Supabase just fits. @@ -94,14 +94,14 @@ Phase two, now underway, is focused on modularizing the data model, splitting la The results of Bree’s migration to Supabase have been immediate and measurable. Engineers can ship faster, performance is up across the board, and Supabase has become a foundational part of the team’s AI-driven development workflow. - + After migrating to Supabase, we saw immediate lift in developer velocity. Engineers can iterate on features much faster by writing Postgres SQL queries. Latency improvements are notable. Bree has observed double-digit reductions in response times across core functions and expects further gains as their architecture is optimized for Supabase. - + It already has double-digit latency improvements. With further optimization, some functions were 10x faster. @@ -117,7 +117,7 @@ Bree’s journey reflects the reasons developers and fast-moving teams choose Su - **AI-native infrastructure** – Supabase supports tools like Cursor and `pgvector` out of the box, making it ideal for AI-powered development - **Built to scale** – With high availability, backups, and a flexible Postgres foundation, Supabase is ready for growth - + Supabase puts your mind at ease. It has the best support experience I’ve ever seen. It’s a disruptive technology to bring to any organization. It’s very worth switching to. diff --git a/apps/www/_customers/euka.mdx b/apps/www/_customers/euka.mdx new file mode 100644 index 00000000000..7a8f2b4728a --- /dev/null +++ b/apps/www/_customers/euka.mdx @@ -0,0 +1,112 @@ +--- +name: Euka +title: 'Euka used Supabase to unlock faster growth' +# Use meta_title to add a custom meta title. Otherwise it defaults to '{name} | Supabase Customer Stories': +# meta_title: +description: Supabase helped Euka accelerate development, simplify AI feature rollout, and scale creator marketing faster than ever. A Postgres-based platform gave them the structure, speed, and flexibility they needed to grow. +# Use meta_description to add a custom meta description. Otherwise it defaults to {description}: +meta_description: Discover how Supabase helped Euka accelerate development. +author: prashant +author_title: Prashant Sridharan +author_url: https://github.com/CoolAssPuppy +author_image_url: https://avatars.githubusercontent.com/u/914007?v=4 +logo: /images/customers/logos/euka.png +logo_inverse: /images/customers/logos/light/euka.png +og_image: /images/customers/og/euka.jpg +tags: + - supabase +date: '2025-04-29' +company_url: https://www.euka.ai +stats: [{ stat: '00,000', label: Example stat }] +misc: [{ label: 'Founded', text: 'Canada' }] +about: Turning TikTok affiliates into a revenue-generating engine. +# "healthcare" | "fintech" | "ecommerce" | "education" | "gaming" | "media" | "real-estate" | "saas" | "social" | "analytics" | "ai" | "developer-tools" +industry: ['media', 'social'] +# "startup" | "enterprise" | "indie_dev" +company_size: 'startup' +# "Asia" | "Europe" | "North America" | "South America" | "Africa" | "Oceania" +region: 'North America' +# "database" | "auth" | "storage" | "realtime" | "functions" | "vector" +supabase_products: ['database', 'auth', 'storage', 'functions', 'vector'] +--- + + + Supabase gave us the tools we needed to move fast, scale quickly, and stay flexible as we grow — + all without the heavy lifting. + + +Euka is reimagining how brands scale their presence on social media. Their AI-powered platform helps businesses find, manage, and scale creator partnerships, primarily on TikTok Shop. By automating the most time-consuming parts of user-generated content (UGC) campaigns, Euka allows brands to build awareness and drive sales faster than ever before. + +As they expanded beyond ecommerce into SaaS and consumer software, Euka needed a backend platform that could scale with them without slowing them down. + +## The challenge + +Euka’s vision required a backend that could support rapid iteration, structured data analytics, seamless AI integration, and scalable storage. To succeed, Euka needed: + +- A relational database that supported structured queries. +- Integrated services like authentication, storage, and AI search. +- A platform that scaled easily as their business grew. +- A developer-first experience to enable faster shipping. + +## Choosing Supabase + +When it came time to choose a permanent foundation for the business, Kevin recalled a side project where Supabase had impressed him with its simplicity and speed. This time, for Euka, it was not just about familiarity. It was about finding the right platform to unlock their next phase of growth. + +Supabase provided everything they needed in one place: + +- A fully managed Postgres database with standard SQL support. +- Built-in authentication, storage, edge functions, and vector search. +- Native AI features like pgvector for semantic search. +- An intuitive dashboard and first-class developer experience. + + + It just worked. Supabase gave us a zero-to-one experience that let us build quickly without + worrying about the backend. + + +Supabase allowed the team to move faster without compromising on performance or scalability. It also future-proofed their stack for AI development without needing costly rewrites later. + +## How Euka built with Supabase + +Euka’s architecture focuses on simplicity and rapid iteration. They designed their system to get the most out of Supabase while maintaining flexibility for future growth. + +Their stack includes: + +- **Frontend**: Two Next.js apps deployed on Vercel, one for brands and one for creators. +- **Backend**: Python scripts for data scraping, trend analysis, and creator search, connected directly to Supabase. +- **Storage**: Supabase Storage for managing creator profile pictures, videos, and ecommerce product images. +- **Authentication**: Supabase Auth with ongoing fine-tuning around OAuth flows. +- **AI search**: Supabase’s `pgvector` extension for trending video discovery, using cosine similarity searches. + + + Setting up the database and storage was one of the easiest parts of building our MVP. + + +Supabase’s dashboard also became a powerful internal tool. Engineers could debug quickly by inspecting real-time logs and queries. Non-technical teams like marketing and sales used the dashboard to run simple queries, inspect user data, and export reports without needing custom-built admin panels. + +## The results + +Since launching on Supabase, Euka has accelerated every part of their development lifecycle. They can build quickly and are able to deliver new AI capabilities faster than expected. + + + With Supabase, we saw an immediate lift in developer velocity. Engineers can iterate on features + much faster by writing Postgres SQL queries. + + +Supabase helped Euka achieve: + +- Faster developer cycles by standardizing on SQL. +- Easier scaling through integrated, high-performance services. +- Simplified AI expansion using `pgvector` and seamless embedding storage. +- Greater internal collaboration through an intuitive, accessible dashboard. + +Supabase gave Euka a faster way to ship, a stronger foundation for AI growth, and a platform that can evolve alongside them as they expand into new industries. + + + Honestly, it’s a no-brainer these days. If you’re starting something new and need structured data, + Supabase is the obvious choice. + + +## Ready to build and scale with Supabase? + +Start your journey today at [www.supabase.com](/) diff --git a/apps/www/data/CustomerStories.ts b/apps/www/data/CustomerStories.ts index cdca1608ba2..93350249549 100644 --- a/apps/www/data/CustomerStories.ts +++ b/apps/www/data/CustomerStories.ts @@ -18,6 +18,17 @@ export type CustomerStoryType = { } export const data: CustomerStoryType[] = [ + { + type: 'Customer Story', + title: 'Euka used Supabase to unlock faster growth', + description: + 'Supabase helped Euka accelerate development, simplify AI feature rollout, and scale creator marketing faster than ever.', + organization: 'Euka', + imgUrl: 'images/customers/logos/euka.png', + logo: '/images/customers/logos/euka.png', + logo_inverse: '/images/customers/logos/light/euka.png', + url: '/customers/euka', + }, { type: 'Customer Story', title: "Bree's Migration to Supabase from Fauna", diff --git a/apps/www/public/customers-rss.xml b/apps/www/public/customers-rss.xml index fb6ea467bda..48b0f075937 100644 --- a/apps/www/public/customers-rss.xml +++ b/apps/www/public/customers-rss.xml @@ -5,9 +5,16 @@ https://supabase.com Latest news from Supabase en - Tue, 08 Apr 2025 00:00:00 -0700 + Tue, 29 Apr 2025 00:00:00 -0700 + https://supabase.com/customers/euka + Euka used Supabase to unlock faster growth + https://supabase.com/customers/euka + Supabase helped Euka accelerate development, simplify AI feature rollout, and scale creator marketing faster than ever. A Postgres-based platform gave them the structure, speed, and flexibility they needed to grow. + Tue, 29 Apr 2025 00:00:00 -0700 + + https://supabase.com/customers/bree Bree's Migration to Supabase from Fauna https://supabase.com/customers/bree diff --git a/apps/www/public/images/blog/avatars/kevin-wang-euka.jpeg b/apps/www/public/images/blog/avatars/kevin-wang-euka.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..365a2020ba293e6ed8c24f534c36ea6e0467f51c GIT binary patch literal 52131 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 DF~T7U literal 0 HcmV?d00001 diff --git a/apps/www/public/images/customers/logos/euka.png b/apps/www/public/images/customers/logos/euka.png new file mode 100644 index 0000000000000000000000000000000000000000..d60da3dc2a6456c7c88b16608403143492a4953b GIT binary patch literal 3373 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 Date: Wed, 30 Apr 2025 11:04:30 -0700 Subject: [PATCH 63/86] fix(docs): Fix example file name in auth/quickstarts/nextjs.mdx (#35364) fix: Fix example file name in auth/quickstarts/nextjs.mdx --- apps/docs/content/guides/auth/quickstarts/nextjs.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/auth/quickstarts/nextjs.mdx b/apps/docs/content/guides/auth/quickstarts/nextjs.mdx index 90b0bec7d26..69e6a29ef44 100644 --- a/apps/docs/content/guides/auth/quickstarts/nextjs.mdx +++ b/apps/docs/content/guides/auth/quickstarts/nextjs.mdx @@ -52,7 +52,7 @@ hideToc: true - Rename `.env.local.example` to `.env.local` and populate with [your project's URL and Anon Key](https://supabase.com/dashboard/project/_/settings/api). + Rename `.env.example` to `.env.local` and populate with [your project's URL and Anon Key](https://supabase.com/dashboard/project/_/settings/api). From 0797b10bf34b679c45cfcf1f3715c5ac52983c11 Mon Sep 17 00:00:00 2001 From: samrose Date: Wed, 30 Apr 2025 15:48:57 -0400 Subject: [PATCH 64/86] docs: small correction in plan_filter doc for usage (#35379) * docs: small correction in plan_filter doc for usage * docs: fixing proofread comments * docs: prettier formmatting fix * docs: omit unnecessary `load` command --------- Co-authored-by: Bobbie Soedirgo --- .../guides/database/extensions/pg_plan_filter.mdx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx b/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx index fe490755a0b..17e8782a234 100644 --- a/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx +++ b/apps/docs/content/guides/database/extensions/pg_plan_filter.mdx @@ -8,19 +8,9 @@ description: 'Block queries over a total cost limit' ## Enable the extension -`pg_plan_filter` can be enabled on a per connection basis: +The extension is already enabled by default via `shared_preload_libraries` setting. -{/* prettier-ignore */} -```sql -load 'plan_filter'; -``` - -or for all connections: - -{/* prettier-ignore */} -```sql -alter database some_db set session_preload_libraries = 'plan_filter'; -``` +You can follow the instructions below. ## API @@ -66,7 +56,6 @@ Now we can choose a `statement_cost_filter` value between the total cost for the {/* prettier-ignore */} ```sql -load 'plan_filter'; set plan_filter.statement_cost_limit = 50; -- between 2.49 and 135.0 select * from book where id = 1; From c638255cb225a9c244e6364281ed9a39f6bbb092 Mon Sep 17 00:00:00 2001 From: Wen Bo Xie <5532241+w3b6x9@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:54:53 -0400 Subject: [PATCH 65/86] fix: change notion to linear team mapping (#35363) --- apps/docs/components/Feedback/Feedback.tsx | 4 ++-- .../components/Feedback/Feedback.utils.ts | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/docs/components/Feedback/Feedback.tsx b/apps/docs/components/Feedback/Feedback.tsx index 16771ca6e5b..e2aa841aa43 100644 --- a/apps/docs/components/Feedback/Feedback.tsx +++ b/apps/docs/components/Feedback/Feedback.tsx @@ -18,7 +18,7 @@ import { Button, cn } from 'ui' import { IS_PLATFORM } from '~/lib/constants' import { useSendFeedbackMutation } from '~/lib/fetch/feedback' import { useSendTelemetryEvent } from '~/lib/telemetry' -import { getNotionTeam, getSanitizedTabParams } from './Feedback.utils' +import { getLinearTeam, getSanitizedTabParams } from './Feedback.utils' import { type FeedbackFields, FeedbackModal } from './FeedbackModal' const FeedbackButton = forwardRef< @@ -134,7 +134,7 @@ function Feedback({ className }: { className?: string }) { title, // @ts-expect-error -- can't click this button without having a state.response isHelpful: state.response === 'yes', - team: getNotionTeam(pathname), + team: getLinearTeam(pathname), }) setModalOpen(false) refocusButton() diff --git a/apps/docs/components/Feedback/Feedback.utils.ts b/apps/docs/components/Feedback/Feedback.utils.ts index cf9ff4c989a..2fa626ce14d 100644 --- a/apps/docs/components/Feedback/Feedback.utils.ts +++ b/apps/docs/components/Feedback/Feedback.utils.ts @@ -3,8 +3,8 @@ import { pick } from 'lodash' /** * Gets the Notion team to send feedback to based on the pathname. */ -const getNotionTeam = (pathname: string) => { - const DEFAULT_TEAM = 'team-docs' +const getLinearTeam = (pathname: string) => { + const DEFAULT_TEAM = 'Docs' // Pathname has format `/guides/(team)/**` const pathParts = pathname.split('/') @@ -13,26 +13,26 @@ const getNotionTeam = (pathname: string) => { switch (pathParts[2]) { case 'database': - return 'team-postgres' + return 'Postgres' case 'auth': - return 'team-auth' + return 'Auth' case 'storage': - return 'team-storage' + return 'Storage' case 'functions': - return 'team-functions' + return 'Functions' case 'realtime': - return 'team-realtime' + return 'Realtime' case 'ai': - return 'team-ai' + return 'AI' case 'local-development': case 'self-hosting': case 'deployment': - return 'team-dev-workflows' + return 'Dev Workflows' case 'integrations': - return 'team-api' + return 'API' case 'platform': case 'monitoring-troubleshooting': - return 'team-infra' + return 'Infra' default: return DEFAULT_TEAM } @@ -51,4 +51,4 @@ const getSanitizedTabParams = () => { return pick(Object.fromEntries(searchParams.entries()), queryGroups) } -export { getNotionTeam, getSanitizedTabParams } +export { getLinearTeam, getSanitizedTabParams } From 84ff291a090d538409dc4c208b45778a44e70c0d Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Wed, 30 Apr 2025 20:24:09 -0400 Subject: [PATCH 66/86] fix(docs): wrappers tags (#35386) Pull wrappers tags from tag docs_v*, not tag v* --- .../extensions/wrappers/[[...slug]]/page.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx index f5c6712dfcc..8028d6c5625 100644 --- a/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx +++ b/apps/docs/app/guides/database/extensions/wrappers/[[...slug]]/page.tsx @@ -28,10 +28,10 @@ const externalSite = 'https://supabase.github.io/wrappers' type TagQueryResponse = { repository: { - releases: { + refs: { nodes: | { - tagName: string + name: string }[] | null pageInfo: { @@ -45,16 +45,17 @@ type TagQueryResponse = { const tagQuery = ` query TagQuery($owner: String!, $name: String!, $after: String) { repository(owner: $owner, name: $name) { - releases( + refs( + refPrefix: "refs/tags/", orderBy: { - field: CREATED_AT, + field: TAG_COMMIT_DATE, direction: DESC }, first: 5, after: $after ) { nodes { - tagName + name } pageInfo { hasNextPage @@ -69,7 +70,7 @@ async function getLatestRelease(after: string | null = null) { try { const { repository: { - releases: { + refs: { nodes, pageInfo: { hasNextPage, endCursor }, }, @@ -85,7 +86,7 @@ async function getLatestRelease(after: string | null = null) { }) return ( - nodes?.find((node) => node?.tagName?.match(/^v\d+\.\d+\.\d+/)).tagName ?? + nodes?.find((node) => node?.name?.match(/^docs_v\d+\.\d+\.\d+/))?.name ?? (hasNextPage && endCursor ? await getLatestRelease(endCursor) : null) ) } catch (error) { From 38173acc34677d0823fca5f2b99f13f87fb48e64 Mon Sep 17 00:00:00 2001 From: Pamela Chia Date: Thu, 1 May 2025 19:24:39 +0800 Subject: [PATCH 67/86] fix: error when telemetry groups unknown (#35403) --- packages/common/telemetry.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/common/telemetry.tsx b/packages/common/telemetry.tsx index 29278df8cfa..51e9295aee1 100644 --- a/packages/common/telemetry.tsx +++ b/packages/common/telemetry.tsx @@ -172,7 +172,14 @@ export function sendTelemetryEvent(API_URL: string, event: TelemetryEvent, pathn ...getSharedTelemetryData(pathname), action: event.action, custom_properties: 'properties' in event ? event.properties : {}, - groups: 'groups' in event ? event.groups : {}, + groups: 'groups' in event ? { ...event.groups } : {}, + } + + if (body.groups?.project === 'Unknown') { + delete body.groups.project + if (body.groups?.organization === 'Unknown') { + delete body.groups + } } return post(`${ensurePlatformSuffix(API_URL)}/telemetry/event`, body, { From 2ca5a1285428501cb6ea27ea836a049b10e87e18 Mon Sep 17 00:00:00 2001 From: yujiseok <83855636+yujiseok@users.noreply.github.com> Date: Fri, 2 May 2025 00:25:22 +0900 Subject: [PATCH 68/86] fix broken Apple REST API link in auth-apple.mdx (#35404) - replace broken Sign in with Apple REST API link --- apps/docs/content/guides/auth/social-login/auth-apple.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 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 0316b186396..aa2faa122e1 100644 --- a/apps/docs/content/guides/auth/social-login/auth-apple.mdx +++ b/apps/docs/content/guides/auth/social-login/auth-apple.mdx @@ -14,7 +14,7 @@ To support Sign in with Apple, you need to configure the [Apple provider in the There are three general ways to use Sign in with Apple, depending on the application you're trying to build: - Sign in on the web or in web-based apps - - Using an OAuth flow initiated by Supabase Auth using the [Sign in with Apple REST API](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api). + - Using an OAuth flow initiated by Supabase Auth using the [Sign in with Apple REST API](https://developer.apple.com/documentation/signinwithapplerestapi). - Using [Sign in with Apple JS](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js) directly in the browser, usually suitable for websites. - Sign in natively inside iOS, macOS, watchOS or tvOS apps using [Apple's Authentication Services](https://developer.apple.com/documentation/authenticationservices) @@ -35,7 +35,7 @@ When developing with Expo, you can test Sign in with Apple via the Expo Go app, Sign in with Apple's OAuth flow is designed for web or browser based sign in methods. It can be used on web-based apps as well as websites, though some users can benefit by using Sign in with Apple JS directly. - Behind the scenes, Supabase Auth uses the [REST APIs](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api) provided by Apple. + Behind the scenes, Supabase Auth uses the [REST APIs](https://developer.apple.com/documentation/signinwithapplerestapi) provided by Apple. <$Partial path="create_client_snippet.mdx" /> From 7307f96879923efd538c3e5c5aca47114beca1ba Mon Sep 17 00:00:00 2001 From: Julian S <64325884+hijuliancode@users.noreply.github.com> Date: Thu, 1 May 2025 10:32:15 -0500 Subject: [PATCH 69/86] Update declarative-database-schemas.mdx (#35397) The alert "Make sure your local database is stopped before diffing your schema." appears after completing the diff step. It would be more useful to display it before performing the diff, to ensure that "supabase stop" is executed beforehand and not afterward. --- .../declarative-database-schemas.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/docs/content/guides/local-development/declarative-database-schemas.mdx b/apps/docs/content/guides/local-development/declarative-database-schemas.mdx index 26eccaca0a3..37b2b085342 100644 --- a/apps/docs/content/guides/local-development/declarative-database-schemas.mdx +++ b/apps/docs/content/guides/local-development/declarative-database-schemas.mdx @@ -42,6 +42,12 @@ create table "employees" ( + + +Make sure your local database is stopped before diffing your schema. + + + @@ -64,12 +70,6 @@ supabase db diff -f create_employees_table - - -Make sure your local database is stopped before diffing your schema. - - - From 263b0c1c5b734c75e4315f99f444d8ed3209f524 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Thu, 1 May 2025 13:16:13 -0230 Subject: [PATCH 70/86] Fix pointer events (#35408) --- apps/studio/components/ui/SchemaSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/ui/SchemaSelector.tsx b/apps/studio/components/ui/SchemaSelector.tsx index a8103d0d931..7a0293ca219 100644 --- a/apps/studio/components/ui/SchemaSelector.tsx +++ b/apps/studio/components/ui/SchemaSelector.tsx @@ -123,7 +123,7 @@ const SchemaSelector = ({ Date: Fri, 2 May 2025 12:10:41 +1000 Subject: [PATCH 71/86] feat: billing address, name and tax id for org creation/upgrade (#34922) We currently get support requests about adding or amending invoices due to no billing address being set. Changes in this PR: - Unify tax id and billing address into a single form - Add ability to set billing name that diverges from org name - Add ability to set billing address & tax id during org creation (only paid plans) - Add ability to set billing address & tax id during plan upgrade --- .../BillingAddress/BillingAddress.tsx | 235 ------- .../BillingBreakdown/BillingBreakdown.tsx | 12 +- .../BillingAddress.constants.ts | 0 .../BillingCustomerData.tsx | 178 +++++ .../BillingCustomerDataExistingOrgDialog.tsx | 215 ++++++ .../BillingCustomerDataForm.tsx | 338 ++++++++++ .../BillingCustomerDataNewOrgDialog.tsx | 122 ++++ .../TaxID.constants.ts | 0 .../TaxID.utils.ts | 0 .../useBillingCustomerDataForm.ts | 131 ++++ .../BillingSettings/BillingEmail.tsx | 13 +- .../BillingSettings/BillingSettings.tsx | 13 +- .../CostControl/CostControl.tsx | 10 +- .../BillingSettings/CreditBalance.tsx | 10 +- .../PaymentMethods/PaymentMethods.tsx | 13 +- .../Subscription/PlanUpdateSidePanel.tsx | 4 +- .../Subscription/Subscription.tsx | 10 +- .../SubscriptionPlanUpdateDialog.tsx | 630 ++++++++---------- .../BillingSettings/TaxID/TaxID.tsx | 261 -------- .../InvoicesSettings/InvoicesSection.tsx | 9 +- .../Organization/NewOrg/NewOrgForm.tsx | 29 +- .../organization-create-mutation.ts | 11 +- ...zation-customer-profile-update-mutation.ts | 9 +- .../organization-tax-id-query.ts | 6 +- .../organization-tax-id-update-mutation.ts | 2 +- apps/studio/hooks/misc/useCheckPermissions.ts | 16 +- apps/studio/pages/org/[slug]/billing.tsx | 19 +- .../components/Billing/TaxID.utils.test.ts | 2 +- packages/api-types/types/platform.d.ts | 31 +- 29 files changed, 1383 insertions(+), 946 deletions(-) delete mode 100644 apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx rename apps/studio/components/interfaces/Organization/BillingSettings/{BillingAddress => BillingCustomerData}/BillingAddress.constants.ts (100%) create mode 100644 apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx create mode 100644 apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataExistingOrgDialog.tsx create mode 100644 apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx create mode 100644 apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataNewOrgDialog.tsx rename apps/studio/components/interfaces/Organization/BillingSettings/{TaxID => BillingCustomerData}/TaxID.constants.ts (100%) rename apps/studio/components/interfaces/Organization/BillingSettings/{TaxID => BillingCustomerData}/TaxID.utils.ts (100%) create mode 100644 apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm.ts delete mode 100644 apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx deleted file mode 100644 index 0275aaae5f7..00000000000 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' -import { useEffect } from 'react' -import { toast } from 'sonner' - -import { - ScaffoldSection, - ScaffoldSectionContent, - ScaffoldSectionDetail, -} from 'components/layouts/Scaffold' -import AlertError from 'components/ui/AlertError' -import { FormActions } from 'components/ui/Forms/FormActions' -import { FormPanel } from 'components/ui/Forms/FormPanel' -import { FormSection, FormSectionContent } from 'components/ui/Forms/FormSection' -import NoPermission from 'components/ui/NoPermission' -import PartnerManagedResource from 'components/ui/PartnerManagedResource' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' -import { useOrganizationCustomerProfileUpdateMutation } from 'data/organizations/organization-customer-profile-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { Form, Input, Listbox } from 'ui' -import { COUNTRIES } from './BillingAddress.constants' - -const BillingAddress = () => { - const { slug } = useParams() - const selectedOrganization = useSelectedOrganization() - - const canReadBillingAddress = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.customer' - ) - const canUpdateBillingAddress = useCheckPermissions( - PermissionAction.BILLING_WRITE, - 'stripe.customer' - ) - - const { data, error, isLoading, isSuccess, isError } = useOrganizationCustomerProfileQuery( - { slug }, - { enabled: canReadBillingAddress } - ) - const { mutate: updateCustomerProfile, isLoading: isUpdating } = - useOrganizationCustomerProfileUpdateMutation() - - const formId = 'billing-address-form' - const { city, country, line1, line2, postal_code, state } = data?.address ?? {} - const initialValues = { city, country, line1, line2, postal_code, state } - - const validate = (values: any) => { - const errors = {} as any - if ( - (values.line1 || values.line2 || values.postal_code || values.state || values.city) && - !values.country - ) { - errors['country'] = 'Please select a country' - } - if ( - (values.country || values.line2 || values.postal_code || values.state || values.city) && - !values.line1 - ) { - errors['line1'] = 'Please provide an address line' - } - return errors - } - - const onSubmit = async (values: any, { resetForm }: any) => { - if (!slug) return console.error('Slug is required') - - const address = !values.line1 ? null : values - - updateCustomerProfile( - { slug, address }, - { - onSuccess: () => { - toast.success('Successfully updated billing address') - resetForm({ values, initialValues: values }) - }, - } - ) - } - - return ( - - -

- - - {selectedOrganization?.managed_by !== undefined && - selectedOrganization?.managed_by !== 'supabase' ? ( - - ) : !canReadBillingAddress ? ( - - ) : ( - <> - {isLoading && ( -
- - - -
- )} - - {isError && ( - - )} - - {isSuccess && ( - - {({ values, initialValues, handleReset, resetForm }: any) => { - const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) - - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - if (isSuccess && data !== undefined) { - const { city, country, line1, line2, postal_code, state } = - data?.address ?? {} - const values = { city, country, line1, line2, postal_code, state } - resetForm({ values, initialValues: values }) - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccess]) - - return ( - - -
- } - > - - - - -
- - - --- - - {COUNTRIES.map((country) => ( - - {country.name} - - ))} - - -
-
- - -
-
-
- - ) - }} - - )} - - )} - - - ) -} - -export default BillingAddress diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx index 28b5d774ba8..699e4b85330 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingBreakdown.tsx @@ -11,19 +11,17 @@ import { import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import SparkBar from 'components/ui/SparkBar' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import UpcomingInvoice from './UpcomingInvoice' -import SparkBar from 'components/ui/SparkBar' const BillingBreakdown = () => { const { slug: orgSlug } = useParams() - const canReadSubscriptions = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const { data: subscription, @@ -74,7 +72,7 @@ const BillingBreakdown = () => {
- {!canReadSubscriptions ? ( + {isPermissionsLoaded && !canReadSubscriptions ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.constants.ts b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingAddress.constants.ts similarity index 100% rename from apps/studio/components/interfaces/Organization/BillingSettings/BillingAddress/BillingAddress.constants.ts rename to apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingAddress.constants.ts diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx new file mode 100644 index 00000000000..5082e90df99 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerData.tsx @@ -0,0 +1,178 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useMemo, useState } from 'react' +import { toast } from 'sonner' + +import { useParams } from 'common' +import { + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from 'components/layouts/Scaffold' +import AlertError from 'components/ui/AlertError' +import NoPermission from 'components/ui/NoPermission' +import PartnerManagedResource from 'components/ui/PartnerManagedResource' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' +import { useOrganizationCustomerProfileUpdateMutation } from 'data/organizations/organization-customer-profile-update-mutation' +import { useOrganizationTaxIdQuery } from 'data/organizations/organization-tax-id-query' +import { useOrganizationTaxIdUpdateMutation } from 'data/organizations/organization-tax-id-update-mutation' +import { + useAsyncCheckProjectPermissions, + useCheckPermissions, +} from 'hooks/misc/useCheckPermissions' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { Button, Card, CardFooter, Form_Shadcn_ as Form } from 'ui' +import { BillingCustomerDataForm } from './BillingCustomerDataForm' +import { TAX_IDS } from './TaxID.constants' +import { useBillingCustomerDataForm } from './useBillingCustomerDataForm' + +export const BillingCustomerData = () => { + const { slug } = useParams() + const selectedOrganization = useSelectedOrganization() + + const { isSuccess: isPermissionsLoaded, can: canReadBillingCustomerData } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.customer') + const canUpdateBillingCustomerData = useCheckPermissions( + PermissionAction.BILLING_WRITE, + 'stripe.customer' + ) + + const { + data: customerProfile, + error, + isLoading, + isSuccess, + } = useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingCustomerData }) + + const { + data: taxId, + error: errorLoadingTaxId, + isLoading: isLoadingTaxId, + isSuccess: loadedTaxId, + } = useOrganizationTaxIdQuery({ slug }) + + const initialCustomerData = useMemo( + () => ({ + ...customerProfile?.address, + billing_name: customerProfile?.billing_name, + tax_id_type: taxId?.type, + tax_id_value: taxId?.value, + tax_id_name: taxId + ? TAX_IDS.find( + (option) => option.type === taxId.type && option.countryIso2 === taxId.country + )?.name || '' + : '', + }), + [customerProfile, taxId] + ) + + const { mutateAsync: updateCustomerProfile } = useOrganizationCustomerProfileUpdateMutation() + const { mutateAsync: updateTaxId } = useOrganizationTaxIdUpdateMutation() + + const [isSubmitting, setIsSubmitting] = useState(false) + + const { form, handleSubmit, handleReset, isDirty } = useBillingCustomerDataForm({ + initialCustomerData, + onCustomerDataChange: async (data) => { + setIsSubmitting(true) + + try { + await updateCustomerProfile({ + slug, + address: data.address, + billing_name: data.billing_name, + }) + + await updateTaxId({ slug, taxId: data.tax_id }) + + toast.success('Successfully updated billing data') + + setIsSubmitting(false) + } catch (error: any) { + toast.error(`Failed updating billing data: ${error.message}`) + setIsSubmitting(false) + } + }, + }) + + const isSubmitDisabled = !isDirty || !canUpdateBillingCustomerData || isSubmitting + + return ( + + +
+

Billing Address & Tax ID

+

+ This will be reflected in every upcoming invoice, past invoices are not affected +

+
+
+ + {selectedOrganization?.managed_by !== undefined && + selectedOrganization?.managed_by !== 'supabase' ? ( + + ) : isPermissionsLoaded && !canReadBillingCustomerData ? ( + + ) : ( + <> + {(isLoading || isLoadingTaxId) && ( +
+ + + +
+ )} + + {(error || errorLoadingTaxId) && ( + + )} + + {isSuccess && loadedTaxId && ( + +
+ + + + {!canUpdateBillingCustomerData && ( + + You need additional permissions to manage this organization's billing + address + + )} +
+ + +
+
+ + +
+ )} + + )} +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataExistingOrgDialog.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataExistingOrgDialog.tsx new file mode 100644 index 00000000000..9fac673f8fc --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataExistingOrgDialog.tsx @@ -0,0 +1,215 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { Pencil } from 'lucide-react' +import { useMemo, useState } from 'react' +import { toast } from 'sonner' + +import AlertError from 'components/ui/AlertError' +import NoPermission from 'components/ui/NoPermission' +import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' +import { useOrganizationCustomerProfileUpdateMutation } from 'data/organizations/organization-customer-profile-update-mutation' +import { useOrganizationTaxIdQuery } from 'data/organizations/organization-tax-id-query' +import { useOrganizationTaxIdUpdateMutation } from 'data/organizations/organization-tax-id-update-mutation' +import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogSectionSeparator, + DialogTitle, + Form_Shadcn_ as Form, + Label_Shadcn_ as Label, +} from 'ui' +import { BillingCustomerDataForm } from './BillingCustomerDataForm' +import { TAX_IDS } from './TaxID.constants' +import { useBillingCustomerDataForm } from './useBillingCustomerDataForm' + +export const BillingCustomerDataExistingOrgDialog = () => { + const [open, setOpen] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + + const { slug } = useSelectedOrganization() ?? {} + + const canReadBillingCustomerData = useCheckPermissions( + PermissionAction.BILLING_READ, + 'stripe.customer' + ) + + const canUpdateBillingCustomerData = useCheckPermissions( + PermissionAction.BILLING_WRITE, + 'stripe.customer' + ) + + const { + data: customerProfile, + error, + isLoading, + isSuccess, + } = useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingCustomerData }) + + const { + data: taxId, + error: errorTaxId, + isLoading: isLoadingTaxId, + isSuccess: isSuccessTaxId, + } = useOrganizationTaxIdQuery({ slug }) + + const initialCustomerData = useMemo( + () => ({ + ...customerProfile?.address, + billing_name: customerProfile?.billing_name, + tax_id_type: taxId?.type, + tax_id_value: taxId?.value, + tax_id_name: taxId + ? TAX_IDS.find( + (option) => option.type === taxId.type && option.countryIso2 === taxId.country + )?.name || '' + : '', + }), + [customerProfile, taxId] + ) + + const { mutateAsync: updateCustomerProfile } = useOrganizationCustomerProfileUpdateMutation() + const { mutateAsync: updateTaxId } = useOrganizationTaxIdUpdateMutation() + + const { form, handleSubmit, handleReset, isDirty } = useBillingCustomerDataForm({ + initialCustomerData, + onCustomerDataChange: async (data) => { + setIsSubmitting(true) + + try { + await updateCustomerProfile({ + slug, + address: data.address ?? undefined, + billing_name: data.billing_name, + }) + + await updateTaxId({ slug, taxId: data.tax_id }) + + toast.success('Successfully updated billing data') + + setOpen(false) + + setIsSubmitting(false) + } catch (error: any) { + toast.error(`Failed updating billing data: ${error.message}`) + setIsSubmitting(false) + } + }, + }) + + const handleClose = () => { + handleReset() + setOpen(false) + } + + const getAddressSummary = () => { + if (!customerProfile?.address?.line1) return 'Optionally add a billing address' + + const parts = [ + customerProfile?.billing_name, + customerProfile?.address?.line1, + customerProfile?.address?.city, + customerProfile?.address?.state, + customerProfile?.address?.country, + ].filter(Boolean) + + return parts.join(', ') + } + + const isSubmitDisabled = !isDirty || !canUpdateBillingCustomerData || isSubmitting + + return ( + <> +
+ + {!canReadBillingCustomerData ? ( + + ) : ( + <> + {(isLoading || isLoadingTaxId) && ( +
+ +
+ )} + {(error || errorTaxId) && ( + + )} + {isSuccess && isSuccessTaxId && ( +
+

{getAddressSummary()}

+ +
+ )} + + )} +
+ + { + if (!value) setOpen(false) + else setOpen(true) + }} + > + + + Billing Address & Tax Id + + This will be reflected in every upcoming invoice, past invoices are not affected + + + +
+ + + + {!canUpdateBillingCustomerData && ( + + You need additional permissions to manage this organization's billing address + + )} +
+ + +
+
+ + +
+
+ + ) +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx new file mode 100644 index 00000000000..7b7a8da1333 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataForm.tsx @@ -0,0 +1,338 @@ +import { Check, ChevronsUpDown, X } from 'lucide-react' +import { UseFormReturn } from 'react-hook-form' +import { z } from 'zod' + +import { useState } from 'react' +import { + Button, + cn, + Command_Shadcn_ as Command, + CommandEmpty_Shadcn_ as CommandEmpty, + CommandGroup_Shadcn_ as CommandGroup, + CommandInput_Shadcn_ as CommandInput, + CommandItem_Shadcn_ as CommandItem, + CommandList_Shadcn_ as CommandList, + FormControl_Shadcn_ as FormControl, + FormField_Shadcn_ as FormField, + FormMessage_Shadcn_ as FormMessage, + Input_Shadcn_ as Input, + Input_Shadcn_, + Popover_Shadcn_ as Popover, + PopoverContent_Shadcn_ as PopoverContent, + PopoverTrigger_Shadcn_ as PopoverTrigger, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { COUNTRIES } from './BillingAddress.constants' +import { TAX_IDS } from './TaxID.constants' + +interface BillingCustomerDataFormProps { + form: UseFormReturn + disabled?: boolean + className?: string +} + +// Define the expected form values structure and validation schema +export const BillingCustomerDataSchema = z + .object({ + billing_name: z.string().min(3, 'Name must be at least 3 letters long'), + line1: z.string().optional(), + line2: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + postal_code: z.string().optional(), + country: z.string().optional(), + tax_id_type: z.string(), + tax_id_value: z.string(), + tax_id_name: z.string(), + }) + .refine( + (data) => { + // its fine to just set the name, but once any other field is set, requires full address + const hasAnyField = data.line1 || data.line2 || data.city || data.state || data.postal_code + // If any field has value, country and line1 must have values. + return !hasAnyField || (!!data.country && !!data.line1) + }, + { + message: 'Country and Address line 1 are required if any other field is provided.', + path: ['line1'], + } + ) + .refine((data) => !(!!data.line1 && !data.country), { + message: 'Please select a country', + path: ['country'], + }) + .refine((data) => !(!!data.country && !data.line1), { + message: 'Please provide an address line 1', + path: ['line1'], + }) + +export type BillingCustomerDataFormValues = z.infer + +export const BillingCustomerDataForm = ({ + form, + disabled = false, + className, +}: BillingCustomerDataFormProps) => { + const [showCountriesPopover, setShowCountriesPopover] = useState(false) + const [showTaxIDsPopover, setShowTaxIDsPopover] = useState(false) + + const onSelectTaxIdType = (name: string) => { + const selectedTaxIdOption = TAX_IDS.find((option) => option.name === name) + if (!selectedTaxIdOption) return + form.setValue('tax_id_type', selectedTaxIdOption.type) + form.setValue('tax_id_value', '') + form.setValue('tax_id_name', name) + } + + const onRemoveTaxId = () => { + form.setValue('tax_id_name', '', { shouldDirty: true }) + form.setValue('tax_id_type', '', { shouldDirty: true }) + form.setValue('tax_id_value', '', { shouldDirty: true }) + } + + const { tax_id_name } = form.watch() + const selectedTaxId = TAX_IDS.find((option) => option.name === tax_id_name) + + return ( +
+ ( + + + + + + + )} + /> + + ( + + + + + + + )} + /> + + ( + + + + + + + )} + /> + +
+ ( + + + + + + + + + + + + No country found. + + {COUNTRIES.map((country) => ( + { + form.setValue('country', country.code, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }) + setShowCountriesPopover(false) + }} + > + + {country.name} + + ))} + + + + + + + + )} + /> + ( + + + + + + + )} + /> +
+ +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> +
+ +
+ ( + + + + + + + + + + + + No tax ID found. + + {TAX_IDS.sort((a, b) => a.country.localeCompare(b.country)).map( + (option) => ( + { + onSelectTaxIdType(option.name) + setShowTaxIDsPopover(false) + }} + > + + {option.country} - {option.name} + + ) + )} + + + + + + + + )} + /> + + {selectedTaxId && ( +
+ ( + + + + + + )} + /> + +
+ )} +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataNewOrgDialog.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataNewOrgDialog.tsx new file mode 100644 index 00000000000..dd072469401 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/BillingCustomerDataNewOrgDialog.tsx @@ -0,0 +1,122 @@ +import { FocusTrap } from '@headlessui/react' +import { Pencil } from 'lucide-react' +import { useState } from 'react' + +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSectionSeparator, + DialogTitle, + DialogTrigger, + Form_Shadcn_ as Form, +} from 'ui' +import { BillingCustomerDataForm } from './BillingCustomerDataForm' +import { FormCustomerData, useBillingCustomerDataForm } from './useBillingCustomerDataForm' + +interface BillingCustomerDataNewOrgDialogProps { + onCustomerDataChange: (data: FormCustomerData) => void +} + +export const BillingCustomerDataNewOrgDialog = ({ + onCustomerDataChange, +}: BillingCustomerDataNewOrgDialogProps) => { + const [open, setOpen] = useState(false) + + const handleDialogClose = () => { + setOpen(false) + } + + const { form, handleSubmit, handleReset, isDirty } = useBillingCustomerDataForm({ + onCustomerDataChange, + }) + + const handleClose = () => { + handleReset() + handleDialogClose() + } + + const getAddressSummary = () => { + const { line1, city, state, country, billing_name } = form.getValues() + + if (!line1) return 'Optionally add a billing address' + + const parts = [billing_name, line1, city, state, country].filter(Boolean) + + return parts.join(', ') + } + + const isSubmitDisabled = !isDirty + + return ( +
+ + { + if (!value) handleDialogClose() + else setOpen(true) + }} + > + + + + + + Billing Address & Tax Id + + + {/* + [Joshen] There's something odd going on with using Dialog and RHF FormField here + where the focus keeps going to the Dialog when tabbing across the input fields, hence the FocusTrap + What's weirder is that once you've cycled the focus through all the inputs at least once, the focus + then no longer goes to the Dialog thereafter + FWIW it's likely happening across the dashboard whereever we're using Dialog and FormField + */} + +
+ { + e.preventDefault() + e.stopPropagation() + form.handleSubmit(handleSubmit)(e) + + // Only close the dialog if the form is valid + if (!Object.keys(form.formState.errors).length) { + handleDialogClose() + } + }} + > + + +
+ + +
+
+ + +
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.constants.ts similarity index 100% rename from apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts rename to apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.constants.ts diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.utils.ts similarity index 100% rename from apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts rename to apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.utils.ts diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm.ts b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm.ts new file mode 100644 index 00000000000..aef1e286377 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingCustomerData/useBillingCustomerDataForm.ts @@ -0,0 +1,131 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useEffect } from 'react' +import { useForm } from 'react-hook-form' + +import { BillingCustomerDataFormValues, BillingCustomerDataSchema } from './BillingCustomerDataForm' +import { TAX_IDS } from './TaxID.constants' +import { sanitizeTaxIdValue } from './TaxID.utils' +import { components } from 'api-types' + +export type FormCustomerData = { + address: components['schemas']['CustomerResponse']['address'] | undefined + billing_name: string + tax_id: components['schemas']['TaxIdResponse']['tax_id'] | null +} + +interface UseBillingAddressFormProps { + initialCustomerData?: Partial | null + onCustomerDataChange: (data: FormCustomerData) => void +} + +export function useBillingCustomerDataForm({ + initialCustomerData, + onCustomerDataChange, +}: UseBillingAddressFormProps) { + const form = useForm({ + resolver: zodResolver(BillingCustomerDataSchema), + defaultValues: { + billing_name: initialCustomerData?.billing_name || '', + line1: initialCustomerData?.line1 || '', + line2: initialCustomerData?.line2 || '', + city: initialCustomerData?.city || '', + state: initialCustomerData?.state || '', + postal_code: initialCustomerData?.postal_code || '', + country: initialCustomerData?.country || '', + tax_id_type: initialCustomerData?.tax_id_type || '', + tax_id_value: initialCustomerData?.tax_id_value || '', + tax_id_name: initialCustomerData?.tax_id_name || '', + }, + }) + + // Update form when initialAddress changes + useEffect(() => { + if (initialCustomerData) { + form.reset({ + billing_name: initialCustomerData.billing_name || '', + line1: initialCustomerData.line1 || '', + line2: initialCustomerData.line2 || '', + city: initialCustomerData.city || '', + state: initialCustomerData.state || '', + postal_code: initialCustomerData.postal_code || '', + country: initialCustomerData.country || '', + tax_id_type: initialCustomerData?.tax_id_type || '', + tax_id_value: initialCustomerData?.tax_id_value || '', + tax_id_name: initialCustomerData?.tax_id_name || '', + }) + } + }, [initialCustomerData]) + + const handleSubmit = async (values: BillingCustomerDataFormValues) => { + const trimmedValues = Object.entries(values).reduce((acc, [key, value]) => { + acc[key as keyof BillingCustomerDataFormValues] = + typeof value === 'string' ? value.trim() : value + return acc + }, {} as BillingCustomerDataFormValues) + + const addressPayload = !trimmedValues.line1 ? null : trimmedValues + + const selectedTaxId = TAX_IDS.find((option) => option.name === values.tax_id_name) + + onCustomerDataChange({ + address: addressPayload + ? { + line1: trimmedValues.line1!, + line2: trimmedValues.line2, + city: trimmedValues.city, + state: trimmedValues.state, + postal_code: trimmedValues.postal_code, + country: trimmedValues.country!, + } + : undefined, + billing_name: trimmedValues.billing_name, + tax_id: + selectedTaxId && values.tax_id_type?.length && values.tax_id_value?.length + ? { + type: values.tax_id_type, + value: sanitizeTaxIdValue({ + value: values.tax_id_value, + name: form.getValues().tax_id_name, + }), + country: selectedTaxId?.countryIso2, + } + : null, + }) + } + + const handleReset = () => { + if (initialCustomerData) { + form.reset({ + billing_name: initialCustomerData.billing_name || '', + line1: initialCustomerData.line1 || '', + line2: initialCustomerData.line2 || '', + city: initialCustomerData.city || '', + state: initialCustomerData.state || '', + postal_code: initialCustomerData.postal_code || '', + country: initialCustomerData.country || '', + tax_id_type: initialCustomerData?.tax_id_type || '', + tax_id_value: initialCustomerData?.tax_id_value || '', + tax_id_name: initialCustomerData?.tax_id_name || '', + }) + } else { + form.reset({ + line1: '', + line2: '', + city: '', + state: '', + postal_code: '', + country: '', + tax_id_type: '', + tax_id_value: '', + tax_id_name: '', + }) + } + } + + return { + form, + handleSubmit, + handleReset, + isDirty: form.formState.isDirty, + } +} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx index 0ee67a41a87..8af7d72ade4 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingEmail.tsx @@ -18,7 +18,10 @@ import { FormSection, FormSectionContent } from 'components/ui/Forms/FormSection import NoPermission from 'components/ui/NoPermission' import { useOrganizationCustomerProfileQuery } from 'data/organizations/organization-customer-profile-query' import { useOrganizationUpdateMutation } from 'data/organizations/organization-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { + useAsyncCheckProjectPermissions, + useCheckPermissions, +} from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { FormMessage_Shadcn_, Input_Shadcn_ } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' @@ -43,10 +46,8 @@ const BillingEmail = () => { const { name, billing_email } = selectedOrganization ?? {} const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') - const canReadBillingEmail = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { isSuccess: isPermissionsLoaded, can: canReadBillingEmail } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const { data: billingCustomer, isLoading: loadingBillingCustomer } = useOrganizationCustomerProfileQuery({ slug }, { enabled: canReadBillingEmail }) @@ -107,7 +108,7 @@ const BillingEmail = () => {
- {!canReadBillingEmail ? ( + {isPermissionsLoaded && !canReadBillingEmail ? ( ) : (
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx index 0c78de65c91..3109fc69ada 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/BillingSettings.tsx @@ -5,19 +5,18 @@ import { ScaffoldDivider, ScaffoldTitle, } from 'components/layouts/Scaffold' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { cn } from 'ui' -import { useOrgSubscriptionQuery } from '../../../../data/subscriptions/org-subscription-query' import InvoicesSection from '../InvoicesSettings/InvoicesSection' -import BillingAddress from './BillingAddress/BillingAddress' import BillingBreakdown from './BillingBreakdown/BillingBreakdown' +import { BillingCustomerData } from './BillingCustomerData/BillingCustomerData' import BillingEmail from './BillingEmail' import CostControl from './CostControl/CostControl' import CreditBalance from './CreditBalance' import PaymentMethods from './PaymentMethods/PaymentMethods' import Subscription from './Subscription/Subscription' -import TaxID from './TaxID/TaxID' const BillingSettings = () => { const { @@ -109,13 +108,7 @@ const BillingSettings = () => { - - - - - - - + )} diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx index 4b63791d318..fc8b417481f 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CostControl/CostControl.tsx @@ -14,7 +14,7 @@ import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useFlag } from 'hooks/ui/useFlag' import { BASE_PATH } from 'lib/constants' import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings' @@ -28,10 +28,8 @@ const CostControl = ({}: CostControlProps) => { const { slug } = useParams() const { resolvedTheme } = useTheme() - const canReadSubscriptions = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const snap = useOrgSettingsPageStateSnapshot() const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') @@ -85,7 +83,7 @@ const CostControl = ({}: CostControlProps) => {
- {!canReadSubscriptions ? ( + {isPermissionsLoaded && !canReadSubscriptions ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx index ab0cfee3adc..11e8d02f1be 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/CreditBalance.tsx @@ -11,7 +11,7 @@ import { FormPanel } from 'components/ui/Forms/FormPanel' import { FormSection, FormSectionContent } from 'components/ui/Forms/FormSection' import NoPermission from 'components/ui/NoPermission' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useFlag } from 'hooks/ui/useFlag' import { CreditTopUp } from './CreditTopUp' @@ -19,10 +19,8 @@ const CreditBalance = () => { const { slug } = useParams() const creditTopUpEnabled = useFlag('creditTopUp') - const canReadSubscriptions = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const { data: subscription, @@ -54,7 +52,7 @@ const CreditBalance = () => {
- {!canReadSubscriptions ? ( + {isPermissionsLoaded && !canReadSubscriptions ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx index 788707ffb5b..15b85fa5ecf 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx @@ -19,7 +19,10 @@ import PartnerManagedResource from 'components/ui/PartnerManagedResource' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrganizationPaymentMethodsQuery } from 'data/organizations/organization-payment-methods-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { + useAsyncCheckProjectPermissions, + useCheckPermissions, +} from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { getURL } from 'lib/helpers' import { Alert, Button } from 'ui' @@ -43,10 +46,8 @@ const PaymentMethods = () => { isSuccess, } = useOrganizationPaymentMethodsQuery({ slug }) - const canReadPaymentMethods = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.payment_methods' - ) + const { isSuccess: isPermissionsLoaded, can: canReadPaymentMethods } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.payment_methods') const canUpdatePaymentMethods = useCheckPermissions( PermissionAction.BILLING_WRITE, 'stripe.payment_methods' @@ -73,7 +74,7 @@ const PaymentMethods = () => { installationId: selectedOrganization?.partner_id, }} /> - ) : !canReadPaymentMethods ? ( + ) : isPermissionsLoaded && !canReadPaymentMethods ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx index 89e9f0af6e6..a153e2cd2f8 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx @@ -26,9 +26,8 @@ import DowngradeModal from './DowngradeModal' import { EnterpriseCard } from './EnterpriseCard' import ExitSurveyModal from './ExitSurveyModal' import MembersExceedLimitModal from './MembersExceedLimitModal' -import SubscriptionPlanUpdateDialog from './SubscriptionPlanUpdateDialog' +import { SubscriptionPlanUpdateDialog } from './SubscriptionPlanUpdateDialog' import UpgradeSurveyModal from './UpgradeModal' -import { PROJECT_STATUS } from 'lib/constants' const PlanUpdateSidePanel = () => { const router = useRouter() @@ -305,7 +304,6 @@ const PlanUpdateSidePanel = () => { billingPartner={billingPartner} subscription={subscription} projects={orgProjects} - slug={slug} currentPlanMeta={{ ...availablePlans.find((p) => p.id === subscription?.plan?.id), features: diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx index 919f88ef2c0..3c31b9a18c1 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/Subscription.tsx @@ -11,7 +11,7 @@ import AlertError from 'components/ui/AlertError' import NoPermission from 'components/ui/NoPermission' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import { useFlag } from 'hooks/ui/useFlag' import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings' import { Alert, Button } from 'ui' @@ -25,10 +25,8 @@ const Subscription = () => { const snap = useOrgSettingsPageStateSnapshot() const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') - const canReadSubscriptions = useCheckPermissions( - PermissionAction.BILLING_READ, - 'stripe.subscriptions' - ) + const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = + useAsyncCheckProjectPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') const { data: subscription, @@ -61,7 +59,7 @@ const Subscription = () => {
- {!canReadSubscriptions ? ( + {isPermissionsLoaded && !canReadSubscriptions ? ( ) : ( <> diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx index 51d91402ff9..96349509809 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx @@ -1,5 +1,5 @@ import { useQueryClient } from '@tanstack/react-query' -import { Check, ExternalLink, InfoIcon } from 'lucide-react' +import { Check, InfoIcon } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import tweets from 'shared-data/tweets' @@ -10,29 +10,17 @@ import AlertError from 'components/ui/AlertError' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { organizationKeys } from 'data/organizations/keys' import { OrganizationBillingSubscriptionPreviewResponse } from 'data/organizations/organization-billing-subscription-preview' +import { ProjectInfo } from 'data/projects/projects-query' import { useOrgSubscriptionUpdateMutation } from 'data/subscriptions/org-subscription-update-mutation' import { SubscriptionTier } from 'data/subscriptions/types' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { PRICING_TIER_PRODUCT_IDS, PROJECT_STATUS } from 'lib/constants' import { formatCurrency } from 'lib/helpers' -import { - Badge, - Button, - Card, - CardContent, - Dialog, - DialogContent, - HoverCard, - HoverCardContent, - HoverCardTrigger, - Table, - TableBody, - TableCell, - TableRow, -} from 'ui' -import { InfoTooltip } from 'ui-patterns/info-tooltip' -import PaymentMethodSelection from './PaymentMethodSelection' -import { ProjectInfo } from 'data/projects/projects-query' +import { Badge, Button, Dialog, DialogContent, Table, TableBody, TableCell, TableRow } from 'ui' import { Admonition } from 'ui-patterns' +import { InfoTooltip } from 'ui-patterns/info-tooltip' +import { BillingCustomerDataExistingOrgDialog } from '../BillingCustomerData/BillingCustomerDataExistingOrgDialog' +import PaymentMethodSelection from './PaymentMethodSelection' const getRandomTweet = () => { const filteredTweets = tweets.filter((it) => it.text.length < 180) @@ -71,12 +59,11 @@ interface Props { billingPartner?: string selectedOrganization: any subscription: any - slug?: string currentPlanMeta: any projects: ProjectInfo[] } -const SubscriptionPlanUpdateDialog = ({ +export const SubscriptionPlanUpdateDialog = ({ selectedTier, onClose, subscriptionPlanMeta, @@ -89,11 +76,11 @@ const SubscriptionPlanUpdateDialog = ({ billingPartner, selectedOrganization, subscription, - slug, currentPlanMeta, projects, }: Props) => { const queryClient = useQueryClient() + const { slug } = useSelectedOrganization() ?? {} const [selectedPaymentMethod, setSelectedPaymentMethod] = useState() const [testimonialTweet, setTestimonialTweet] = useState(getRandomTweet()) @@ -167,6 +154,24 @@ const SubscriptionPlanUpdateDialog = ({ }) : [] + // Calculate remaining days in current billing cycle + const now = Math.floor(Date.now() / 1000) // current time in seconds + const remainingSeconds = subscription?.current_period_end - now + const totalSeconds = subscription?.current_period_end - subscription?.current_period_start + const remainingRatio = remainingSeconds / totalSeconds + + // Calculate prorated credit for current plan + const currentPlanMonthlyPrice = currentPlanMeta?.price ?? 0 + const proratedCredit = currentPlanMonthlyPrice * remainingRatio + + // Calculate new plan cost + const newPlanCost = subscriptionPlanMeta?.priceMonthly ?? 0 + + const customerBalance = ((subscription?.customer_balance ?? 0) / 100) * -1 + + // Calculate total charge (new plan - prorated credit) + const totalCharge = Math.max(0, newPlanCost - proratedCredit - customerBalance) + return ( - - - {(() => { - // Calculate remaining days in current billing cycle - const now = Math.floor(Date.now() / 1000) // current time in seconds - const remainingSeconds = subscription?.current_period_end - now - const totalSeconds = - subscription?.current_period_end - subscription?.current_period_start - const remainingRatio = remainingSeconds / totalSeconds - - // Calculate prorated credit for current plan - const currentPlanMonthlyPrice = currentPlanMeta?.price ?? 0 - const proratedCredit = currentPlanMonthlyPrice * remainingRatio - - // Calculate new plan cost - const newPlanCost = subscriptionPlanMeta?.priceMonthly ?? 0 - - const customerBalance = ((subscription?.customer_balance ?? 0) / 100) * -1 - - // Calculate total charge (new plan - prorated credit) - const totalCharge = Math.max( - 0, - newPlanCost - proratedCredit - customerBalance - ) - - return ( - <> - - - {subscriptionPlanMeta?.name} Plan - - New - - - - {formatCurrency(newPlanCost)} - - - {subscription?.plan?.id !== 'free' && ( - - - Unused Time on {subscription?.plan?.name} Plan - - Your previous plan was charged upfront, so a plan change will - prorate any unused time in credits. If the prorated credits - exceed the new plan charge, the excessive credits are added to - your organization for future use. - - - - -{formatCurrency(proratedCredit)} - - - )} - - {/* Ignore rare case with negative balance (debt) */} - {customerBalance > 0 && ( - - - Credits - - Credits will be used first before charging your card. - - - - {formatCurrency(customerBalance)} - - - )} - - Charge today - - {formatCurrency(totalCharge)} - - - {subscription?.plan?.id !== 'free' && ( - - - + current cycle usage - - Changing your plan resets your billing cycle. If your previous - billing cycle still has any outstanding usage charges, they will - be added to your total charge today. - - - - - View spend - - - - )} - - ) - })()} - -
- - - - - - Monthly invoice estimate is{' '} - {formatCurrency( - Math.round( - subscriptionPreview?.breakdown.reduce( - (prev: number, cur) => prev + cur.total_price, - 0 - ) ?? 0 - ) - )} - - - - -

Your new monthly invoice

- - {subscriptionPreviewError && ( - - )} - - {subscriptionPreviewIsLoading && ( -
- Estimating monthly costs... - - - +
+
+
+ {subscriptionPlanMeta?.name} Plan + + New + +
+
{formatCurrency(newPlanCost)}
+
+ {subscription?.plan?.id !== 'free' && ( +
+
+ Unused Time on {subscription?.plan?.name} Plan + + Your previous plan was charged upfront, so a plan change will prorate + any unused time in credits. If the prorated credits exceed the new plan + charge, the excessive credits are added to your organization for future + use. +
- )} +
+ -{formatCurrency(proratedCredit)} +
+
+ )} - {subscriptionPreviewInitialized && ( - <> - - - {/* Non-compute items and Projects list */} - {(() => { - // Combine all compute-related projects - const computeItems = - subscriptionPreview?.breakdown?.filter( - (item) => - item.description?.toLowerCase().includes('compute') && - item.breakdown?.length > 0 - ) || [] + {/* Ignore rare case with negative balance (debt) */} + {customerBalance > 0 && ( +
+
+ Credits + + Credits will be used first before charging your card. + +
+
+ {formatCurrency(customerBalance)} +
+
+ )} +
+
Charge today
+
+ {formatCurrency(totalCharge)} + {subscription?.plan?.id !== 'free' && ( + <> + {' '} + + + current spend + + + )} +
+
+
+
+ Monthly invoice estimate + +
+

Your new monthly invoice

+

+ Each paid project runs on a dedicated 24/7 server. First project uses + Compute Credits; additional ones cost $10+/month regardless of usage.{' '} + + Learn more + + . +

- const computeCreditsItem = - subscriptionPreview?.breakdown?.find((item) => - item.description.startsWith('Compute Credits') - ) ?? null + {subscriptionPreviewError && ( + + )} - const planItem = subscriptionPreview?.breakdown?.find((item) => - item.description?.toLowerCase().includes('plan') - ) + {subscriptionPreviewIsLoading && ( +
+ Estimating monthly costs... + + + +
+ )} - const allProjects = computeItems.flatMap((item) => - item.breakdown.map((project) => ({ - ...project, - computeType: item.description, - computeCosts: Math.round( - item.total_price / item.breakdown.length - ), - })) - ) + {subscriptionPreviewInitialized && ( + <> +
+ + {/* Non-compute items and Projects list */} + {(() => { + // Combine all compute-related projects + const computeItems = + subscriptionPreview?.breakdown?.filter( + (item) => + item.description?.toLowerCase().includes('compute') && + item.breakdown?.length > 0 + ) || [] - const otherItems = - subscriptionPreview?.breakdown?.filter( - (item) => - !item.description?.toLowerCase().includes('compute') && - !item.description?.toLowerCase().includes('plan') - ) || [] + const computeCreditsItem = + subscriptionPreview?.breakdown?.find((item) => + item.description.startsWith('Compute Credits') + ) ?? null - const content = ( - <> - {/* Combined projects section */} - {allProjects.length > 0 && ( - <> - - - {planItem?.description} - - - {formatCurrency(planItem?.total_price)} - - + const planItem = subscriptionPreview?.breakdown?.find( + (item) => item.description?.toLowerCase().includes('plan') + ) - - - Compute - -

- Each project on a paid plan is a dedicated server - running 24/7 with no pausing. The first project is - covered by Compute Credits. Additional projects will - incur compute costs starting at $10/month, - independent of activity. See{' '} - + item.breakdown.map((project) => ({ + ...project, + computeType: item.description, + computeCosts: Math.round( + item.total_price / item.breakdown.length + ), + })) + ) + + const otherItems = + subscriptionPreview?.breakdown?.filter( + (item) => + !item.description?.toLowerCase().includes('compute') && + !item.description?.toLowerCase().includes('plan') + ) || [] + + const content = ( + <> + {/* Combined projects section */} + {allProjects.length > 0 && ( + <> + + + {planItem?.description} + + + {formatCurrency(planItem?.total_price)} + + + + + + Compute + + + {formatCurrency( + computeItems.reduce( + (sum: number, item) => sum + item.total_price, + 0 + ) + (computeCreditsItem?.total_price ?? 0) + )} + + + {/* Show first 3 projects */} + {allProjects.map((project) => ( + - docs - - . -

- {subscription?.plan?.id === 'free' && ( - <> -

- Mixing paid and non-paid projects in a single - organization is not possible. If you want - projects to be on the Free Plan, use self-serve - project transfers. -

-
- - -
- + + {project.project_name} ({project.computeType}) |{' '} + {formatCurrency(project.computeCosts)} + +
+ ))} + {computeCreditsItem && ( + + + Compute Credits |{' '} + {formatCurrency(computeCreditsItem.total_price)} + + )} - - - - {formatCurrency( - computeItems.reduce( - (sum: number, item) => sum + item.total_price, - 0 - ) + (computeCreditsItem?.total_price ?? 0) - )} - - - {/* Show first 3 projects */} - {allProjects.map((project) => ( - - - {project.project_name} ({project.computeType}) |{' '} - {formatCurrency(project.computeCosts)} - - - ))} - {computeCreditsItem && ( - - - Compute Credits |{' '} - {formatCurrency(computeCreditsItem.total_price)} - - + + )} + + {/* Non-compute items */} + {otherItems.map((item) => ( + + +
+ {item.description ?? 'Unknown'} + {item.breakdown.length > 0 && ( + +

Projects using {item.description}:

+
    + {item.breakdown.map((breakdown) => ( +
  • + {breakdown.project_name} +
  • + ))} +
+
+ )} +
+
+ + {formatCurrency(item.total_price)} + +
+ ))} + + ) + return content + })()} + + + + Total per month (excluding other usage) + + + {formatCurrency( + Math.round( + subscriptionPreview?.breakdown?.reduce( + (prev, cur) => prev + cur.total_price, + 0 + ) ?? 0 + ) ?? 0 )} - - )} - - {/* Non-compute items */} - {otherItems.map((item) => ( - - -
- {item.description ?? 'Unknown'} - {item.breakdown.length > 0 && ( - -

Projects using {item.description}:

-
    - {item.breakdown.map((breakdown) => ( -
  • - {breakdown.project_name} -
  • - ))} -
-
- )} -
-
- - {formatCurrency(item.total_price)} - -
- ))} - - ) - return content - })()} - - - - Total per month (excluding other usage) - - - {formatCurrency( - Math.round( - subscriptionPreview?.breakdown?.reduce( - (prev, cur) => prev + cur.total_price, - 0 - ) ?? 0 - ) ?? 0 - )} - - -
-
- - )} - - + + + + + + )} +
+ +
+
+ {formatCurrency( + Math.round( + subscriptionPreview?.breakdown.reduce( + (prev: number, cur) => prev + cur.total_price, + 0 + ) ?? 0 + ) + )} +
+
+
)}
-
+
{!billingViaPartner ? ( -
+
+ )} -
- @@ -666,5 +586,3 @@ const SubscriptionPlanUpdateDialog = ({ ) } - -export default SubscriptionPlanUpdateDialog diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx deleted file mode 100644 index c374abacd19..00000000000 --- a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import { zodResolver } from '@hookform/resolvers/zod' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useEffect } from 'react' -import { useForm } from 'react-hook-form' -import { toast } from 'sonner' -import * as z from 'zod' - -import { useParams } from 'common' -import { - ScaffoldSection, - ScaffoldSectionContent, - ScaffoldSectionDetail, -} from 'components/layouts/Scaffold' -import AlertError from 'components/ui/AlertError' -import { FormActions } from 'components/ui/Forms/FormActions' -import NoPermission from 'components/ui/NoPermission' -import Panel from 'components/ui/Panel' -import PartnerManagedResource from 'components/ui/PartnerManagedResource' -import ShimmeringLoader from 'components/ui/ShimmeringLoader' -import { useOrganizationTaxIdQuery } from 'data/organizations/organization-tax-id-query' -import { useOrganizationTaxIdUpdateMutation } from 'data/organizations/organization-tax-id-update-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { - Button, - FormControl_Shadcn_, - FormField_Shadcn_, - FormItem_Shadcn_, - Form_Shadcn_, - Input_Shadcn_, - SelectContent_Shadcn_, - SelectGroup_Shadcn_, - SelectItem_Shadcn_, - SelectTrigger_Shadcn_, - SelectValue_Shadcn_, - Select_Shadcn_, -} from 'ui' -import { TAX_IDS } from './TaxID.constants' -import { checkTaxIdEqual, sanitizeTaxIdValue } from './TaxID.utils' -import { X } from 'lucide-react' - -const TaxID = () => { - const { slug } = useParams() - const selectedOrganization = useSelectedOrganization() - - const { data: taxId, error, isLoading, isSuccess, isError } = useOrganizationTaxIdQuery({ slug }) - const { mutate: updateTaxId, isLoading: isUpdating } = useOrganizationTaxIdUpdateMutation({ - onSuccess: () => { - toast.success('Successfully updated tax id') - }, - }) - - const FormSchema = z.object({ type: z.string(), value: z.string(), name: z.string() }) - const form = useForm>({ - mode: 'onBlur', - reValidateMode: 'onBlur', - resolver: zodResolver(FormSchema), - defaultValues: { type: '', value: '', name: '' }, - }) - - const formattedTaxId = taxId - ? { - type: taxId.type, - value: taxId.value, - name: - TAX_IDS.find( - (option) => option.type === taxId.type && option.countryIso2 === taxId.country - )?.name ?? '', - } - : { type: '', value: '', name: '' } - - const hasChanges = !checkTaxIdEqual(form.getValues(), formattedTaxId) - const canReadTaxId = useCheckPermissions(PermissionAction.BILLING_READ, 'stripe.tax_ids') - const canUpdateTaxId = useCheckPermissions(PermissionAction.BILLING_WRITE, 'stripe.tax_ids') - - useEffect(() => { - if (isSuccess && formattedTaxId) { - form.setValue('type', formattedTaxId.type) - form.setValue('value', formattedTaxId.value) - form.setValue('name', formattedTaxId.name) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccess]) - - const onSaveTaxId = async () => { - if (!slug) return console.error('Slug is required') - - const { type, value } = form.getValues() - - if (type?.length && !value?.length) { - form.setError('value', { message: 'Value is required' }) - return - } - - updateTaxId({ - slug, - taxId: - type?.length && value?.length - ? { - type: type, - value: sanitizeTaxIdValue({ value, name: form.getValues().name }), - country: selectedTaxId?.countryIso2, - } - : null, - }) - } - - const onResetTaxIds = () => { - form.setValue('type', formattedTaxId.type) - form.setValue('value', formattedTaxId.value) - form.setValue('name', formattedTaxId.name) - } - - const onSelectTaxIdType = (name: string) => { - const selectedTaxIdOption = TAX_IDS.find((option) => option.name === name) - if (!selectedTaxIdOption) return - form.setValue('type', selectedTaxIdOption.type) - form.setValue('value', '') - form.setValue('name', name) - } - - const onRemoveTaxId = () => { - form.reset() - } - - const { name } = form.watch() - - const selectedTaxId = TAX_IDS.find((option) => option.name === name) - - return ( - - -
-

Tax ID

-

- Add a tax ID to your invoices. Changes only apply to future invoices. -

-

- Make sure the tax ID looks exactly like the placeholder text. -

-
-
- - {selectedOrganization?.managed_by !== undefined && - selectedOrganization?.managed_by !== 'supabase' ? ( - - ) : !canReadTaxId ? ( - - ) : ( - <> - {isLoading && ( -
- - - -
- )} - - {isError && ( - - )} - - {isSuccess && ( - onResetTaxIds()} - helper={ - !canUpdateTaxId - ? "You need additional permissions to manage this organization's tax ID" - : undefined - } - /> - } - > - - onSaveTaxId())} - > - ( - - - onSelectTaxIdType(value)} - > - - - - - - {TAX_IDS.sort((a, b) => a.country.localeCompare(b.country)).map( - (option) => ( - - {option.country} - {option.name} - - ) - )} - - - - - - )} - /> - - {selectedTaxId && ( -
- ( - - - - - - )} - /> - -
- )} - -
-
- )} - - )} -
-
- ) -} - -export default TaxID diff --git a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSection.tsx b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSection.tsx index 35d8c06199f..673df43cff6 100644 --- a/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSection.tsx +++ b/apps/studio/components/interfaces/Organization/InvoicesSettings/InvoicesSection.tsx @@ -5,11 +5,14 @@ import { ScaffoldSectionDetail, } from 'components/layouts/Scaffold' import NoPermission from 'components/ui/NoPermission' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' import InvoicesSettings from './InvoicesSettings' const InvoicesSection = () => { - const canReadInvoices = useCheckPermissions(PermissionAction.BILLING_READ, 'stripe.subscriptions') + const { isSuccess: isPermissionsLoaded, can: canReadInvoices } = useAsyncCheckProjectPermissions( + PermissionAction.BILLING_READ, + 'stripe.subscriptions' + ) return ( @@ -24,7 +27,7 @@ const InvoicesSection = () => {
- {!canReadInvoices ? ( + {isPermissionsLoaded && !canReadInvoices ? ( ) : ( diff --git a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index 4542010b18d..08bb6eeb398 100644 --- a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -1,6 +1,7 @@ import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js' import type { PaymentMethod } from '@stripe/stripe-js' import { useQueryClient } from '@tanstack/react-query' +import _ from 'lodash' import { Edit2, ExternalLink, HelpCircle } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' @@ -16,6 +17,7 @@ import { invalidateOrganizationsQuery, useOrganizationsQuery, } from 'data/organizations/organizations-query' +import { useProjectsQuery } from 'data/projects/projects-query' import { BASE_PATH, PRICING_TIER_LABELS_ORG } from 'lib/constants' import { getURL } from 'lib/helpers' import { useProfile } from 'lib/profile' @@ -34,9 +36,9 @@ import { TooltipContent, TooltipTrigger, } from 'ui' -import { useProjectsQuery } from 'data/projects/projects-query' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import _ from 'lodash' +import { BillingCustomerDataNewOrgDialog } from '../BillingSettings/BillingCustomerData/BillingCustomerDataNewOrgDialog' +import { FormCustomerData } from '../BillingSettings/BillingCustomerData/useBillingCustomerDataForm' const ORG_KIND_TYPES = { PERSONAL: 'Personal', @@ -101,6 +103,8 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { const [isOrgCreationConfirmationModalVisible, setIsOrgCreationConfirmationModalVisible] = useState(false) + const [customerData, setCustomerData] = useState(null) + const [formState, setFormState] = useState({ plan: 'FREE', name: '', @@ -157,7 +161,8 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { router.push(`/new/${org.slug}?projectName=${prefilledProjectName}`) } }, - onError: () => { + onError: (data) => { + toast.error(`Failed to create organization: ${data.message}`) resetPaymentMethod() setNewOrgLoading(false) }, @@ -182,6 +187,9 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { | 'tier_enterprise', ...(formState.kind == 'COMPANY' ? { size: formState.size } : {}), payment_method: paymentMethodId, + billing_name: dbTier === 'FREE' ? undefined : customerData?.billing_name, + address: dbTier === 'FREE' ? undefined : customerData?.address, + tax_id: dbTier === 'FREE' ? undefined : customerData?.tax_id ?? undefined, }) } @@ -462,6 +470,21 @@ const NewOrgForm = ({ onPaymentMethodReset }: NewOrgFormProps) => { )} + {formState.plan !== 'FREE' && ( + +
+
+ + Billing Address + +
+
+ +
+
+
+ )} + {formState.plan !== 'FREE' && ( {paymentMethod?.card !== undefined ? ( diff --git a/apps/studio/data/organizations/organization-create-mutation.ts b/apps/studio/data/organizations/organization-create-mutation.ts index 77842d34cea..acd8c924c12 100644 --- a/apps/studio/data/organizations/organization-create-mutation.ts +++ b/apps/studio/data/organizations/organization-create-mutation.ts @@ -5,6 +5,7 @@ import { handleError, post } from 'data/fetchers' import { permissionKeys } from 'data/permissions/keys' import type { ResponseError } from 'types' import { organizationKeys } from './keys' +import type { components } from 'data/api' export type OrganizationCreateVariables = { name: string @@ -12,6 +13,9 @@ export type OrganizationCreateVariables = { size?: string tier: 'tier_payg' | 'tier_pro' | 'tier_free' | 'tier_team' | 'tier_enterprise' payment_method?: string + billing_name?: string + address?: components['schemas']['CreateOrganizationBody']['address'] + tax_id?: components['schemas']['CreateOrganizationBody']['tax_id'] } export async function createOrganization({ @@ -20,15 +24,20 @@ export async function createOrganization({ size, tier, payment_method, + address, + billing_name, + tax_id, }: OrganizationCreateVariables) { const { data, error } = await post('/platform/organizations', { body: { name, - // @ts-ignore [Joshen] Generated API spec is wrong kind, size, tier, payment_method, + address, + billing_name, + tax_id, }, }) diff --git a/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts b/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts index ca7af88b5d8..ae62b58a1df 100644 --- a/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts +++ b/apps/studio/data/organizations/organization-customer-profile-update-mutation.ts @@ -7,13 +7,15 @@ import type { ResponseError } from 'types' import { organizationKeys } from './keys' export type OrganizationCustomerProfileUpdateVariables = { - slug: string + slug?: string address?: components['schemas']['CustomerResponse']['address'] + billing_name: string } export async function updateOrganizationCustomerProfile({ slug, address, + billing_name, }: OrganizationCustomerProfileUpdateVariables) { if (!slug) return console.error('Slug is required') @@ -26,7 +28,7 @@ export async function updateOrganizationCustomerProfile({ slug, }, }, - body: { address }, + body: { address: address != null ? address : undefined, billing_name }, }) if (error) throw handleError(error) return data @@ -56,13 +58,14 @@ export const useOrganizationCustomerProfileUpdateMutation = ({ OrganizationCustomerProfileUpdateVariables >((vars) => updateOrganizationCustomerProfile(vars), { async onSuccess(data, variables, context) { - const { address, slug } = variables + const { address, slug, billing_name } = variables // We do not invalidate here as GET endpoint data is stale for 1-2 seconds, so we handle state manually queryClient.setQueriesData(organizationKeys.customerProfile(slug), (prev: any) => { if (!prev) return prev return { ...prev, + billing_name, address, } }) diff --git a/apps/studio/data/organizations/organization-tax-id-query.ts b/apps/studio/data/organizations/organization-tax-id-query.ts index 6f47c864250..e672b0611aa 100644 --- a/apps/studio/data/organizations/organization-tax-id-query.ts +++ b/apps/studio/data/organizations/organization-tax-id-query.ts @@ -20,14 +20,10 @@ export async function getOrganizationTaxId( const { data, error } = await get(`/platform/organizations/{slug}/tax-ids`, { params: { path: { slug } }, signal, - headers: { - Version: '2', - }, }) if (error) throw handleError(error) - // @ts-ignore wrong typing due to mgmt api versioning - return (data as components['schemas']['TaxIdV2Response']).tax_id + return (data as components['schemas']['TaxIdResponse']).tax_id } export type OrganizationTaxIdData = Awaited> diff --git a/apps/studio/data/organizations/organization-tax-id-update-mutation.ts b/apps/studio/data/organizations/organization-tax-id-update-mutation.ts index 0af1701d696..14bec45b564 100644 --- a/apps/studio/data/organizations/organization-tax-id-update-mutation.ts +++ b/apps/studio/data/organizations/organization-tax-id-update-mutation.ts @@ -6,7 +6,7 @@ import type { ResponseError } from 'types' import { organizationKeys } from './keys' export type OrganizationTaxIdUpdateVariables = { - slug: string + slug?: string taxId: { type: string; value: string; country?: string } | null } diff --git a/apps/studio/hooks/misc/useCheckPermissions.ts b/apps/studio/hooks/misc/useCheckPermissions.ts index bbff7a730b1..3c9e5597d81 100644 --- a/apps/studio/hooks/misc/useCheckPermissions.ts +++ b/apps/studio/hooks/misc/useCheckPermissions.ts @@ -3,11 +3,11 @@ import jsonLogic from 'json-logic-js' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { usePermissionsQuery } from 'data/permissions/permissions-query' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { IS_PLATFORM } from 'lib/constants' import type { Permission } from 'types' import { useSelectedOrganization } from './useSelectedOrganization' import { useSelectedProject } from './useSelectedProject' -import { useProjectDetailQuery } from 'data/projects/project-detail-query' const toRegexpString = (actionOrResource: string) => `^${actionOrResource.replace('.', '\\.').replace('%', '.*')}$` @@ -80,12 +80,11 @@ export function useGetProjectPermissions( projectRefOverride?: string, enabled = true ) { - const permissionsResult = usePermissionsQuery({ + const { data, isLoading, isSuccess } = usePermissionsQuery({ enabled: permissionsOverride === undefined && enabled, }) - const permissions = - permissionsOverride === undefined ? permissionsResult.data : permissionsOverride + const permissions = permissionsOverride === undefined ? data : permissionsOverride const organizationResult = useSelectedOrganization({ enabled: organizationSlugOverride === undefined && enabled, @@ -109,7 +108,8 @@ export function useGetProjectPermissions( permissions, organizationSlug, projectRef, - isLoading: permissionsResult.isLoading, + isLoading, + isSuccess, } } @@ -193,21 +193,25 @@ export function useAsyncCheckProjectPermissions( organizationSlug: _organizationSlug, projectRef: _projectRef, isLoading: isPermissionsLoading, + isSuccess: isPermissionsSuccess, } = useGetProjectPermissions(permissions, organizationSlug, projectRef, isLoggedIn) if (!isLoggedIn) return { - isLoading: false, + isLoading: true, + isSuccess: false, can: false, } if (!IS_PLATFORM) return { isLoading: false, + isSuccess: true, can: true, } return { isLoading: isPermissionsLoading, + isSuccess: isPermissionsSuccess, can: doPermissionsCheck(allPermissions, action, resource, data, _organizationSlug, _projectRef), } } diff --git a/apps/studio/pages/org/[slug]/billing.tsx b/apps/studio/pages/org/[slug]/billing.tsx index c0abd1ebcfc..0684fbb3326 100644 --- a/apps/studio/pages/org/[slug]/billing.tsx +++ b/apps/studio/pages/org/[slug]/billing.tsx @@ -1,13 +1,10 @@ +import { useEffect } from 'react' + import { useParams } from 'common' import { BillingSettings } from 'components/interfaces/Organization' -import AppLayout from 'components/layouts/AppLayout/AppLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import OrganizationLayout from 'components/layouts/OrganizationLayout' import OrganizationSettingsLayout from 'components/layouts/ProjectLayout/OrganizationSettingsLayout' -import { Loading } from 'components/ui/Loading' -import { usePermissionsQuery } from 'data/permissions/permissions-query' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { useEffect } from 'react' import { ORG_SETTINGS_PANEL_KEYS, useOrgSettingsPageStateSnapshot, @@ -17,8 +14,6 @@ import type { NextPageWithLayout } from 'types' const OrgBillingSettings: NextPageWithLayout = () => { const { panel } = useParams() const snap = useOrgSettingsPageStateSnapshot() - const { isLoading: isLoadingPermissions } = usePermissionsQuery() - const selectedOrganization = useSelectedOrganization() useEffect(() => { const allowedValues = ['subscriptionPlan', 'costControl'] @@ -29,15 +24,7 @@ const OrgBillingSettings: NextPageWithLayout = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [panel]) - return ( - <> - {selectedOrganization === undefined && isLoadingPermissions ? ( - - ) : ( - - )} - - ) + return } OrgBillingSettings.getLayout = (page) => ( diff --git a/apps/studio/tests/components/Billing/TaxID.utils.test.ts b/apps/studio/tests/components/Billing/TaxID.utils.test.ts index 68908eddbf1..40d2960b0b8 100644 --- a/apps/studio/tests/components/Billing/TaxID.utils.test.ts +++ b/apps/studio/tests/components/Billing/TaxID.utils.test.ts @@ -1,4 +1,4 @@ -import { sanitizeTaxIdValue } from 'components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils' +import { sanitizeTaxIdValue } from 'components/interfaces/Organization/BillingSettings/BillingCustomerData/TaxID.utils' import { describe, test, expect } from 'vitest' /** diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index de596658974..547db303237 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -4183,6 +4183,7 @@ export interface components { postal_code?: string state?: string } + billing_name?: string } Buffer: Record BulkDeleteUserContentResponse: { @@ -4288,6 +4289,10 @@ export interface components { session_id: string token_name?: string } + CreateCollectionBody: { + name: string + retention_days: number + } CreateColumnBody: { check?: string comment?: string @@ -4469,10 +4474,24 @@ export interface components { id: string } CreateOrganizationBody: { + address?: { + city?: string + country: string + line1: string + line2?: string + postal_code?: string + state?: string + } + billing_name?: string kind?: string name: string payment_method?: string size?: string + tax_id?: { + country?: string + type: string + value: string + } /** @enum {string} */ tier: 'tier_free' | 'tier_pro' | 'tier_payg' | 'tier_team' | 'tier_enterprise' } @@ -4783,6 +4802,7 @@ export interface components { state?: string } balance: number + billing_name?: string billing_via_partner: boolean email: string } @@ -4856,7 +4876,6 @@ export interface components { } DeleteOAuthAppResponse: { client_id: string - client_secret_alias: string created_at: string icon?: string id: string @@ -5859,7 +5878,6 @@ export interface components { app_id?: string authorized_at?: string client_id?: string - client_secret_alias?: string created_at?: string created_by?: string icon?: string @@ -6815,7 +6833,6 @@ export interface components { } PutOAuthAppResponse: { client_id: string - client_secret_alias: string created_at: string icon?: string id: string @@ -7362,6 +7379,10 @@ export interface components { description?: string name?: string } + UpdateCollectionBody: { + name: string + retention_days: number + } UpdateColumnBody: { check?: string comment?: string @@ -14151,7 +14172,7 @@ export interface operations { } requestBody: { content: { - 'application/json': components['schemas']['LFSource'] + 'application/json': components['schemas']['CreateCollectionBody'] } } responses: { @@ -14260,7 +14281,7 @@ export interface operations { } requestBody: { content: { - 'application/json': components['schemas']['LFSource'] + 'application/json': components['schemas']['UpdateCollectionBody'] } } responses: { From c8a5c6736f6142103ba873a001eb96df18890ae8 Mon Sep 17 00:00:00 2001 From: Inian Date: Fri, 2 May 2025 15:33:46 +0800 Subject: [PATCH 72/86] fix(docs): update upgrading language (#35419) --- .../content/guides/platform/upgrading.mdx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/docs/content/guides/platform/upgrading.mdx b/apps/docs/content/guides/platform/upgrading.mdx index 37fa3f7346b..6ab96ea03b4 100644 --- a/apps/docs/content/guides/platform/upgrading.mdx +++ b/apps/docs/content/guides/platform/upgrading.mdx @@ -4,7 +4,13 @@ title: Upgrading Supabase ships fast and we endeavor to add all new features to existing projects wherever possible. In some cases, access to new features require upgrading or migrating your Supabase project. -You can upgrade your project using `pg_upgrade` or by pausing and restoring your project. + + +This guide refers to upgrading the Postgres version of your Supabase Project. For scaling your compute size, refer to the [Compute and Disk page](/docs/guides/platform/compute-and-disk). + + + +You can upgrade your project using in-place upgrades or by pausing and restoring your project. @@ -14,33 +20,27 @@ The Migrating and Upgrading guide has been divided into two sections. To migrate -## `pg_upgrade` - - - -This upgrade method is currently in Beta. - - +## In-place upgrades 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. +In-place upgrades uses `pg_upgrade`. For projects larger than 1GB, this method is generally faster than a pause and 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. 1. Use the "Upgrade project" button on the [Infrastructure](https://supabase.com/dashboard/project/_/settings/infrastructure) section of your dashboard. -Additionally, if a pg_upgrade upgrade should fail, your original DB would be brought back up online and be able to service requests. +Additionally, if the upgrade should fail, your original database would be brought back up online and be able to service requests. -As a rough rule of thumb, pg_upgrade operates at ~100MBps (when executing an upgrade on your data). Using the size of your database, you can use this metric to derive an approximate sense of the downtime window necessary for the upgrade. During this window, you should plan for your DB and associated services to be unavailable. +As a rough rule of thumb, pg_upgrade operates at ~100MBps (when executing an upgrade on your data). Using the size of your database, you can use this metric to derive an approximate sense of the downtime window necessary for the upgrade. During this window, you should plan for your database and associated services to be unavailable. ## Pause and restore -We recommend using the pg_upgrade method, as it is faster, and more reliable. Additionally, only Free-tier projects are eligible to use the Pause + Restore method. +We recommend using the In-place upgrade method, as it is faster, and more reliable. Additionally, only Free-tier projects are eligible to use the Pause and Restore method. @@ -106,18 +106,18 @@ When upgrading, the Supabase platform will "right-size" your disk based on the c #### Objects dependent on Postgres extensions -pg_upgrade does not support upgrading of databases containing reg\* data types referencing system OIDs. +In-place upgrades do not support upgrading of databases containing reg\* data types referencing system OIDs. If you have created any objects that depend on the following extensions, you will need to recreate them after the upgrade. #### `pg_cron` records [pg_cron](https://github.com/citusdata/pg_cron#viewing-job-run-details) does not automatically clean up historical records. This can lead to extremely large `cron.job_run_details` tables if the records are not regularly pruned; you should clean unnecessary records from this table prior to an upgrade. -During the `pg_upgrade` process, the `pg_cron` extension gets dropped and recreated. Prior to this process, the `cron.job_run_details` table is duplicated to avoid losing historical logs. The instantaneous disk pressure created by duplicating an extremely large details table can cause at best unnecessary performance degradation, or at worst, upgrade process failures. +During an in-place upgrade, the `pg_cron` extension gets dropped and recreated. Prior to this process, the `cron.job_run_details` table is duplicated to avoid losing historical logs. The instantaneous disk pressure created by duplicating an extremely large details table can cause at best unnecessary performance degradation, or at worst, upgrade process failures. #### Extensions -pg_upgrade does not currently support upgrading of databases using extensions older than the following versions: +In-place upgrades do not currently support upgrading of databases using extensions older than the following versions: - TimescaleDB 2.16.1 - plv8 3.1.10 From 5a03fde0d6d1d5cc39d0f176a2dea9f40bfbfed4 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Fri, 2 May 2025 10:12:55 +0200 Subject: [PATCH 73/86] fix table exports (#35420) * fix table exports * change return for removing square brackets --- apps/studio/data/table-rows/table-rows-query.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/data/table-rows/table-rows-query.ts b/apps/studio/data/table-rows/table-rows-query.ts index 1241ab9e4cf..3738ab31ab2 100644 --- a/apps/studio/data/table-rows/table-rows-query.ts +++ b/apps/studio/data/table-rows/table-rows-query.ts @@ -142,9 +142,9 @@ export const fetchAllTableRows = async ({ ) try { - const { result } = await executeWithRetry(async () => { + const { result } = await executeWithRetry(async () => executeSql({ projectRef, connectionString, sql: query }) - }) + ) rows.push(...result) progressCallback?.(rows.length) From 12ba4da4a58d45539b6727b71e5c62dff7970872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Fri, 2 May 2025 15:45:03 +0700 Subject: [PATCH 74/86] feat: groundwork for larger instance sizes (#35373) --- .../DiskManagement/DiskManagement.types.ts | 24 + .../DiskManagement/DiskManagement.utils.ts | 16 + .../ui/DiskManagement.constants.tsx | 34 + .../interfaces/Organization/Usage/Compute.tsx | 8 + .../ConnectionPooling.constants.ts | 32 + .../data/analytics/org-daily-stats-query.ts | 24 + .../data/analytics/project-log-stats-query.ts | 4 +- .../data/integrations/integrations-query.ts | 1 - apps/studio/data/invoices/invoices-query.ts | 2 +- .../notifications/notifications-v2-query.ts | 4 +- ...anization-member-role-unassign-mutation.ts | 2 +- ...rganization-member-role-update-mutation.ts | 2 +- .../data/projects/new-project.constants.ts | 64 ++ packages/api-types/types/api.d.ts | 410 +++++++- packages/api-types/types/platform.d.ts | 952 ++++++++++++------ 15 files changed, 1233 insertions(+), 346 deletions(-) diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.types.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.types.ts index 0ae74c4f7b9..1cad60fa35f 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.types.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.types.ts @@ -13,6 +13,14 @@ export type ComputeInstanceAddonVariantId = | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' > | 'ci_nano' @@ -28,6 +36,14 @@ export type ComputeInstanceSize = | '8XL' | '12XL' | '16XL' + | '24XL' + | '24XL - Optimized CPU' + | '24XL - Optimized Memory' + | '24XL - High Memory' + | '48XL' + | '48XL - Optimized CPU' + | '48XL - Optimized Memory' + | '48XL - High Memory' export type InfraInstanceSize = | 'pico' @@ -42,6 +58,14 @@ export type InfraInstanceSize = | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' export interface DiskManagementMessage { message: string diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts index bef3303bfca..a1040ca8b73 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.utils.ts @@ -307,6 +307,14 @@ export const mapComputeSizeNameToAddonVariantId = ( '8xlarge': 'ci_8xlarge', '12xlarge': 'ci_12xlarge', '16xlarge': 'ci_16xlarge', + '24xlarge': 'ci_24xlarge', + '24xlarge_optimized_memory': 'ci_24xlarge_optimized_memory', + '24xlarge_optimized_cpu': 'ci_24xlarge_optimized_cpu', + '24xlarge_high_memory': 'ci_24xlarge_high_memory', + '48xlarge': 'ci_48xlarge', + '48xlarge_optimized_memory': 'ci_48xlarge_optimized_memory', + '48xlarge_optimized_cpu': 'ci_48xlarge_optimized_cpu', + '48xlarge_high_memory': 'ci_48xlarge_high_memory', }[computeSize ?? 'nano'] as ComputeInstanceAddonVariantId } @@ -325,6 +333,14 @@ export const mapAddOnVariantIdToComputeSize = ( ci_8xlarge: '8XL', ci_12xlarge: '12XL', ci_16xlarge: '16XL', + ci_24xlarge: '24XL', + ci_24xlarge_optimized_memory: '24XL - Optimized Memory', + ci_24xlarge_optimized_cpu: '24XL - Optimized CPU', + ci_24xlarge_high_memory: '24XL - High Memory', + ci_48xlarge: '48XL', + ci_48xlarge_optimized_memory: '48XL - Optimized Memory', + ci_48xlarge_optimized_cpu: '48XL - Optimized CPU', + ci_48xlarge_high_memory: '48XL - High Memory', }[addonVariantId] as ComputeInstanceSize } diff --git a/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx b/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx index 7409aa2dbad..c3e151ef5ac 100644 --- a/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx +++ b/apps/studio/components/interfaces/DiskManagement/ui/DiskManagement.constants.tsx @@ -71,6 +71,14 @@ export const COMPUTE_BASELINE_IOPS = { ci_8xlarge: 40000, ci_12xlarge: 50000, ci_16xlarge: 80000, + ci_24xlarge: 120000, + ci_24xlarge_optimized_cpu: 120000, + ci_24xlarge_optimized_memory: 120000, + ci_24xlarge_high_memory: 120000, + ci_48xlarge: 240000, + ci_48xlarge_optimized_cpu: 240000, + ci_48xlarge_optimized_memory: 240000, + ci_48xlarge_high_memory: 240000, } export const COMPUTE_MAX_IOPS = { ci_nano: 11800, @@ -84,7 +92,16 @@ export const COMPUTE_MAX_IOPS = { ci_8xlarge: 40000, ci_12xlarge: 50000, ci_16xlarge: 80000, + ci_24xlarge: 120000, + ci_24xlarge_optimized_cpu: 120000, + ci_24xlarge_optimized_memory: 120000, + ci_24xlarge_high_memory: 120000, + ci_48xlarge: 240000, + ci_48xlarge_optimized_cpu: 240000, + ci_48xlarge_optimized_memory: 240000, + ci_48xlarge_high_memory: 240000, } + export const COMPUTE_BASELINE_THROUGHPUT = { ci_nano: 43, ci_micro: 87, @@ -97,7 +114,16 @@ export const COMPUTE_BASELINE_THROUGHPUT = { ci_8xlarge: 9500, ci_12xlarge: 14250, ci_16xlarge: 19000, + ci_24xlarge: 30000, + ci_24xlarge_optimized_cpu: 30000, + ci_24xlarge_optimized_memory: 30000, + ci_24xlarge_high_memory: 30000, + ci_48xlarge: 40000, + ci_48xlarge_optimized_cpu: 40000, + ci_48xlarge_optimized_memory: 40000, + ci_48xlarge_high_memory: 40000, } + export const COMPUTE_MAX_THROUGHPUT = { ci_nano: 2085, ci_micro: 2085, @@ -110,6 +136,14 @@ export const COMPUTE_MAX_THROUGHPUT = { ci_8xlarge: 9500, ci_12xlarge: 14250, ci_16xlarge: 19000, + ci_24xlarge: 30000, + ci_24xlarge_optimized_cpu: 30000, + ci_24xlarge_optimized_memory: 30000, + ci_24xlarge_high_memory: 30000, + ci_48xlarge: 40000, + ci_48xlarge_optimized_cpu: 40000, + ci_48xlarge_optimized_memory: 40000, + ci_48xlarge_high_memory: 40000, } export const RESTRICTED_COMPUTE_FOR_IOPS_ON_GP3 = ['ci_nano', 'ci_micro', 'ci_small', 'ci_medium'] diff --git a/apps/studio/components/interfaces/Organization/Usage/Compute.tsx b/apps/studio/components/interfaces/Organization/Usage/Compute.tsx index 50059f297a5..4436a14590d 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Compute.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Compute.tsx @@ -48,6 +48,14 @@ const Compute = ({ orgSlug, projectRef, startDate, endDate }: ComputeProps) => { [ComputeUsageMetric.COMPUTE_HOURS_8XL]: 'red', [ComputeUsageMetric.COMPUTE_HOURS_12XL]: 'dark-red', [ComputeUsageMetric.COMPUTE_HOURS_16XL]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_24XL]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_24XL_OPTIMIZED_CPU]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_24XL_OPTIMIZED_MEMORY]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_24XL_HIGH_MEMORY]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_48XL]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_48XL_OPTIMIZED_CPU]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_48XL_OPTIMIZED_MEMORY]: 'purple', + [ComputeUsageMetric.COMPUTE_HOURS_48XL_HIGH_MEMORY]: 'purple', } const attributes: Attribute[] = Object.keys(ComputeUsageMetric).map((it) => ({ diff --git a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.constants.ts b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.constants.ts index c9407d2e881..e2e49a2b487 100644 --- a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.constants.ts +++ b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.constants.ts @@ -48,4 +48,36 @@ export const POOLING_OPTIMIZATIONS = { maxClientConn: 12000, poolSize: 128, }, + ci_24xlarge: { + maxClientConn: 18000, + poolSize: 192, + }, + ci_24xlarge_optimized_cpu: { + maxClientConn: 18000, + poolSize: 192, + }, + ci_24xlarge_optimized_memory: { + maxClientConn: 18000, + poolSize: 192, + }, + ci_24xlarge_high_memory: { + maxClientConn: 18000, + poolSize: 192, + }, + ci_48xlarge: { + maxClientConn: 36000, + poolSize: 384, + }, + ci_48xlarge_optimized_cpu: { + maxClientConn: 36000, + poolSize: 384, + }, + ci_48xlarge_optimized_memory: { + maxClientConn: 36000, + poolSize: 384, + }, + ci_48xlarge_high_memory: { + maxClientConn: 36000, + poolSize: 384, + }, } diff --git a/apps/studio/data/analytics/org-daily-stats-query.ts b/apps/studio/data/analytics/org-daily-stats-query.ts index 7c267ea047c..ee5788aa353 100644 --- a/apps/studio/data/analytics/org-daily-stats-query.ts +++ b/apps/studio/data/analytics/org-daily-stats-query.ts @@ -56,6 +56,14 @@ export enum ComputeUsageMetric { COMPUTE_HOURS_8XL = 'COMPUTE_HOURS_8XL', COMPUTE_HOURS_12XL = 'COMPUTE_HOURS_12XL', COMPUTE_HOURS_16XL = 'COMPUTE_HOURS_16XL', + COMPUTE_HOURS_24XL = 'COMPUTE_HOURS_24XL', + COMPUTE_HOURS_24XL_OPTIMIZED_CPU = 'COMPUTE_HOURS_24XL_OPTIMIZED_CPU', + COMPUTE_HOURS_24XL_OPTIMIZED_MEMORY = 'COMPUTE_HOURS_24XL_OPTIMIZED_MEMORY', + COMPUTE_HOURS_24XL_HIGH_MEMORY = 'COMPUTE_HOURS_24XL_HIGH_MEMORY', + COMPUTE_HOURS_48XL = 'COMPUTE_HOURS_48XL', + COMPUTE_HOURS_48XL_OPTIMIZED_CPU = 'COMPUTE_HOURS_48XL_OPTIMIZED_CPU', + COMPUTE_HOURS_48XL_OPTIMIZED_MEMORY = 'COMPUTE_HOURS_48XL_OPTIMIZED_MEMORY', + COMPUTE_HOURS_48XL_HIGH_MEMORY = 'COMPUTE_HOURS_48XL_HIGH_MEMORY', } export const computeUsageMetricLabel = (computeUsageMetric: ComputeUsageMetric) => { @@ -82,6 +90,22 @@ export const computeUsageMetricLabel = (computeUsageMetric: ComputeUsageMetric) return '12XL' case 'COMPUTE_HOURS_16XL': return '16XL' + case 'COMPUTE_HOURS_24XL': + return '24XL' + case 'COMPUTE_HOURS_24XL_OPTIMIZED_CPU': + return '24XL - Optimized CPU' + case 'COMPUTE_HOURS_24XL_OPTIMIZED_MEMORY': + return '24XL - Optimized Memory' + case 'COMPUTE_HOURS_24XL_HIGH_MEMORY': + return '24XL - High Memory' + case 'COMPUTE_HOURS_48XL': + return '48XL' + case 'COMPUTE_HOURS_48XL_OPTIMIZED_CPU': + return '48XL - Optimized CPU' + case 'COMPUTE_HOURS_48XL_OPTIMIZED_MEMORY': + return '48XL - Optimized Memory' + case 'COMPUTE_HOURS_48XL_HIGH_MEMORY': + return '48XL - High Memory' } } diff --git a/apps/studio/data/analytics/project-log-stats-query.ts b/apps/studio/data/analytics/project-log-stats-query.ts index db6f085c46f..76167f33d9e 100644 --- a/apps/studio/data/analytics/project-log-stats-query.ts +++ b/apps/studio/data/analytics/project-log-stats-query.ts @@ -5,7 +5,9 @@ import { analyticsKeys } from './keys' export type ProjectLogStatsVariables = { projectRef?: string - interval?: operations['UsageApiController_getApiCounts']['parameters']['query']['interval'] + interval?: NonNullable< + operations['UsageApiController_getApiCounts']['parameters']['query'] + >['interval'] } export type ProjectLogStatsResponse = { diff --git a/apps/studio/data/integrations/integrations-query.ts b/apps/studio/data/integrations/integrations-query.ts index 752c452b2e0..e922fb4042a 100644 --- a/apps/studio/data/integrations/integrations-query.ts +++ b/apps/studio/data/integrations/integrations-query.ts @@ -6,7 +6,6 @@ import { integrationKeys } from './keys' export async function getIntegrations(signal?: AbortSignal) { const { data, error } = await get('/platform/integrations', { - params: { query: { integration_name: '' } }, signal, }) diff --git a/apps/studio/data/invoices/invoices-query.ts b/apps/studio/data/invoices/invoices-query.ts index 88f8699d40a..23747215b08 100644 --- a/apps/studio/data/invoices/invoices-query.ts +++ b/apps/studio/data/invoices/invoices-query.ts @@ -18,7 +18,7 @@ export async function getInvoices( const { data, error } = await get(`/platform/organizations/{slug}/billing/invoices`, { params: { path: { slug }, - query: { offset: offset.toString(), limit: limit.toString() }, + query: { offset, limit }, }, signal, }) diff --git a/apps/studio/data/notifications/notifications-v2-query.ts b/apps/studio/data/notifications/notifications-v2-query.ts index 0aec4235e7c..60df39adb79 100644 --- a/apps/studio/data/notifications/notifications-v2-query.ts +++ b/apps/studio/data/notifications/notifications-v2-query.ts @@ -40,8 +40,8 @@ export async function getNotifications(options: NotificationVariables, signal?: const { data, error } = await get('/platform/notifications', { params: { query: { - offset: String(page * limit), - limit: String(limit), + offset: page * limit, + limit, // [Alaister]: 'as any' is needed because the API types don't reflect an array of strings ...(status !== undefined ? { status } : { status: ['new', 'seen'].join(',') as any }), ...(filters.priority.length > 0 ? { priority: filters.priority.join(',') as any } : {}), diff --git a/apps/studio/data/organization-members/organization-member-role-unassign-mutation.ts b/apps/studio/data/organization-members/organization-member-role-unassign-mutation.ts index f486d86c99b..c7d90609118 100644 --- a/apps/studio/data/organization-members/organization-member-role-unassign-mutation.ts +++ b/apps/studio/data/organization-members/organization-member-role-unassign-mutation.ts @@ -25,7 +25,7 @@ export async function unassignOrganizationMemberRole({ path: { slug, gotrue_id: gotrueId, - role_id: roleId.toString(), + role_id: roleId, }, }, } diff --git a/apps/studio/data/organization-members/organization-member-role-update-mutation.ts b/apps/studio/data/organization-members/organization-member-role-update-mutation.ts index 645337d6ede..0866ae2d4ca 100644 --- a/apps/studio/data/organization-members/organization-member-role-update-mutation.ts +++ b/apps/studio/data/organization-members/organization-member-role-update-mutation.ts @@ -25,7 +25,7 @@ export async function assignOrganizationMemberRole({ const { data, error } = await put( '/platform/organizations/{slug}/members/{gotrue_id}/roles/{role_id}', { - params: { path: { slug, gotrue_id: gotrueId, role_id: roleId.toString() } }, + params: { path: { slug, gotrue_id: gotrueId, role_id: roleId } }, body: { name: roleName, role_scoped_projects: projects }, } ) diff --git a/apps/studio/data/projects/new-project.constants.ts b/apps/studio/data/projects/new-project.constants.ts index ecdaf43fee9..98240ddd5dd 100644 --- a/apps/studio/data/projects/new-project.constants.ts +++ b/apps/studio/data/projects/new-project.constants.ts @@ -105,4 +105,68 @@ export const instanceSizeSpecs: Record< priceMonthly: 3730, cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], }, + '24xlarge': { + label: '24XL', + ram: '384 GB', + cpu: '96-core', + priceHourly: 9.73, + priceMonthly: 7100, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '24xlarge_optimized_cpu': { + label: '24XL - Optimized CPU', + ram: '192 GB', + cpu: '96-core', + priceHourly: 8.9, + priceMonthly: 6500, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '24xlarge_optimized_memory': { + label: '24XL - Optimized Memory', + ram: '768 GB', + cpu: '96-core', + priceHourly: 13.84, + priceMonthly: 10100, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '24xlarge_high_memory': { + label: '24XL - High Memory', + ram: '1536 GB', + cpu: '96-core', + priceHourly: 21.91, + priceMonthly: 16000, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '48xlarge': { + label: '48XL', + ram: '768 GB', + cpu: '192-core', + priceHourly: 19.47, + priceMonthly: 14200, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '48xlarge_optimized_cpu': { + label: '48XL - Optimized CPU', + ram: '384 GB', + cpu: '192-core', + priceHourly: 17.8, + priceMonthly: 13000, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '48xlarge_optimized_memory': { + label: '48XL - Optimized Memory', + ram: '1536 GB', + cpu: '192-core', + priceHourly: 27.68, + priceMonthly: 20200, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, + '48xlarge_high_memory': { + label: '48XL - High Memory', + ram: '3072 GB', + cpu: '192-core', + priceHourly: 43.84, + priceMonthly: 32000, + cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id], + }, } diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 4f4cd475400..661af01cdbb 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -237,6 +237,48 @@ export interface paths { patch?: never trace?: never } + '/v1/projects/{ref}/advisors/performance': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Gets project performance advisors. + * @deprecated + * @description This is an **experimental** endpoint. It is subject to change or removal in future versions. Use it with caution, as it may not remain supported or stable. + */ + get: operations['getPerformanceAdvisors'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v1/projects/{ref}/advisors/security': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * Gets project security advisors. + * @deprecated + * @description This is an **experimental** endpoint. It is subject to change or removal in future versions. Use it with caution, as it may not remain supported or stable. + */ + get: operations['getSecurityAdvisors'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/v1/projects/{ref}/analytics/endpoints/logs.all': { parameters: { query?: never @@ -891,6 +933,23 @@ export interface paths { patch?: never trace?: never } + '/v1/projects/{ref}/network-bans/retrieve/enriched': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** [Beta] Gets project's network bans with additional information about which databases they affect */ + post: operations['v1-list-all-network-bans-enriched'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/v1/projects/{ref}/network-restrictions': { parameters: { query?: never @@ -1347,6 +1406,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' ) | 'cd_default' | ('pitr_7' | 'pitr_14' | 'pitr_28') @@ -1419,6 +1486,7 @@ export interface components { external_twitter_client_id: string | null external_twitter_enabled: boolean | null external_twitter_secret: string | null + external_web3_solana_enabled: boolean | null external_workos_client_id: string | null external_workos_enabled: boolean | null external_workos_secret: string | null @@ -1478,6 +1546,7 @@ export interface components { rate_limit_sms_sent: number | null rate_limit_token_refresh: number | null rate_limit_verify: number | null + rate_limit_web3: number | null refresh_token_rotation_enabled: boolean | null saml_allow_encrypted_assertions: boolean | null saml_enabled: boolean | null @@ -1604,7 +1673,7 @@ export interface components { status: 'ACTIVE' | 'REMOVED' | 'THROTTLED' verify_jwt?: boolean version: number - } + }[] BulkUpdateFunctionResponse: { functions: { created_at: number @@ -1645,6 +1714,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' git_branch?: string persistent?: boolean /** @@ -1719,7 +1796,7 @@ export interface components { */ name: string value: string - } + }[] CreateSigningKeyBody: { /** @enum {string} */ algorithm: 'EdDSA' | 'ES256' | 'RS256' | 'HS256' @@ -1909,6 +1986,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' ) | 'cd_default' | ('pitr_7' | 'pitr_14' | 'pitr_28') @@ -1952,6 +2037,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' ) | 'cd_default' | ('pitr_7' | 'pitr_14' | 'pitr_28') @@ -2005,6 +2098,13 @@ export interface components { NetworkBanResponse: { banned_ipv4_addresses: string[] } + NetworkBanResponseEnriched: { + banned_ipv4_addresses: { + banned_address: string + identifier: string + type: string + }[] + } NetworkRestrictionsRequest: { dbAllowedCidrs?: string[] dbAllowedCidrsV6?: string[] @@ -2132,12 +2232,12 @@ export interface components { /** @enum {string} */ ReleaseChannel: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' | 'preview' RemoveNetworkBanRequest: { + identifier?: string ipv4_addresses: string[] } RemoveReadReplicaBody: { database_identifier: string } - RestoreProjectBodyDto: Record SecretResponse: { name: string updated_at?: string @@ -2375,6 +2475,7 @@ export interface components { external_twitter_client_id?: string | null external_twitter_enabled?: boolean | null external_twitter_secret?: string | null + external_web3_solana_enabled?: boolean | null external_workos_client_id?: string | null external_workos_enabled?: boolean | null external_workos_secret?: string | null @@ -2440,6 +2541,7 @@ export interface components { rate_limit_sms_sent?: number | null rate_limit_token_refresh?: number | null rate_limit_verify?: number | null + rate_limit_web3?: number | null refresh_token_rotation_enabled?: boolean | null saml_enabled?: boolean | null saml_external_url?: string | null @@ -2680,12 +2782,6 @@ export interface components { region: string walg_enabled: boolean } - V1CreateFunctionBody: { - body: string - name: string - slug: string - verify_jwt?: boolean - } V1CreateMigrationBody: { name?: string query: string @@ -2705,6 +2801,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' /** * @deprecated * @description This field is deprecated and is ignored in this request @@ -2799,6 +2903,59 @@ export interface components { db_schema: string max_rows: number } + V1ProjectAdvisorsResponseDto: { + lints: { + cache_key: string + categories: ('PERFORMANCE' | 'SECURITY')[] + description: string + detail: string + /** @enum {string} */ + facing: 'EXTERNAL' + /** @enum {string} */ + level: 'ERROR' | 'WARN' | 'INFO' + metadata?: { + entity?: string + fkey_columns?: number[] + fkey_name?: string + name?: string + schema?: string + /** @enum {string} */ + type?: 'table' | 'view' | 'auth' | 'function' | 'extension' | 'compliance' + } + /** @enum {string} */ + name: + | 'unindexed_foreign_keys' + | 'auth_users_exposed' + | 'auth_rls_initplan' + | 'no_primary_key' + | 'unused_index' + | 'multiple_permissive_policies' + | 'policy_exists_rls_disabled' + | 'rls_enabled_no_policy' + | 'duplicate_index' + | 'security_definer_view' + | 'function_search_path_mutable' + | 'rls_disabled_in_public' + | 'extension_in_public' + | 'rls_references_user_metadata' + | 'materialized_view_in_api' + | 'foreign_table_in_api' + | 'unsupported_reg_types' + | 'auth_otp_long_expiry' + | 'auth_otp_short_length' + | 'ssl_not_enforced' + | 'network_restrictions_not_set' + | 'password_requirements_min_length' + | 'pitr_not_enabled' + | 'auth_leaked_password_protection' + | 'auth_insufficient_mfa_options' + | 'auth_password_policy_missing' + | 'leaked_service_key' + | 'no_backup_admin' + remediation: string + title: string + }[] + } V1ProjectRefResponse: { /** Format: int64 */ id: number @@ -2945,6 +3102,7 @@ export interface operations { query?: never header?: never path: { + /** @description Branch ID */ branch_id: string } cookie?: never @@ -2973,6 +3131,7 @@ export interface operations { query?: never header?: never path: { + /** @description Branch ID */ branch_id: string } cookie?: never @@ -3001,6 +3160,7 @@ export interface operations { query?: never header?: never path: { + /** @description Branch ID */ branch_id: string } cookie?: never @@ -3033,6 +3193,7 @@ export interface operations { query?: never header?: never path: { + /** @description Branch ID */ branch_id: string } cookie?: never @@ -3065,6 +3226,7 @@ export interface operations { query?: never header?: never path: { + /** @description Branch ID */ branch_id: string } cookie?: never @@ -3097,6 +3259,7 @@ export interface operations { query?: never header?: never path: { + /** @description Branch ID */ branch_id: string } cookie?: never @@ -3131,7 +3294,9 @@ export interface operations { code_challenge?: string code_challenge_method?: 'plain' | 'sha256' | 'S256' redirect_uri: string - response_type: string + response_mode?: string + response_type: 'code' | 'token' | 'id_token token' + scope?: string state?: string } header?: never @@ -3253,6 +3418,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -3280,6 +3446,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -3407,6 +3574,62 @@ export interface operations { } } } + getPerformanceAdvisors: { + parameters: { + query?: never + header?: never + path: { + /** @description Project ref */ + ref: string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['V1ProjectAdvisorsResponseDto'] + } + } + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } + getSecurityAdvisors: { + parameters: { + query?: never + header?: never + path: { + /** @description Project ref */ + ref: string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['V1ProjectAdvisorsResponseDto'] + } + } + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } getLogs: { parameters: { query?: { @@ -3416,6 +3639,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3441,10 +3665,12 @@ export interface operations { 'v1-get-project-api-keys': { parameters: { query?: { + /** @description Boolean string, true or false */ reveal?: boolean } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3470,10 +3696,12 @@ export interface operations { createApiKey: { parameters: { query?: { + /** @description Boolean string, true or false */ reveal?: boolean } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3503,11 +3731,13 @@ export interface operations { getApiKey: { parameters: { query?: { + /** @description Boolean string, true or false */ reveal?: boolean } header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -3533,11 +3763,13 @@ export interface operations { deleteApiKey: { parameters: { query?: { + /** @description Boolean string, true or false */ reveal?: boolean } header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -3563,11 +3795,13 @@ export interface operations { updateApiKey: { parameters: { query?: { + /** @description Boolean string, true or false */ reveal?: boolean } header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -3599,6 +3833,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3633,6 +3868,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3669,22 +3905,8 @@ export interface operations { query?: never header?: never path: { - addon_variant: - | 'ci_micro' - | 'ci_small' - | 'ci_medium' - | 'ci_large' - | 'ci_xlarge' - | 'ci_2xlarge' - | 'ci_4xlarge' - | 'ci_8xlarge' - | 'ci_12xlarge' - | 'ci_16xlarge' - | 'cd_default' - | 'pitr_7' - | 'pitr_14' - | 'pitr_28' - | 'ipv4_default' + addon_variant: unknown + /** @description Project ref */ ref: string } cookie?: never @@ -3717,6 +3939,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3751,6 +3974,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3789,6 +4013,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3821,6 +4046,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3855,6 +4081,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3893,6 +4120,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3920,6 +4148,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -3953,6 +4182,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -3981,6 +4211,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -4009,6 +4240,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -4040,6 +4272,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4074,6 +4307,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4113,6 +4347,7 @@ export interface operations { header?: never path: { provider_id: string + /** @description Project ref */ ref: string } cookie?: never @@ -4148,6 +4383,7 @@ export interface operations { header?: never path: { provider_id: string + /** @description Project ref */ ref: string } cookie?: never @@ -4187,6 +4423,7 @@ export interface operations { header?: never path: { provider_id: string + /** @description Project ref */ ref: string } cookie?: never @@ -4221,6 +4458,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4248,6 +4486,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4279,6 +4518,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string tpa_id: string } @@ -4307,6 +4547,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string tpa_id: string } @@ -4370,6 +4611,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4398,6 +4640,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4436,6 +4679,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4470,6 +4714,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4508,6 +4753,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4542,6 +4788,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4578,6 +4825,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4612,6 +4860,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4644,6 +4893,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4678,6 +4928,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4716,6 +4967,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4785,6 +5037,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4814,6 +5067,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4841,6 +5095,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4873,8 +5128,12 @@ export interface operations { 'v1-apply-a-migration': { parameters: { query?: never - header?: never + header?: { + /** @description A unique key to ensure the same migration is tracked only once. */ + 'Idempotency-Key'?: string + } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4913,6 +5172,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -4984,6 +5244,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5018,13 +5279,14 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never } requestBody: { content: { - 'application/json': components['schemas']['BulkUpdateFunctionBody'][] + 'application/json': components['schemas']['BulkUpdateFunctionBody'] } } responses: { @@ -5055,24 +5317,22 @@ export interface operations { parameters: { query?: { entrypoint_path?: string + /** @description Boolean string, true or false */ import_map?: boolean import_map_path?: string name?: string slug?: string + /** @description Boolean string, true or false */ verify_jwt?: boolean } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never } - requestBody: { - content: { - 'application/json': components['schemas']['V1CreateFunctionBody'] - 'application/vnd.denoland.eszip': components['schemas']['V1CreateFunctionBody'] - } - } + requestBody?: never responses: { 201: { headers: { @@ -5102,7 +5362,9 @@ export interface operations { query?: never header?: never path: { + /** @description Function slug */ function_slug: string + /** @description Project ref */ ref: string } cookie?: never @@ -5137,7 +5399,9 @@ export interface operations { query?: never header?: never path: { + /** @description Function slug */ function_slug: string + /** @description Project ref */ ref: string } cookie?: never @@ -5169,14 +5433,19 @@ export interface operations { parameters: { query?: { entrypoint_path?: string + /** @description Boolean string, true or false */ import_map?: boolean import_map_path?: string name?: string + slug?: string + /** @description Boolean string, true or false */ verify_jwt?: boolean } header?: never path: { + /** @description Function slug */ function_slug: string + /** @description Project ref */ ref: string } cookie?: never @@ -5216,7 +5485,9 @@ export interface operations { query?: never header?: never path: { + /** @description Function slug */ function_slug: string + /** @description Project ref */ ref: string } cookie?: never @@ -5247,11 +5518,13 @@ export interface operations { 'v1-deploy-a-function': { parameters: { query?: { + /** @description Boolean string, true or false */ bundleOnly?: boolean slug?: string } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5293,6 +5566,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5327,6 +5601,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5363,6 +5638,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5392,11 +5668,47 @@ export interface operations { } } } + 'v1-list-all-network-bans-enriched': { + parameters: { + query?: never + header?: never + path: { + /** @description Project ref */ + ref: string + } + cookie?: never + } + requestBody?: never + responses: { + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['NetworkBanResponseEnriched'] + } + } + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Failed to retrieve project's enriched network bans */ + 500: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } 'v1-get-network-restrictions': { parameters: { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5431,6 +5743,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5495,6 +5808,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5529,6 +5843,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5641,6 +5956,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5677,6 +5993,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5781,6 +6098,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5808,15 +6126,12 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never } - requestBody: { - content: { - 'application/json': components['schemas']['RestoreProjectBodyDto'] - } - } + requestBody?: never responses: { 200: { headers: { @@ -5837,6 +6152,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5862,6 +6178,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5896,13 +6213,14 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never } requestBody: { content: { - 'application/json': components['schemas']['CreateSecretBody'][] + 'application/json': components['schemas']['CreateSecretBody'] } } responses: { @@ -5932,6 +6250,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -5970,6 +6289,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6004,6 +6324,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6079,6 +6400,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6113,6 +6435,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6151,6 +6474,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6187,6 +6511,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6221,6 +6546,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6255,6 +6581,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6287,6 +6614,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6325,6 +6653,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -6363,6 +6692,7 @@ export interface operations { query?: { cursor?: string limit?: string + /** @description Project ref */ project_ref?: string sort_by?: 'name' | 'inserted_at' sort_order?: 'asc' | 'desc' diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index 547db303237..4a330ce8f5b 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -1216,6 +1216,24 @@ export interface paths { patch?: never trace?: never } + '/platform/organizations/{slug}/members/mfa/enforcement': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Gets organization MFA enforcement state */ + get: operations['MfaController_getMFAState'] + put?: never + post?: never + delete?: never + options?: never + head?: never + /** Update organization MFA enforcement state */ + patch: operations['MfaController_updateMFAState'] + trace?: never + } '/platform/organizations/{slug}/members/reached-free-project-limit': { parameters: { query?: never @@ -2987,6 +3005,23 @@ export interface paths { patch?: never trace?: never } + '/platform/projects/{ref}/run-lints/{name}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Run project lint by name */ + get: operations['ProjectRunLintsController_runLintByName'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/platform/projects/{ref}/run-lints/leaked-service-key': { parameters: { query?: never @@ -3140,63 +3175,6 @@ export interface paths { patch?: never trace?: never } - '/platform/props/project/{ref}/api': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** - * Gets project's api info - * @deprecated - */ - get: operations['ApiController_getProjectApi'] - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/platform/props/project/{ref}/jwt-secret-update-status': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** Gets the last JWT secret update status */ - get: operations['JwtSecretUpdateStatusController_getJwtSecretUpdateStatus'] - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } - '/platform/props/project/{ref}/settings': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** - * Gets project's settings - * @deprecated - */ - get: operations['PropsSettingsController_getProjectSettingsDeprecated'] - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } '/platform/replication/{ref}/pipelines': { parameters: { query?: never @@ -4060,34 +4038,6 @@ export interface components { } result?: unknown[] } - ApiResponse: { - autoApiService: { - app: { - id: number - name: string - } - app_config: { - [key: string]: unknown - } - defaultApiKey: string - endpoint: string - id: number - name: string - project: { - /** @description Project ref */ - ref: string - } - /** @enum {string} */ - protocol: 'https' - restUrl: string - service_api_keys: { - api_key_encrypted?: string - name: string - tags: string - }[] - serviceApiKey: string - } - } ApproveAuthorizationResponse: { url: string } @@ -4189,6 +4139,9 @@ export interface components { BulkDeleteUserContentResponse: { id: string } + ChangeMFAEnforcementStateRequestDto: { + enforced: boolean + } CloneBackupsResponse: { backups: { id: number @@ -4544,6 +4497,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' name: string org_id?: number organization_slug?: string @@ -4578,6 +4539,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' inserted_at: string | null is_branch_enabled: boolean is_physical_backups_enabled: boolean | null @@ -5288,6 +5257,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' | 'cd_default' | 'pitr_7' | 'pitr_14' @@ -5554,6 +5531,7 @@ export interface components { RATE_LIMIT_SMS_SENT: number RATE_LIMIT_TOKEN_REFRESH: number RATE_LIMIT_VERIFY: number + RATE_LIMIT_WEB3?: number | null REFRESH_TOKEN_ROTATION_ENABLED: boolean SAML_ALLOW_ENCRYPTED_ASSERTIONS: boolean SAML_ENABLED: boolean @@ -5607,9 +5585,6 @@ export interface components { /** @enum {string} */ privilege_type: 'ALL' | 'SELECT' | 'INSERT' | 'UPDATE' | 'REFERENCES' }[] - HCaptchaBody: { - hcaptchaToken: string - } InvitationByTokenResponse: { authorized_user: boolean email_match: boolean @@ -5641,17 +5616,6 @@ export interface components { InvoicePaymentLinkResponse: { redirectUrl: string } - JwtSecretUpdateStatus: { - jwtSecretUpdateStatus: { - change_tracking_id?: string - /** @enum {number} */ - error?: 0 | 1 | 2 | 3 | 4 | 5 - /** @enum {number} */ - progress?: 0 | 1 | 2 | 3 | 4 | 5 - /** @enum {number} */ - status?: 0 | 1 | 2 - } - } LFAccessToken: { description: string | null id: number @@ -5841,6 +5805,9 @@ export interface components { primary_email: string username: string } + MfaStatusResponseDto: { + enforced: boolean + } MoveObjectBody: { from: string to: string @@ -5934,6 +5901,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' region: string /** @enum {string} */ status: @@ -6038,6 +6013,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' inserted_at: string | null is_branch_enabled: boolean is_physical_backups_enabled: boolean | null @@ -6108,6 +6091,14 @@ export interface components { | 'COMPUTE_HOURS_8XL' | 'COMPUTE_HOURS_12XL' | 'COMPUTE_HOURS_16XL' + | 'COMPUTE_HOURS_24XL' + | 'COMPUTE_HOURS_24XL_OPTIMIZED_CPU' + | 'COMPUTE_HOURS_24XL_OPTIMIZED_MEMORY' + | 'COMPUTE_HOURS_24XL_HIGH_MEMORY' + | 'COMPUTE_HOURS_48XL' + | 'COMPUTE_HOURS_48XL_OPTIMIZED_CPU' + | 'COMPUTE_HOURS_48XL_OPTIMIZED_MEMORY' + | 'COMPUTE_HOURS_48XL_HIGH_MEMORY' | 'CUSTOM_DOMAIN' | 'PITR_7' | 'PITR_14' @@ -6576,6 +6567,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' | 'cd_default' | 'pitr_7' | 'pitr_14' @@ -6619,6 +6618,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' | 'cd_default' | 'pitr_7' | 'pitr_14' @@ -6697,6 +6704,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' inserted_at: string is_branch_enabled: boolean is_physical_backups_enabled: boolean @@ -6746,6 +6761,14 @@ export interface components { | '8xlarge' | '12xlarge' | '16xlarge' + | '24xlarge' + | '24xlarge_optimized_memory' + | '24xlarge_optimized_cpu' + | '24xlarge_high_memory' + | '48xlarge' + | '48xlarge_optimized_memory' + | '48xlarge_optimized_cpu' + | '48xlarge_high_memory' inserted_at: string | null is_branch_enabled: boolean is_physical_backups_enabled: boolean | null @@ -7011,6 +7034,59 @@ export interface components { /** @enum {string} */ privilege_type: 'ALL' | 'SELECT' | 'INSERT' | 'UPDATE' | 'REFERENCES' }[] + RunLintByNameResponseDto: { + lints: { + cache_key: string + categories: ('PERFORMANCE' | 'SECURITY')[] + description: string + detail: string + /** @enum {string} */ + facing: 'EXTERNAL' + /** @enum {string} */ + level: 'ERROR' | 'WARN' | 'INFO' + metadata?: { + entity?: string + fkey_columns?: number[] + fkey_name?: string + name?: string + schema?: string + /** @enum {string} */ + type?: 'table' | 'view' | 'auth' | 'function' | 'extension' | 'compliance' + } + /** @enum {string} */ + name: + | 'unindexed_foreign_keys' + | 'auth_users_exposed' + | 'auth_rls_initplan' + | 'no_primary_key' + | 'unused_index' + | 'multiple_permissive_policies' + | 'policy_exists_rls_disabled' + | 'rls_enabled_no_policy' + | 'duplicate_index' + | 'security_definer_view' + | 'function_search_path_mutable' + | 'rls_disabled_in_public' + | 'extension_in_public' + | 'rls_references_user_metadata' + | 'materialized_view_in_api' + | 'foreign_table_in_api' + | 'unsupported_reg_types' + | 'auth_otp_long_expiry' + | 'auth_otp_short_length' + | 'ssl_not_enforced' + | 'network_restrictions_not_set' + | 'password_requirements_min_length' + | 'pitr_not_enabled' + | 'auth_leaked_password_protection' + | 'auth_insufficient_mfa_options' + | 'auth_password_policy_missing' + | 'leaked_service_key' + | 'no_backup_admin' + remediation: string + title: string + }[] + } RunQueryBody: { query: string } @@ -7064,57 +7140,8 @@ export interface components { postgrest?: string 'supabase-postgres': string } - SettingsResponse: { - project: { - cloud_provider: string - db_dns_name: string - db_host: string - db_name: string - db_port: number - db_user: string - id: number - inserted_at: string - jwt_secret: string - name: string - ref: string - region: string - services?: { - app: { - id: number - name: string - } - app_config: { - [key: string]: unknown - } - id: number - name: string - service_api_keys: { - api_key?: string - api_key_encrypted?: string - name: string - tags: string - }[] - }[] - ssl_enforced: boolean - status: string - } - services: { - app: { - id: number - name: string - } - app_config: { - [key: string]: unknown - } - id: number - name: string - service_api_keys: { - api_key?: string - api_key_encrypted?: string - name: string - tags: string - }[] - }[] + SetupIntentRequest: { + hcaptchaToken?: string } SetupIntentResponse: { client_secret: string @@ -7327,6 +7354,14 @@ export interface components { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' | 'cd_default' | 'pitr_7' | 'pitr_14' @@ -7509,6 +7544,7 @@ export interface components { EXTERNAL_TWITTER_CLIENT_ID?: string | null EXTERNAL_TWITTER_ENABLED?: boolean | null EXTERNAL_TWITTER_SECRET?: string | null + EXTERNAL_WEB3_SOLANA_ENABLED?: boolean | null EXTERNAL_WORKOS_CLIENT_ID?: string | null EXTERNAL_WORKOS_ENABLED?: boolean | null EXTERNAL_WORKOS_SECRET?: string | null @@ -7654,11 +7690,16 @@ export interface components { /** @enum {string} */ status: 'new' | 'seen' | 'archived' }[] - UpdateNotificationExceptionParamsDto: { + UpdateNotificationExceptionBodyDto: { /** Format: uuid */ - id: string - /** @description Project ref */ - ref: string + assigned_to?: string + is_disabled?: boolean + lint_category?: string + lint_metadata?: { + [key: string]: unknown + } + lint_name?: string + note?: string } UpdateNotificationsBodyV1: { ids: string[] @@ -8090,6 +8131,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8118,6 +8160,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8150,6 +8193,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8188,6 +8232,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8226,6 +8271,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8264,6 +8310,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8302,6 +8349,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8375,6 +8423,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8414,6 +8463,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -8447,6 +8497,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -8486,6 +8537,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -8641,6 +8693,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8729,6 +8782,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8759,6 +8813,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8789,6 +8844,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8819,6 +8875,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8847,6 +8904,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8879,6 +8937,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -8939,8 +8998,8 @@ export interface operations { parameters: { query: { id: number - limit: number - offset: number + limit?: number + offset?: number } header: { 'x-connection-encrypted': string @@ -9098,10 +9157,7 @@ export interface operations { } IntegrationsController_getUserInstallations: { parameters: { - query: { - /** @description Filter results by integration name. Optional */ - integration_name: unknown - } + query?: never header?: never path?: never cookie?: never @@ -9634,12 +9690,12 @@ export interface operations { NotificationsController_getNotificationsV2: { parameters: { query?: { - limit?: string - offset?: string + limit?: number + offset?: number org_slug?: string - priority?: 'Critical' | 'Warning' | 'Info' + priority?: string project_ref?: string - status?: 'new' | 'seen' | 'archived' + status?: string } header?: never path?: never @@ -9772,6 +9828,7 @@ export interface operations { query?: never header?: never path: { + /** @description Oauth authorization id */ id: string } cookie?: never @@ -9849,6 +9906,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -9870,6 +9928,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -9902,6 +9961,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -9932,7 +9992,9 @@ export interface operations { OrgAuditLogsController_getAuditLogs: { parameters: { query: { + /** @description End timestamp */ iso_timestamp_end: string + /** @description Start timestamp */ iso_timestamp_start: string } header?: never @@ -9972,6 +10034,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10010,6 +10073,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10046,12 +10110,13 @@ export interface operations { } OrgInvoicesController_getInvoices: { parameters: { - query: { - limit: string - offset: string + query?: { + limit?: number + offset?: number } header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10086,6 +10151,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10121,6 +10187,7 @@ export interface operations { header?: never path: { invoiceId: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10156,6 +10223,7 @@ export interface operations { header?: never path: { invoiceId: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10190,6 +10258,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10259,6 +10328,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10293,6 +10363,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10329,6 +10400,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10365,6 +10437,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10393,6 +10466,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10428,12 +10502,57 @@ export interface operations { parameters: { query: { endDate: string - metric: string + interval?: string + metric: + | 'EGRESS' + | 'DATABASE_SIZE' + | 'STORAGE_SIZE' + | 'MONTHLY_ACTIVE_USERS' + | 'MONTHLY_ACTIVE_SSO_USERS' + | 'FUNCTION_INVOCATIONS' + | 'STORAGE_IMAGES_TRANSFORMED' + | 'REALTIME_MESSAGE_COUNT' + | 'REALTIME_PEAK_CONNECTIONS' + | 'DISK_SIZE_GB_HOURS_GP3' + | 'DISK_SIZE_GB_HOURS_IO2' + | 'AUTH_MFA_PHONE' + | 'AUTH_MFA_WEB_AUTHN' + | 'LOG_DRAIN_EVENTS' + | 'MONTHLY_ACTIVE_THIRD_PARTY_USERS' + | 'DISK_THROUGHPUT_GP3' + | 'DISK_IOPS_GP3' + | 'DISK_IOPS_IO2' + | 'COMPUTE_HOURS_BRANCH' + | 'COMPUTE_HOURS_XS' + | 'COMPUTE_HOURS_SM' + | 'COMPUTE_HOURS_MD' + | 'COMPUTE_HOURS_L' + | 'COMPUTE_HOURS_XL' + | 'COMPUTE_HOURS_2XL' + | 'COMPUTE_HOURS_4XL' + | 'COMPUTE_HOURS_8XL' + | 'COMPUTE_HOURS_12XL' + | 'COMPUTE_HOURS_16XL' + | 'COMPUTE_HOURS_24XL' + | 'COMPUTE_HOURS_24XL_OPTIMIZED_CPU' + | 'COMPUTE_HOURS_24XL_OPTIMIZED_MEMORY' + | 'COMPUTE_HOURS_24XL_HIGH_MEMORY' + | 'COMPUTE_HOURS_48XL' + | 'COMPUTE_HOURS_48XL_OPTIMIZED_CPU' + | 'COMPUTE_HOURS_48XL_OPTIMIZED_MEMORY' + | 'COMPUTE_HOURS_48XL_HIGH_MEMORY' + | 'CUSTOM_DOMAIN' + | 'PITR_7' + | 'PITR_14' + | 'PITR_28' + | 'IPV4' + | 'LOG_DRAIN' projectRef?: string startDate: string } header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10466,6 +10585,7 @@ export interface operations { } header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10536,6 +10656,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10565,6 +10686,7 @@ export interface operations { header?: never path: { gotrue_id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10592,6 +10714,7 @@ export interface operations { header?: never path: { gotrue_id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10623,7 +10746,8 @@ export interface operations { header?: never path: { gotrue_id: string - role_id: string + role_id: number + /** @description Organization slug */ slug: string } cookie?: never @@ -10655,7 +10779,8 @@ export interface operations { header?: never path: { gotrue_id: string - role_id: string + role_id: number + /** @description Organization slug */ slug: string } cookie?: never @@ -10682,6 +10807,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10710,6 +10836,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10741,6 +10868,7 @@ export interface operations { header?: never path: { id: number + /** @description Organization slug */ slug: string } cookie?: never @@ -10767,6 +10895,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string token: string } @@ -10796,6 +10925,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string token: string } @@ -10818,6 +10948,92 @@ export interface operations { } } } + MfaController_getMFAState: { + parameters: { + query?: never + header?: never + path: { + /** @description Organization slug */ + slug: string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['MfaStatusResponseDto'] + } + } + /** @description MFA enfocement state on organization. */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['MfaStatusResponseDto'] + } + } + /** @description Failed to get organization MFA status */ + 500: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } + MfaController_updateMFAState: { + parameters: { + query?: never + header?: never + path: { + /** @description Organization slug */ + slug: string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['ChangeMFAEnforcementStateRequestDto'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['MfaStatusResponseDto'] + } + } + /** @description MFA enfocement state on organization updated successfully. */ + 201: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['MfaStatusResponseDto'] + } + } + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Failed to update organization MFA enforcement */ + 500: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } ReachedFreeProjectLimitController_getMembersWhoReachedFreeProjectLimit: { parameters: { query?: never @@ -10854,6 +11070,7 @@ export interface operations { } header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10881,6 +11098,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -10913,6 +11131,7 @@ export interface operations { header?: never path: { app_id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10941,6 +11160,7 @@ export interface operations { header?: never path: { app_id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10970,6 +11190,7 @@ export interface operations { path: { app_id: string secret_id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -10996,6 +11217,7 @@ export interface operations { header?: never path: { id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -11028,6 +11250,7 @@ export interface operations { header?: never path: { id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -11056,6 +11279,7 @@ export interface operations { header?: never path: { id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -11083,7 +11307,9 @@ export interface operations { query?: never header?: never path: { + /** @description Oauth authorization id */ id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -11111,7 +11337,9 @@ export interface operations { query?: never header?: never path: { + /** @description Oauth authorization id */ id: string + /** @description Organization slug */ slug: string } cookie?: never @@ -11139,6 +11367,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11173,6 +11402,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11209,6 +11439,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11250,11 +11481,7 @@ export interface operations { } cookie?: never } - requestBody: { - content: { - 'application/json': components['schemas']['HCaptchaBody'] - } - } + requestBody?: never responses: { 201: { headers: { @@ -11284,6 +11511,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11347,6 +11575,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11381,6 +11610,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11419,6 +11649,7 @@ export interface operations { query?: never header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11477,12 +11708,14 @@ export interface operations { OrgUsageController_getOrgUsage: { parameters: { query?: { + apikey?: string end?: string project_ref?: string start?: string } header?: never path: { + /** @description Organization slug */ slug: string } cookie?: never @@ -11534,6 +11767,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11570,6 +11804,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11610,6 +11845,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11653,6 +11889,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11689,6 +11926,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11725,14 +11963,15 @@ export interface operations { ColumnsController_deleteColumn: { parameters: { query: { + /** @description Boolean string, true or false */ cascade?: boolean - /** @description Column ID */ id: string } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11765,13 +12004,13 @@ export interface operations { ColumnsController_updateColumn: { parameters: { query: { - /** @description Column ID */ id: string } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11812,6 +12051,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11848,6 +12088,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11884,13 +12125,13 @@ export interface operations { ExtensionsController_deleteExtension: { parameters: { query: { - /** @description Extension ID */ id: string } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11932,6 +12173,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -11971,6 +12213,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12007,6 +12250,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12043,13 +12287,13 @@ export interface operations { FunctionsController_deleteFunction: { parameters: { query: { - /** @description Function ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12082,13 +12326,13 @@ export interface operations { FunctionsController_updateFunction: { parameters: { query: { - /** @description Function ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12128,6 +12372,7 @@ export interface operations { excluded_schemas?: string id?: string include_columns?: string + include_system_schemas?: string included_schemas?: string limit?: string offset?: string @@ -12136,6 +12381,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12175,6 +12421,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12211,6 +12458,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12247,13 +12495,13 @@ export interface operations { PoliciesController_deletePolicy: { parameters: { query: { - /** @description Policy ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12286,13 +12534,13 @@ export interface operations { PoliciesController_updatePolicy: { parameters: { query: { - /** @description Policy ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12333,6 +12581,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12369,6 +12618,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12405,13 +12655,13 @@ export interface operations { PublicationsController_deletePublication: { parameters: { query: { - /** @description Publication ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12444,13 +12694,13 @@ export interface operations { PublicationsController_updatePublication: { parameters: { query: { - /** @description Publication ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12491,6 +12741,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12529,6 +12780,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12561,6 +12813,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12595,6 +12848,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12631,6 +12885,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12667,13 +12922,13 @@ export interface operations { SchemasController_deleteSchema: { parameters: { query: { - /** @description Schema ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12706,13 +12961,13 @@ export interface operations { SchemasController_updateSchema: { parameters: { query: { - /** @description Schema ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12761,6 +13016,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12797,6 +13053,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12833,14 +13090,15 @@ export interface operations { TablesController_deleteTable: { parameters: { query: { + /** @description Boolean string, true or false */ cascade?: boolean - /** @description Table ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12873,13 +13131,13 @@ export interface operations { TablesController_updateTable: { parameters: { query: { - /** @description Table ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12923,6 +13181,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12959,6 +13218,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -12995,13 +13255,13 @@ export interface operations { TriggersController_deleteTrigger: { parameters: { query: { - /** @description Trigger ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13034,13 +13294,13 @@ export interface operations { TriggersController_updateTrigger: { parameters: { query: { - /** @description Trigger ID */ id: number } header: { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13084,6 +13344,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13128,6 +13389,7 @@ export interface operations { 'x-connection-encrypted': string } path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13331,7 +13593,6 @@ export interface operations { query?: never header?: never path: { - /** @description Access token ID */ id: number } cookie?: never @@ -13360,7 +13621,6 @@ export interface operations { query?: never header?: never path: { - /** @description Access token ID */ id: number } cookie?: never @@ -13387,7 +13647,9 @@ export interface operations { UserAuditLogsController_getAuditLogs: { parameters: { query: { + /** @description End timestamp */ iso_timestamp_end: string + /** @description Start timestamp */ iso_timestamp_start: string } header?: never @@ -13582,6 +13844,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13603,6 +13866,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13630,6 +13894,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13782,10 +14047,12 @@ export interface operations { query?: { iso_timestamp_end?: string iso_timestamp_start?: string + project?: string sql?: string } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -13817,11 +14084,12 @@ export interface operations { } UsageApiController_getApiCounts: { parameters: { - query: { - interval: 'minutely' | 'hourly' | 'daily' | '15min' | '30min' | '1hr' | '3hr' | '1day' + query?: { + interval?: 'minutely' | 'hourly' | 'daily' | '15min' | '30min' | '1hr' | '3hr' | '1day' } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14132,6 +14400,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14166,6 +14435,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14204,6 +14474,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string token: string } @@ -14239,6 +14510,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string token: string } @@ -14274,6 +14546,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string token: string } @@ -14313,6 +14586,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string token: string variant: 'dot' | 'json_schema' @@ -14655,6 +14929,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14689,6 +14964,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14736,6 +15012,14 @@ export interface operations { | 'ci_8xlarge' | 'ci_12xlarge' | 'ci_16xlarge' + | 'ci_24xlarge' + | 'ci_24xlarge_optimized_cpu' + | 'ci_24xlarge_optimized_memory' + | 'ci_24xlarge_high_memory' + | 'ci_48xlarge' + | 'ci_48xlarge_optimized_cpu' + | 'ci_48xlarge_optimized_memory' + | 'ci_48xlarge_high_memory' | 'cd_default' | 'pitr_7' | 'pitr_14' @@ -14744,6 +15028,7 @@ export interface operations { | 'auth_mfa_phone_default' | 'auth_mfa_web_authn_default' | 'log_drain_default' + /** @description Project ref */ ref: string } cookie?: never @@ -14776,6 +15061,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14804,6 +15090,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14842,6 +15129,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14870,6 +15158,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -14898,6 +15187,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15010,6 +15300,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15038,6 +15329,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15135,6 +15427,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15169,6 +15462,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15207,6 +15501,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15235,6 +15530,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15272,6 +15568,7 @@ export interface operations { parameters: { query?: { cursor?: string + /** @description Boolean string, true or false */ favorite?: boolean limit?: string name?: string @@ -15282,6 +15579,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15310,6 +15608,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15340,6 +15639,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15374,6 +15674,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15405,6 +15706,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15441,6 +15743,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15469,6 +15772,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15502,9 +15806,7 @@ export interface operations { ids: string[] } header?: never - path: { - ref: string - } + path?: never cookie?: never } requestBody?: never @@ -15537,6 +15839,7 @@ export interface operations { path: { /** @description Content folder id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -15567,6 +15870,7 @@ export interface operations { path: { /** @description Content folder id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -15598,6 +15902,7 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never @@ -15661,6 +15966,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15689,6 +15995,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15710,6 +16017,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15775,6 +16083,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15809,6 +16118,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15845,6 +16155,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15879,6 +16190,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15917,6 +16229,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -15984,6 +16297,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16067,6 +16381,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16095,6 +16410,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16129,6 +16445,7 @@ export interface operations { } header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16156,13 +16473,14 @@ export interface operations { header?: never path: { id: string + /** @description Project ref */ ref: string } cookie?: never } requestBody: { content: { - 'application/json': components['schemas']['UpdateNotificationExceptionParamsDto'] + 'application/json': components['schemas']['UpdateNotificationExceptionBodyDto'] } } responses: { @@ -16272,6 +16590,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16339,6 +16658,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16377,6 +16697,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16411,6 +16732,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16468,6 +16790,63 @@ export interface operations { } } } + ProjectRunLintsController_runLintByName: { + parameters: { + query?: never + header?: never + path: { + name: + | 'unindexed_foreign_keys' + | 'auth_users_exposed' + | 'auth_rls_initplan' + | 'no_primary_key' + | 'unused_index' + | 'multiple_permissive_policies' + | 'policy_exists_rls_disabled' + | 'rls_enabled_no_policy' + | 'duplicate_index' + | 'security_definer_view' + | 'function_search_path_mutable' + | 'rls_disabled_in_public' + | 'extension_in_public' + | 'rls_references_user_metadata' + | 'materialized_view_in_api' + | 'foreign_table_in_api' + | 'unsupported_reg_types' + | 'auth_otp_long_expiry' + | 'auth_otp_short_length' + | 'ssl_not_enforced' + | 'network_restrictions_not_set' + | 'password_requirements_min_length' + | 'pitr_not_enabled' + | 'auth_leaked_password_protection' + | 'auth_insufficient_mfa_options' + | 'auth_password_policy_missing' + | 'leaked_service_key' + | 'no_backup_admin' + /** @description Project ref */ + ref: string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['RunLintByNameResponseDto'] + } + } + 403: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } ProjectRunLintsController_runLeakedServiceKeyLint: { parameters: { query?: never @@ -16646,6 +17025,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16675,6 +17055,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16722,97 +17103,12 @@ export interface operations { } } } - ApiController_getProjectApi: { - parameters: { - query?: never - header?: never - path: { - /** @description Project ref */ - ref: string - } - cookie?: never - } - requestBody?: never - responses: { - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['ApiResponse'] - } - } - /** @description Failed to retrieve project's api info */ - 500: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } - JwtSecretUpdateStatusController_getJwtSecretUpdateStatus: { - parameters: { - query?: never - header?: never - path: { - /** @description Project ref */ - ref: string - } - cookie?: never - } - requestBody?: never - responses: { - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['JwtSecretUpdateStatus'] - } - } - /** @description Failed to retrieve JWT secret update status */ - 500: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } - PropsSettingsController_getProjectSettingsDeprecated: { - parameters: { - query?: never - header?: never - path: { - ref: string - } - cookie?: never - } - requestBody?: never - responses: { - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['SettingsResponse'] - } - } - /** @description Failed to retrieve project's settings */ - 500: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } ReplicationPipelinesController_getPipelines: { parameters: { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16847,6 +17143,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -16886,7 +17183,9 @@ export interface operations { query?: never header?: never path: { + /** @description Pipeline id */ pipeline_id: number + /** @description Project ref */ ref: string } cookie?: never @@ -16922,7 +17221,9 @@ export interface operations { query?: never header?: never path: { + /** @description Pipeline id */ pipeline_id: number + /** @description Project ref */ ref: string } cookie?: never @@ -16959,7 +17260,9 @@ export interface operations { query?: never header?: never path: { + /** @description Pipeline id */ pipeline_id: number + /** @description Project ref */ ref: string } cookie?: never @@ -16992,7 +17295,9 @@ export interface operations { query?: never header?: never path: { + /** @description Pipeline id */ pipeline_id: number + /** @description Project ref */ ref: string } cookie?: never @@ -17025,7 +17330,9 @@ export interface operations { query?: never header?: never path: { + /** @description Pipeline id */ pipeline_id: number + /** @description Project ref */ ref: string } cookie?: never @@ -17060,7 +17367,9 @@ export interface operations { query?: never header?: never path: { + /** @description Pipeline id */ pipeline_id: number + /** @description Project ref */ ref: string } cookie?: never @@ -17093,6 +17402,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -17127,6 +17437,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -17166,7 +17477,9 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string + /** @description Sink id */ sink_id: number } cookie?: never @@ -17202,7 +17515,9 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string + /** @description Sink id */ sink_id: number } cookie?: never @@ -17239,7 +17554,9 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string + /** @description Sink id */ sink_id: number } cookie?: never @@ -17272,6 +17589,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -17307,6 +17625,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -17342,7 +17661,9 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string + /** @description Source id */ source_id: number } cookie?: never @@ -17378,7 +17699,9 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string + /** @description Source id */ source_id: number } cookie?: never @@ -17422,7 +17745,9 @@ export interface operations { query?: never header?: never path: { + /** @description Publication name */ publication_name: string + /** @description Project ref */ ref: string source_id: number } @@ -17457,7 +17782,9 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string + /** @description Source id */ source_id: number } cookie?: never @@ -17615,6 +17942,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -17649,6 +17977,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -17687,7 +18016,9 @@ export interface operations { query?: never header?: never path: { + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17722,7 +18053,9 @@ export interface operations { query?: never header?: never path: { + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17755,7 +18088,9 @@ export interface operations { query?: never header?: never path: { + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17792,7 +18127,9 @@ export interface operations { query?: never header?: never path: { + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17825,8 +18162,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17863,8 +18201,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17903,8 +18242,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17941,8 +18281,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -17981,8 +18322,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -18019,8 +18361,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -18059,8 +18402,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -18099,8 +18443,9 @@ export interface operations { query?: never header?: never path: { - /** @description Bucket ID */ + /** @description Storage bucket id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -18139,6 +18484,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -18173,6 +18519,7 @@ export interface operations { query?: never header?: never path: { + /** @description Project ref */ ref: string } cookie?: never @@ -18211,7 +18558,9 @@ export interface operations { query?: never header?: never path: { + /** @description Storage credential id */ id: string + /** @description Project ref */ ref: string } cookie?: never @@ -18267,7 +18616,7 @@ export interface operations { } requestBody: { content: { - 'application/json': components['schemas']['HCaptchaBody'] + 'application/json': components['schemas']['SetupIntentRequest'] } } responses: { @@ -18585,8 +18934,8 @@ export interface operations { } VercelProjectsController_getVercelProjects: { parameters: { - query: { - teamId: string + query?: { + teamId?: string } header: { vercel_authorization: string @@ -18615,8 +18964,8 @@ export interface operations { } VercelProjectsController_getVercelProject: { parameters: { - query: { - teamId: string + query?: { + teamId?: string } header: { vercel_authorization: string @@ -18649,7 +18998,7 @@ export interface operations { parameters: { query: { projectId: string - teamId: string + teamId?: string } header: { vercel_authorization: string @@ -18680,7 +19029,7 @@ export interface operations { parameters: { query: { projectId: string - teamId: string + teamId?: string } header: { vercel_authorization: string @@ -18742,9 +19091,11 @@ export interface operations { WorkflowRunController_listWorkflowRuns: { parameters: { query?: { + /** @description Branch ID */ branch_id?: string limit?: number offset?: number + /** @description Project ref */ project_ref?: string } header?: never @@ -18773,7 +19124,9 @@ export interface operations { WorkflowRunController_countWorkflowRuns: { parameters: { query?: { + /** @description Branch ID */ branch_id?: string + /** @description Project ref */ project_ref?: string } header?: never @@ -18804,6 +19157,7 @@ export interface operations { query?: never header?: never path: { + /** @description Workflow run ID */ workflow_run_id: string } cookie?: never From 8e48426117e3b7e3a0ee49f0ae1e4248a0688d3a Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Fri, 2 May 2025 12:02:44 +0200 Subject: [PATCH 75/86] table exports: fix type, add tests (#35422) fix type, add tests --- .../data/table-rows/table-rows-query.test.ts | 37 +++++++++++++++++++ .../data/table-rows/table-rows-query.ts | 7 ++-- 2 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 apps/studio/data/table-rows/table-rows-query.test.ts diff --git a/apps/studio/data/table-rows/table-rows-query.test.ts b/apps/studio/data/table-rows/table-rows-query.test.ts new file mode 100644 index 00000000000..7b3af9a0575 --- /dev/null +++ b/apps/studio/data/table-rows/table-rows-query.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it, vi } from 'vitest' +import { executeWithRetry } from './table-rows-query' + +describe('executeWithRetry', () => { + it('should return the result of the function when successful', async () => { + const mockFn = vi.fn().mockResolvedValue('success') + const result = await executeWithRetry(mockFn) + expect(result).toBe('success') + expect(mockFn).toHaveBeenCalledTimes(1) + }) + + it('should retry on 429 errors with exponential backoff', async () => { + const mockFn = vi + .fn() + .mockRejectedValueOnce({ status: 429, headers: { get: () => '1' } }) + .mockRejectedValueOnce({ status: 429, headers: { get: () => '1' } }) + .mockResolvedValue('success') + + const result = await executeWithRetry(mockFn) + expect(result).toBe('success') + expect(mockFn).toHaveBeenCalledTimes(3) + }) + + it('should throw error after max retries', async () => { + const mockFn = vi.fn().mockRejectedValue({ status: 429, headers: { get: () => '1' } }) + + await expect(executeWithRetry(mockFn, 2)).rejects.toMatchObject({ status: 429 }) + expect(mockFn).toHaveBeenCalledTimes(3) // Initial attempt + 2 retries + }) + + it('should throw non-429 errors immediately', async () => { + const mockFn = vi.fn().mockRejectedValue(new Error('Some other error')) + + await expect(executeWithRetry(mockFn)).rejects.toThrow('Some other error') + expect(mockFn).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/studio/data/table-rows/table-rows-query.ts b/apps/studio/data/table-rows/table-rows-query.ts index 3738ab31ab2..cb8b15eb315 100644 --- a/apps/studio/data/table-rows/table-rows-query.ts +++ b/apps/studio/data/table-rows/table-rows-query.ts @@ -45,11 +45,11 @@ async function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } -async function executeWithRetry( - fn: () => Promise, +export async function executeWithRetry( + fn: () => Promise, maxRetries: number = 3, baseDelay: number = 500 -): Promise { +): Promise { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn() @@ -64,6 +64,7 @@ async function executeWithRetry( throw error } } + throw new Error('Max retries reached without success') } // TODO: fetchAllTableRows is used for CSV export, but since it doesn't actually truncate anything, (compare to getTableRows) From f01b4faab038f8d342cbe62002ab629b30c2801d Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Fri, 2 May 2025 12:03:08 +0200 Subject: [PATCH 76/86] Update destructive regex and add tests (#35423) update destructive regex and add tests --- .../interfaces/SQLEditor/SQLEditor.constants.ts | 4 +++- .../interfaces/SQLEditor/SQLEditor.utils.test.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts index 776575c517c..bb8d77a0cad 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts @@ -22,7 +22,9 @@ export const sqlAiDisclaimerComment = ` export const untitledSnippetTitle = 'Untitled query' -export const destructiveSqlRegex = [/^(.*;)?\s*(drop|delete|truncate)\s/is] +export const destructiveSqlRegex = [ + /^(.*;)?\s*(drop|delete|truncate|alter\s+table\s+.*\s+drop\s+column)\s/is, +] export const ASSISTANT_TEMPLATES = [ { diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts index bea8f7a8f91..b8736ded8e1 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts @@ -246,4 +246,17 @@ describe('SQLEditor.utils:updateWithoutWhere', () => { expect(match).toBe(true) }) + + it('should catch potential destructive queries', () => { + const DESTRUCTIVE_QUERIES = [ + `ALTER TABLE test DROP COLUMN test;`, + `DELETE FROM test;`, + `DROP TABLE test;`, + `TRUNCATE TABLE test;`, + ] + + DESTRUCTIVE_QUERIES.forEach((query) => { + expect(checkDestructiveQuery(query), `Query ${query} should be destructive`).toBe(true) + }) + }) }) From 651587d98a97144b05241214953aa3d7acfa6e27 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Fri, 2 May 2025 09:11:24 -0230 Subject: [PATCH 77/86] React-based (#35425) --- apps/ui-library/app/(app)/page.tsx | 4 ++-- apps/ui-library/content/docs/getting-started/introduction.mdx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ui-library/app/(app)/page.tsx b/apps/ui-library/app/(app)/page.tsx index 129b8b3352c..dc0579832ae 100644 --- a/apps/ui-library/app/(app)/page.tsx +++ b/apps/ui-library/app/(app)/page.tsx @@ -41,8 +41,8 @@ export default function Home() { UI Blocks for Supabase Projects

- A collection of components and blocks built on the shadcn/ui library that - connect your front-end to your Supabase back-end via a single command. + A collection of React components and blocks built on the shadcn/ui library + that connect your front-end to your Supabase back-end via a single command.

Get Started diff --git a/apps/ui-library/content/docs/getting-started/introduction.mdx b/apps/ui-library/content/docs/getting-started/introduction.mdx index 9747238e25c..bc2cd067f34 100644 --- a/apps/ui-library/content/docs/getting-started/introduction.mdx +++ b/apps/ui-library/content/docs/getting-started/introduction.mdx @@ -1,6 +1,6 @@ --- title: Introduction -description: A flexible, open-source UI component library built on shadcn/ui, designed to simplify Supabase-powered projects with pre-built Auth, Storage, and Realtime features. +description: A flexible, open-source, React-based UI component library built on shadcn/ui, designed to simplify Supabase-powered projects with pre-built Auth, Storage, and Realtime features. --- Our Supabase UI component library is a set of components built on top of the shadcn/ui registry system. It's designed to make building new or existing Supabase-powered projects faster and easier by providing pre-built solutions for common but challenging features—such as authentication, file uploads, and real-time updates. From 02aea98053989cb4d255692a0f8b30fc10443d0d Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Fri, 2 May 2025 11:13:19 -0400 Subject: [PATCH 78/86] docs(third-party auth,clerk): correct examples (#35412) Correct the code examples for the Clerk third-party auth docs: - Fix TypeScript error - Change RLS policy check to accept both [Clerk v1 and v2 JWTs](https://clerk.com/docs/backend-requests/resources/session-tokens) Pulled code samples from an example app that I've checked in to make maintenance and testing easier in the future. --- .../content/guides/auth/third-party/clerk.mdx | 47 +- apps/docs/features/directives/CodeSample.ts | 21 +- examples/clerk/.gitignore | 44 + examples/clerk/README.md | 36 + examples/clerk/app/favicon.ico | Bin 0 -> 25931 bytes examples/clerk/app/globals.css | 26 + examples/clerk/app/layout.tsx | 55 + examples/clerk/app/page.tsx | 85 + examples/clerk/components/insert.tsx | 24 + examples/clerk/components/read.tsx | 24 + examples/clerk/eslint.config.mjs | 16 + examples/clerk/hooks/useSupabaseClient.ts | 16 + examples/clerk/middleware.ts | 12 + examples/clerk/next.config.ts | 7 + examples/clerk/package.json | 29 + examples/clerk/pnpm-lock.yaml | 3906 +++++++++++++++++ examples/clerk/postcss.config.mjs | 5 + examples/clerk/public/file.svg | 1 + examples/clerk/public/globe.svg | 1 + examples/clerk/public/next.svg | 1 + examples/clerk/public/vercel.svg | 1 + examples/clerk/public/window.svg | 1 + examples/clerk/supabase/.gitignore | 4 + examples/clerk/supabase/config.toml | 161 + .../20250501155648_setup_database.sql | 35 + examples/clerk/supabase/seed.sql | 0 examples/clerk/tsconfig.json | 27 + 27 files changed, 4550 insertions(+), 35 deletions(-) create mode 100644 examples/clerk/.gitignore create mode 100644 examples/clerk/README.md create mode 100644 examples/clerk/app/favicon.ico create mode 100644 examples/clerk/app/globals.css create mode 100644 examples/clerk/app/layout.tsx create mode 100644 examples/clerk/app/page.tsx create mode 100644 examples/clerk/components/insert.tsx create mode 100644 examples/clerk/components/read.tsx create mode 100644 examples/clerk/eslint.config.mjs create mode 100644 examples/clerk/hooks/useSupabaseClient.ts create mode 100644 examples/clerk/middleware.ts create mode 100644 examples/clerk/next.config.ts create mode 100644 examples/clerk/package.json create mode 100644 examples/clerk/pnpm-lock.yaml create mode 100644 examples/clerk/postcss.config.mjs create mode 100644 examples/clerk/public/file.svg create mode 100644 examples/clerk/public/globe.svg create mode 100644 examples/clerk/public/next.svg create mode 100644 examples/clerk/public/vercel.svg create mode 100644 examples/clerk/public/window.svg create mode 100644 examples/clerk/supabase/.gitignore create mode 100644 examples/clerk/supabase/config.toml create mode 100644 examples/clerk/supabase/migrations/20250501155648_setup_database.sql create mode 100644 examples/clerk/supabase/seed.sql create mode 100644 examples/clerk/tsconfig.json diff --git a/apps/docs/content/guides/auth/third-party/clerk.mdx b/apps/docs/content/guides/auth/third-party/clerk.mdx index 3a422696986..852eb4dedc2 100644 --- a/apps/docs/content/guides/auth/third-party/clerk.mdx +++ b/apps/docs/content/guides/auth/third-party/clerk.mdx @@ -37,15 +37,11 @@ If you are not able to use [Clerk's Connect with Supabase page](https://dashboar -```typescript -import { createClient } from '@supabase/supabase-js' - -const supabase = createClient('https://.supabase.co', 'SUPABASE_ANON_KEY', { - accessToken: () => { - return Clerk.session?.getToken() - }, -}) -``` +<$CodeSample +path="/clerk/hooks/useSupabaseClient.ts" +lines={[[6, 14]]} +hideElidedLines={true} +/> @@ -99,34 +95,23 @@ The recommended way to design RLS policies with Clerk is to use claims present i ### Example: Check user organization role -```sql -create policy "Only organization admins can insert in table" - on table_name - for insert - to authenticated - with check ( - ((select auth.jwt()->>'org_role') = 'org:admin') - and - (organization_id = (select auth.jwt()->>'org_id')) - ); -``` +<$CodeSample +path="/clerk/supabase/migrations/20250501155648_setup_database.sql" +lines={[[10, 18]]} +hideElidedLines={true} +/> -This RLS policy checks that the newly inserted row in the table has the user's declared organization ID (from the `org_id` session token claim) in the `organization_id` column. Additionally it ensures that they're an `org:admin`. +This RLS policy checks that the newly inserted row in the table has the user's declared organization ID in the `organization_id` column. Additionally it ensures that they're an `org:admin`. This way only organization admins can add rows to the table, for organizations they're a member of. ### Example: Check user has passed second factor verification -```sql -create policy "Only users that have passed second factor verification can read from table" - on table_name - for select - as restrictive - to authenticated - using ( - ((select auth.jwt()->'fva'->>1) != '-1') - ); -``` +<$CodeSample +path="/clerk/supabase/migrations/20250501155648_setup_database.sql" +lines={[[28, 35]]} +hideElidedLines={true} +/> This example uses a restrictive RLS policy checks that the [second factor verification](https://clerk.com/docs/guides/reverification) age element in the `fva` claim is not `'-1'` indicating the user has passed through second factor verification. diff --git a/apps/docs/features/directives/CodeSample.ts b/apps/docs/features/directives/CodeSample.ts index 8b92b746d09..cc425a51f31 100644 --- a/apps/docs/features/directives/CodeSample.ts +++ b/apps/docs/features/directives/CodeSample.ts @@ -10,6 +10,7 @@ * path="/path/to/file.ts" * lines={[1, 2], [5, 7]} // -1 may be used in end position as an alias for the last line, e.g., [1, -1] * meta="utils/client.ts" // Optional, for displaying a file path on the code block + * hideElidedLines={true} // Optional, for hiding elided lines in the code block * /> * ``` * @@ -24,6 +25,7 @@ * path="/path/to/file.ts" * lines={[1, 2], [5, 7]} // -1 may be used in end position as an alias for the last line, e.g., [1, -1] * meta="utils/client.ts" // Optional, for displaying a file path on the code block + * hideElidedLines={true} // Optional, for hiding elided lines in the code block * /> */ @@ -77,6 +79,7 @@ const codeSampleExternalSchema = z.object({ path: z.string().transform((v) => (v.startsWith('/') ? v : `/${v}`)), lines: linesValidator, meta: z.string().optional(), + hideElidedLines: z.coerce.boolean().default(false), }) type ICodeSampleExternal = z.infer & AdditionalMeta @@ -88,6 +91,7 @@ const codeSampleInternalSchema = z.object({ path: z.string().transform((v) => (v.startsWith('/') ? v : `/${v}`)), lines: linesValidator, meta: z.string().optional(), + hideElidedLines: z.coerce.boolean().default(false), }) type ICodeSampleInternal = z.infer & AdditionalMeta @@ -147,6 +151,9 @@ async function fetchSourceCodeContent(tree: Root, deps: Dependencies) { const path = getAttributeValue(node, 'path') const lines = getAttributeValueExpression(getAttributeValue(node, 'lines')) const meta = getAttributeValue(node, 'meta') + const hideElidedLines = getAttributeValueExpression( + getAttributeValue(node, 'hideElidedLines') + ) const result = codeSampleExternalSchema.safeParse({ external: isExternal, @@ -156,6 +163,7 @@ async function fetchSourceCodeContent(tree: Root, deps: Dependencies) { path, lines, meta, + hideElidedLines, }) if (!result.success) { @@ -186,12 +194,16 @@ async function fetchSourceCodeContent(tree: Root, deps: Dependencies) { const path = getAttributeValue(node, 'path') const lines = getAttributeValueExpression(getAttributeValue(node, 'lines')) const meta = getAttributeValue(node, 'meta') + const hideElidedLines = getAttributeValueExpression( + getAttributeValue(node, 'hideElidedLines') + ) const result = codeSampleInternalSchema.safeParse({ external: isExternal, path, lines, meta, + hideElidedLines, }) if (!result.success) { @@ -230,7 +242,7 @@ function rewriteNodes(contentMap: Map { - if (index !== 0 || start !== 1) { + if (!hideElidedLines && (index !== 0 || start !== 1)) { acc.push(_createElidedLine(lang, contentLines, start, end)) } // Start and end are 1-indexed and inclusive acc.push(...contentLines.slice(start - 1, end === -1 ? contentLines.length : end)) - if (index === arr.length - 1 && end !== -1 && end !== contentLines.length) { + if (!hideElidedLines && index === arr.length - 1 && end !== -1 && end !== contentLines.length) { acc.push(_createElidedLine(lang, contentLines, start, end)) } diff --git a/examples/clerk/.gitignore b/examples/clerk/.gitignore new file mode 100644 index 00000000000..e2764f94943 --- /dev/null +++ b/examples/clerk/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# clerk configuration (can include secrets) +/.clerk/ diff --git a/examples/clerk/README.md b/examples/clerk/README.md new file mode 100644 index 00000000000..e215bc4ccf1 --- /dev/null +++ b/examples/clerk/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/examples/clerk/app/favicon.ico b/examples/clerk/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/examples/clerk/app/globals.css b/examples/clerk/app/globals.css new file mode 100644 index 00000000000..a2dc41ecee5 --- /dev/null +++ b/examples/clerk/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/examples/clerk/app/layout.tsx b/examples/clerk/app/layout.tsx new file mode 100644 index 00000000000..4403e254775 --- /dev/null +++ b/examples/clerk/app/layout.tsx @@ -0,0 +1,55 @@ +import type { Metadata } from 'next' +import { + ClerkProvider, + OrganizationSwitcher, + SignInButton, + SignUpButton, + SignedIn, + SignedOut, + UserButton, +} from '@clerk/nextjs' +import { Geist, Geist_Mono } from 'next/font/google' +import './globals.css' +import { InsertData } from '@/components/insert' + +const geistSans = Geist({ + variable: '--font-geist-sans', + subsets: ['latin'], +}) + +const geistMono = Geist_Mono({ + variable: '--font-geist-mono', + subsets: ['latin'], +}) + +export const metadata: Metadata = { + title: 'Clerk Next.js Quickstart', + description: 'Generated by create next app', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + +
+ + + + + + + + + +
+ {children} + + +
+ ) +} diff --git a/examples/clerk/app/page.tsx b/examples/clerk/app/page.tsx new file mode 100644 index 00000000000..a2f1f35d279 --- /dev/null +++ b/examples/clerk/app/page.tsx @@ -0,0 +1,85 @@ +import { ReadData } from '@/components/read' +import Image from 'next/image' + +export default function Home() { + return ( +
+ ) +} diff --git a/examples/clerk/components/insert.tsx b/examples/clerk/components/insert.tsx new file mode 100644 index 00000000000..6e2f400d0c3 --- /dev/null +++ b/examples/clerk/components/insert.tsx @@ -0,0 +1,24 @@ +'use client' + +import { useSupabaseClient } from '@/hooks/useSupabaseClient' +import { useUser } from '@clerk/nextjs' + +export function InsertData() { + const supabase = useSupabaseClient() + + const { user } = useUser() + const organizationId = user?.organizationMemberships?.[0]?.organization?.id + + async function onInsertRow() { + if (organizationId) { + const { error } = await supabase + .from('secured_table') + .insert({ organization_id: organizationId }) + if (error) { + console.error(error) + } + } + } + + return +} diff --git a/examples/clerk/components/read.tsx b/examples/clerk/components/read.tsx new file mode 100644 index 00000000000..dc727c5ff80 --- /dev/null +++ b/examples/clerk/components/read.tsx @@ -0,0 +1,24 @@ +'use client' + +import { useSupabaseClient } from '@/hooks/useSupabaseClient' +import { useEffect, useState } from 'react' + +export function ReadData() { + const supabase = useSupabaseClient() + const [data, setData] = useState>([]) + + useEffect(() => { + supabase + .from('secured_table') + .select('*') + .then(({ data, error }) => { + if (error) { + console.error(error) + } else { + setData(data) + } + }) + }, [supabase]) + + return
{JSON.stringify(data, null, 2)}
+} diff --git a/examples/clerk/eslint.config.mjs b/examples/clerk/eslint.config.mjs new file mode 100644 index 00000000000..c85fb67c463 --- /dev/null +++ b/examples/clerk/eslint.config.mjs @@ -0,0 +1,16 @@ +import { dirname } from "path"; +import { fileURLToPath } from "url"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [ + ...compat.extends("next/core-web-vitals", "next/typescript"), +]; + +export default eslintConfig; diff --git a/examples/clerk/hooks/useSupabaseClient.ts b/examples/clerk/hooks/useSupabaseClient.ts new file mode 100644 index 00000000000..5c796d58e82 --- /dev/null +++ b/examples/clerk/hooks/useSupabaseClient.ts @@ -0,0 +1,16 @@ +import { useSession } from '@clerk/nextjs' +import { createClient } from '@supabase/supabase-js' + +export function useSupabaseClient() { + const { session } = useSession() + const supabaseClient = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + // Session accessed from Clerk SDK, either as Clerk.session (vanilla + // JavaScript) or useSession (React) + accessToken: async () => session?.getToken() ?? null, + } + ) + return supabaseClient +} diff --git a/examples/clerk/middleware.ts b/examples/clerk/middleware.ts new file mode 100644 index 00000000000..6f5aa9bd8f1 --- /dev/null +++ b/examples/clerk/middleware.ts @@ -0,0 +1,12 @@ +import { clerkMiddleware } from '@clerk/nextjs/server' + +export default clerkMiddleware() + +export const config = { + matcher: [ + // Skip Next.js internals and all static files, unless found in search params + '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', + // Always run for API routes + '/(api|trpc)(.*)', + ], +} diff --git a/examples/clerk/next.config.ts b/examples/clerk/next.config.ts new file mode 100644 index 00000000000..e9ffa3083ad --- /dev/null +++ b/examples/clerk/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/examples/clerk/package.json b/examples/clerk/package.json new file mode 100644 index 00000000000..5ca64fd2649 --- /dev/null +++ b/examples/clerk/package.json @@ -0,0 +1,29 @@ +{ + "name": "clerk", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@clerk/nextjs": "^6.18.2", + "@supabase/supabase-js": "^2.49.4", + "next": "15.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "15.3.1", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/examples/clerk/pnpm-lock.yaml b/examples/clerk/pnpm-lock.yaml new file mode 100644 index 00000000000..f1e977b279b --- /dev/null +++ b/examples/clerk/pnpm-lock.yaml @@ -0,0 +1,3906 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@clerk/nextjs': + specifier: ^6.18.2 + version: 6.18.2(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@supabase/supabase-js': + specifier: ^2.49.4 + version: 2.49.4 + next: + specifier: 15.3.1 + version: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: ^19.0.0 + version: 19.1.0 + react-dom: + specifier: ^19.0.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@eslint/eslintrc': + specifier: ^3 + version: 3.3.1 + '@tailwindcss/postcss': + specifier: ^4 + version: 4.1.5 + '@types/node': + specifier: ^20 + version: 20.17.32 + '@types/react': + specifier: ^19 + version: 19.1.2 + '@types/react-dom': + specifier: ^19 + version: 19.1.3(@types/react@19.1.2) + eslint: + specifier: ^9 + version: 9.25.1(jiti@2.4.2) + eslint-config-next: + specifier: 15.3.1 + version: 15.3.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + tailwindcss: + specifier: ^4 + version: 4.1.5 + typescript: + specifier: ^5 + version: 5.8.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@clerk/backend@1.30.2': + resolution: {integrity: sha512-D8eCqiD6S0soqgJ9N0y0FC8MBvUX13d6GU9DgLIDOFltqzZ2QYP82Is7jzMBWb81+vqhx9kVp8EHPFyfI9buag==} + engines: {node: '>=18.17.0'} + peerDependencies: + svix: ^1.62.0 + peerDependenciesMeta: + svix: + optional: true + + '@clerk/clerk-react@5.30.2': + resolution: {integrity: sha512-sFFjNMIJG0Oy6ngO1bRQIUYhRfC32R2xtQMihhVd8bs97G5kFcGlwjpzxdwSw8NZKoQ6dONIu9lo4ACpWImFMw==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + + '@clerk/nextjs@6.18.2': + resolution: {integrity: sha512-COpljflwoX3uleIfrDNCllwyKaTIt3NTO02swm8ZKVA4opmHbQ4xdDg1RhTS+oMoK1jK7lSO50azkcil6kQNqA==} + engines: {node: '>=18.17.0'} + peerDependencies: + next: ^13.5.7 || ^14.2.25 || ^15.2.3 + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + + '@clerk/shared@3.7.6': + resolution: {integrity: sha512-WB2LbNmdksYoPuQqbmEIeBBf/ncUAVtjn9prMH6KRcy5ira0Xu6ewmVy+DzOFKVyAKHM6iZcV3S5QMsk9wWriQ==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@clerk/types@4.56.1': + resolution: {integrity: sha512-PqPnAOJdjeNIlTgRGjEh5lAKPyoxuUyqGbMN8NazKJBqpnykuH9E/EXwTdROKDIvwmrV0L47U5sP2n6xu+4rbA==} + engines: {node: '>=18.17.0'} + + '@emnapi/core@1.4.3': + resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} + + '@emnapi/runtime@1.4.3': + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + + '@emnapi/wasi-threads@1.0.2': + resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} + + '@eslint-community/eslint-utils@4.6.1': + resolution: {integrity: sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.20.0': + resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.2.1': + resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.13.0': + resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.25.1': + resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.8': + resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.2': + resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} + engines: {node: '>=18.18'} + + '@img/sharp-darwin-arm64@0.34.1': + resolution: {integrity: sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.1': + resolution: {integrity: sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.1.0': + resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.1.0': + resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.1.0': + resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.1.0': + resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.1.0': + resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.1.0': + resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.1.0': + resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.1': + resolution: {integrity: sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.1': + resolution: {integrity: sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.34.1': + resolution: {integrity: sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.1': + resolution: {integrity: sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.1': + resolution: {integrity: sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.1': + resolution: {integrity: sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.1': + resolution: {integrity: sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.34.1': + resolution: {integrity: sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.1': + resolution: {integrity: sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@napi-rs/wasm-runtime@0.2.9': + resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==} + + '@next/env@15.3.1': + resolution: {integrity: sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==} + + '@next/eslint-plugin-next@15.3.1': + resolution: {integrity: sha512-oEs4dsfM6iyER3jTzMm4kDSbrQJq8wZw5fmT6fg2V3SMo+kgG+cShzLfEV20senZzv8VF+puNLheiGPlBGsv2A==} + + '@next/swc-darwin-arm64@15.3.1': + resolution: {integrity: sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.3.1': + resolution: {integrity: sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.3.1': + resolution: {integrity: sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@15.3.1': + resolution: {integrity: sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@15.3.1': + resolution: {integrity: sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@15.3.1': + resolution: {integrity: sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@15.3.1': + resolution: {integrity: sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.3.1': + resolution: {integrity: sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/eslint-patch@1.11.0': + resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==} + + '@supabase/auth-js@2.69.1': + resolution: {integrity: sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==} + + '@supabase/functions-js@2.4.4': + resolution: {integrity: sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==} + + '@supabase/node-fetch@2.6.15': + resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==} + engines: {node: 4.x || >=6.0.0} + + '@supabase/postgrest-js@1.19.4': + resolution: {integrity: sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==} + + '@supabase/realtime-js@2.11.2': + resolution: {integrity: sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==} + + '@supabase/storage-js@2.7.1': + resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==} + + '@supabase/supabase-js@2.49.4': + resolution: {integrity: sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==} + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.5': + resolution: {integrity: sha512-CBhSWo0vLnWhXIvpD0qsPephiaUYfHUX3U9anwDaHZAeuGpTiB3XmsxPAN6qX7bFhipyGBqOa1QYQVVhkOUGxg==} + + '@tailwindcss/oxide-android-arm64@4.1.5': + resolution: {integrity: sha512-LVvM0GirXHED02j7hSECm8l9GGJ1RfgpWCW+DRn5TvSaxVsv28gRtoL4aWKGnXqwvI3zu1GABeDNDVZeDPOQrw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.5': + resolution: {integrity: sha512-//TfCA3pNrgnw4rRJOqavW7XUk8gsg9ddi8cwcsWXp99tzdBAZW0WXrD8wDyNbqjW316Pk2hiN/NJx/KWHl8oA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.5': + resolution: {integrity: sha512-XQorp3Q6/WzRd9OalgHgaqgEbjP3qjHrlSUb5k1EuS1Z9NE9+BbzSORraO+ecW432cbCN7RVGGL/lSnHxcd+7Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.5': + resolution: {integrity: sha512-bPrLWbxo8gAo97ZmrCbOdtlz/Dkuy8NK97aFbVpkJ2nJ2Jo/rsCbu0TlGx8joCuA3q6vMWTSn01JY46iwG+clg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5': + resolution: {integrity: sha512-1gtQJY9JzMAhgAfvd/ZaVOjh/Ju/nCoAsvOVJenWZfs05wb8zq+GOTnZALWGqKIYEtyNpCzvMk+ocGpxwdvaVg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.5': + resolution: {integrity: sha512-dtlaHU2v7MtdxBXoqhxwsWjav7oim7Whc6S9wq/i/uUMTWAzq/gijq1InSgn2yTnh43kR+SFvcSyEF0GCNu1PQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.5': + resolution: {integrity: sha512-fg0F6nAeYcJ3CriqDT1iVrqALMwD37+sLzXs8Rjy8Z1ZHshJoYceodfyUwGJEsQoTyWbliFNRs2wMQNXtT7MVA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.5': + resolution: {integrity: sha512-SO+F2YEIAHa1AITwc8oPwMOWhgorPzzcbhWEb+4oLi953h45FklDmM8dPSZ7hNHpIk9p/SCZKUYn35t5fjGtHA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.5': + resolution: {integrity: sha512-6UbBBplywkk/R+PqqioskUeXfKcBht3KU7juTi1UszJLx0KPXUo10v2Ok04iBJIaDPkIFkUOVboXms5Yxvaz+g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.5': + resolution: {integrity: sha512-hwALf2K9FHuiXTPqmo1KeOb83fTRNbe9r/Ixv9ZNQ/R24yw8Ge1HOWDDgTdtzntIaIUJG5dfXCf4g9AD4RiyhQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.5': + resolution: {integrity: sha512-oDKncffWzaovJbkuR7/OTNFRJQVdiw/n8HnzaCItrNQUeQgjy7oUiYpsm9HUBgpmvmDpSSbGaCa2Evzvk3eFmA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.5': + resolution: {integrity: sha512-WiR4dtyrFdbb+ov0LK+7XsFOsG+0xs0PKZKkt41KDn9jYpO7baE3bXiudPVkTqUEwNfiglCygQHl2jklvSBi7Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.5': + resolution: {integrity: sha512-1n4br1znquEvyW/QuqMKQZlBen+jxAbvyduU87RS8R3tUSvByAkcaMTkJepNIrTlYhD+U25K4iiCIxE6BGdRYA==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.5': + resolution: {integrity: sha512-5lAC2/pzuyfhsFgk6I58HcNy6vPK3dV/PoPxSDuOTVbDvCddYHzHiJZZInGIY0venvzzfrTEUAXJFULAfFmObg==} + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@20.17.32': + resolution: {integrity: sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==} + + '@types/phoenix@1.6.6': + resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} + + '@types/react-dom@19.1.3': + resolution: {integrity: sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react@19.1.2': + resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@typescript-eslint/eslint-plugin@8.31.1': + resolution: {integrity: sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/parser@8.31.1': + resolution: {integrity: sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@8.31.1': + resolution: {integrity: sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.31.1': + resolution: {integrity: sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/types@8.31.1': + resolution: {integrity: sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.31.1': + resolution: {integrity: sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@8.31.1': + resolution: {integrity: sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@8.31.1': + resolution: {integrity: sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unrs/resolver-binding-darwin-arm64@1.7.2': + resolution: {integrity: sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.7.2': + resolution: {integrity: sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.7.2': + resolution: {integrity: sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.7.2': + resolution: {integrity: sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.7.2': + resolution: {integrity: sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.7.2': + resolution: {integrity: sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.7.2': + resolution: {integrity: sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.7.2': + resolution: {integrity: sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.7.2': + resolution: {integrity: sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.7.2': + resolution: {integrity: sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.7.2': + resolution: {integrity: sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.7.2': + resolution: {integrity: sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.7.2': + resolution: {integrity: sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.7.2': + resolution: {integrity: sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.7.2': + resolution: {integrity: sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.7.2': + resolution: {integrity: sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.7.2': + resolution: {integrity: sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==} + cpu: [x64] + os: [win32] + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.10.3: + resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001716: + resolution: {integrity: sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + + es-abstract@1.23.9: + resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.1: + resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@15.3.1: + resolution: {integrity: sha512-GnmyVd9TE/Ihe3RrvcafFhXErErtr2jS0JDeCSp3vWvy86AXwHsRBt0E3MqP/m8ACS1ivcsi5uaqjbhsG18qKw==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.0: + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.31.0: + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.3.0: + resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.25.1: + resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.4.4: + resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.0: + resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-darwin-arm64@1.29.2: + resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.29.2: + resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.29.2: + resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.29.2: + resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.29.2: + resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.29.2: + resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.29.2: + resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.29.2: + resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.29.2: + resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.29.2: + resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.29.2: + resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.2.3: + resolution: {integrity: sha512-Mi7JISo/4Ij2tDZ2xBE2WH+/KvVlkhA6juEjpEeRAVPNCpN3nxJo/5FhDNKgBcdmcmhaH6JjgST4xY/23ZYK0w==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next@15.3.1: + resolution: {integrity: sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.34.1: + resolution: {integrity: sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + + snakecase-keys@8.0.1: + resolution: {integrity: sha512-Sj51kE1zC7zh6TDlNNz0/Jn1n5HiHdoQErxO8jLtnyrkJW/M5PrI7x05uDgY3BO7OUQYKCvmeMurW6BPUdwEOw==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + swr@2.3.3: + resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + tailwindcss@4.1.5: + resolution: {integrity: sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA==} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tinyglobby@0.2.13: + resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@4.40.1: + resolution: {integrity: sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==} + engines: {node: '>=16'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + unrs-resolver@1.7.2: + resolution: {integrity: sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + ws@8.18.1: + resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@clerk/backend@1.30.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@clerk/shared': 3.7.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/types': 4.56.1 + cookie: 1.0.2 + snakecase-keys: 8.0.1 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-dom + + '@clerk/clerk-react@5.30.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@clerk/shared': 3.7.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/types': 4.56.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + + '@clerk/nextjs@6.18.2(next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@clerk/backend': 1.30.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/clerk-react': 5.30.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/shared': 3.7.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@clerk/types': 4.56.1 + next: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + server-only: 0.0.1 + tslib: 2.8.1 + transitivePeerDependencies: + - svix + + '@clerk/shared@3.7.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@clerk/types': 4.56.1 + dequal: 2.0.3 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.5 + std-env: 3.9.0 + swr: 2.3.3(react@19.1.0) + optionalDependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@clerk/types@4.56.1': + dependencies: + csstype: 3.1.3 + + '@emnapi/core@1.4.3': + dependencies: + '@emnapi/wasi-threads': 1.0.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.6.1(eslint@9.25.1(jiti@2.4.2))': + dependencies: + eslint: 9.25.1(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.20.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.0 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.2.1': {} + + '@eslint/core@0.13.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.0 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.25.1': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.2.8': + dependencies: + '@eslint/core': 0.13.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.2': {} + + '@img/sharp-darwin-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.1.0 + optional: true + + '@img/sharp-darwin-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.1.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.1.0': + optional: true + + '@img/sharp-libvips-darwin-x64@1.1.0': + optional: true + + '@img/sharp-libvips-linux-arm64@1.1.0': + optional: true + + '@img/sharp-libvips-linux-arm@1.1.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.1.0': + optional: true + + '@img/sharp-libvips-linux-s390x@1.1.0': + optional: true + + '@img/sharp-libvips-linux-x64@1.1.0': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.1.0': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.1.0': + optional: true + + '@img/sharp-linux-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.1.0 + optional: true + + '@img/sharp-linux-arm@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.1.0 + optional: true + + '@img/sharp-linux-s390x@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.1.0 + optional: true + + '@img/sharp-linux-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.1.0 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.1': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + optional: true + + '@img/sharp-wasm32@0.34.1': + dependencies: + '@emnapi/runtime': 1.4.3 + optional: true + + '@img/sharp-win32-ia32@0.34.1': + optional: true + + '@img/sharp-win32-x64@0.34.1': + optional: true + + '@napi-rs/wasm-runtime@0.2.9': + dependencies: + '@emnapi/core': 1.4.3 + '@emnapi/runtime': 1.4.3 + '@tybys/wasm-util': 0.9.0 + optional: true + + '@next/env@15.3.1': {} + + '@next/eslint-plugin-next@15.3.1': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@15.3.1': + optional: true + + '@next/swc-darwin-x64@15.3.1': + optional: true + + '@next/swc-linux-arm64-gnu@15.3.1': + optional: true + + '@next/swc-linux-arm64-musl@15.3.1': + optional: true + + '@next/swc-linux-x64-gnu@15.3.1': + optional: true + + '@next/swc-linux-x64-musl@15.3.1': + optional: true + + '@next/swc-win32-arm64-msvc@15.3.1': + optional: true + + '@next/swc-win32-x64-msvc@15.3.1': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@rtsao/scc@1.1.0': {} + + '@rushstack/eslint-patch@1.11.0': {} + + '@supabase/auth-js@2.69.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/functions-js@2.4.4': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/node-fetch@2.6.15': + dependencies: + whatwg-url: 5.0.0 + + '@supabase/postgrest-js@1.19.4': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/realtime-js@2.11.2': + dependencies: + '@supabase/node-fetch': 2.6.15 + '@types/phoenix': 1.6.6 + '@types/ws': 8.18.1 + ws: 8.18.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/storage-js@2.7.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/supabase-js@2.49.4': + dependencies: + '@supabase/auth-js': 2.69.1 + '@supabase/functions-js': 2.4.4 + '@supabase/node-fetch': 2.6.15 + '@supabase/postgrest-js': 1.19.4 + '@supabase/realtime-js': 2.11.2 + '@supabase/storage-js': 2.7.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.5': + dependencies: + enhanced-resolve: 5.18.1 + jiti: 2.4.2 + lightningcss: 1.29.2 + tailwindcss: 4.1.5 + + '@tailwindcss/oxide-android-arm64@4.1.5': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.5': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.5': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.5': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.5': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.5': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.5': + optional: true + + '@tailwindcss/oxide@4.1.5': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.5 + '@tailwindcss/oxide-darwin-arm64': 4.1.5 + '@tailwindcss/oxide-darwin-x64': 4.1.5 + '@tailwindcss/oxide-freebsd-x64': 4.1.5 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.5 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.5 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.5 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.5 + '@tailwindcss/oxide-linux-x64-musl': 4.1.5 + '@tailwindcss/oxide-wasm32-wasi': 4.1.5 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.5 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.5 + + '@tailwindcss/postcss@4.1.5': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.5 + '@tailwindcss/oxide': 4.1.5 + postcss: 8.5.3 + tailwindcss: 4.1.5 + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/estree@1.0.7': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@20.17.32': + dependencies: + undici-types: 6.19.8 + + '@types/phoenix@1.6.6': {} + + '@types/react-dom@19.1.3(@types/react@19.1.2)': + dependencies: + '@types/react': 19.1.2 + + '@types/react@19.1.2': + dependencies: + csstype: 3.1.3 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 20.17.32 + + '@typescript-eslint/eslint-plugin@8.31.1(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.31.1 + '@typescript-eslint/type-utils': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.31.1 + eslint: 9.25.1(jiti@2.4.2) + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.31.1 + '@typescript-eslint/types': 8.31.1 + '@typescript-eslint/typescript-estree': 8.31.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.31.1 + debug: 4.4.0 + eslint: 9.25.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.31.1': + dependencies: + '@typescript-eslint/types': 8.31.1 + '@typescript-eslint/visitor-keys': 8.31.1 + + '@typescript-eslint/type-utils@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.31.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + debug: 4.4.0 + eslint: 9.25.1(jiti@2.4.2) + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.31.1': {} + + '@typescript-eslint/typescript-estree@8.31.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.31.1 + '@typescript-eslint/visitor-keys': 8.31.1 + debug: 4.4.0 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.1 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.31.1 + '@typescript-eslint/types': 8.31.1 + '@typescript-eslint/typescript-estree': 8.31.1(typescript@5.8.3) + eslint: 9.25.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.31.1': + dependencies: + '@typescript-eslint/types': 8.31.1 + eslint-visitor-keys: 4.2.0 + + '@unrs/resolver-binding-darwin-arm64@1.7.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.7.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.7.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.7.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.7.2': + dependencies: + '@napi-rs/wasm-runtime': 0.2.9 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.7.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.7.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.7.2': + optional: true + + acorn-jsx@5.3.2(acorn@8.14.1): + dependencies: + acorn: 8.14.1 + + acorn@8.14.1: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.10.3: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001716: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + client-only@0.0.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + + concat-map@0.0.1: {} + + cookie@1.0.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + damerau-levenshtein@1.0.8: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + dequal@2.0.3: {} + + detect-libc@2.0.4: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + es-abstract@1.23.9: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escape-string-regexp@4.0.0: {} + + eslint-config-next@15.3.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@next/eslint-plugin-next': 15.3.1 + '@rushstack/eslint-patch': 1.11.0 + '@typescript-eslint/eslint-plugin': 8.31.1(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.25.1(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-react: 7.37.5(eslint@9.25.1(jiti@2.4.2)) + eslint-plugin-react-hooks: 5.2.0(eslint@9.25.1(jiti@2.4.2)) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.0 + eslint: 9.25.1(jiti@2.4.2) + get-tsconfig: 4.10.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.13 + unrs-resolver: 1.7.2 + optionalDependencies: + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.25.1(jiti@2.4.2)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.25.1(jiti@2.4.2)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.25.1(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.25.1(jiti@2.4.2)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.25.1(jiti@2.4.2) + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.25.1(jiti@2.4.2)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.31.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.25.1(jiti@2.4.2)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.8 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.10.3 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.25.1(jiti@2.4.2) + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@5.2.0(eslint@9.25.1(jiti@2.4.2)): + dependencies: + eslint: 9.25.1(jiti@2.4.2) + + eslint-plugin-react@7.37.5(eslint@9.25.1(jiti@2.4.2)): + dependencies: + array-includes: 3.1.8 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 9.25.1(jiti@2.4.2) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.3.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.25.1(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.6.1(eslint@9.25.1(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.20.0 + '@eslint/config-helpers': 0.2.1 + '@eslint/core': 0.13.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.25.1 + '@eslint/plugin-kit': 0.2.8 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.2 + '@types/estree': 1.0.7 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.0 + escape-string-regexp: 4.0.0 + eslint-scope: 8.3.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) + eslint-visitor-keys: 4.2.0 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.4.4(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.10.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + globals@14.0.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.3.2: + optional: true + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.1 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jiti@2.4.2: {} + + js-cookie@3.0.5: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.8 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-darwin-arm64@1.29.2: + optional: true + + lightningcss-darwin-x64@1.29.2: + optional: true + + lightningcss-freebsd-x64@1.29.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.29.2: + optional: true + + lightningcss-linux-arm64-gnu@1.29.2: + optional: true + + lightningcss-linux-arm64-musl@1.29.2: + optional: true + + lightningcss-linux-x64-gnu@1.29.2: + optional: true + + lightningcss-linux-x64-musl@1.29.2: + optional: true + + lightningcss-win32-arm64-msvc@1.29.2: + optional: true + + lightningcss-win32-x64-msvc@1.29.2: + optional: true + + lightningcss@1.29.2: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.29.2 + lightningcss-darwin-x64: 1.29.2 + lightningcss-freebsd-x64: 1.29.2 + lightningcss-linux-arm-gnueabihf: 1.29.2 + lightningcss-linux-arm64-gnu: 1.29.2 + lightningcss-linux-arm64-musl: 1.29.2 + lightningcss-linux-x64-gnu: 1.29.2 + lightningcss-linux-x64-musl: 1.29.2 + lightningcss-win32-arm64-msvc: 1.29.2 + lightningcss-win32-x64-msvc: 1.29.2 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + map-obj@4.3.0: {} + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + napi-postinstall@0.2.3: {} + + natural-compare@1.4.0: {} + + next@15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + '@next/env': 15.3.1 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.15 + busboy: 1.6.0 + caniuse-lite: 1.0.30001716 + postcss: 8.4.31 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + styled-jsx: 5.1.6(react@19.1.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.3.1 + '@next/swc-darwin-x64': 15.3.1 + '@next/swc-linux-arm64-gnu': 15.3.1 + '@next/swc-linux-arm64-musl': 15.3.1 + '@next/swc-linux-x64-gnu': 15.3.1 + '@next/swc-linux-x64-musl': 15.3.1 + '@next/swc-win32-arm64-msvc': 15.3.1 + '@next/swc-win32-x64-msvc': 15.3.1 + sharp: 0.34.1 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + + react-is@16.13.1: {} + + react@19.1.0: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.26.0: {} + + semver@6.3.1: {} + + semver@7.7.1: {} + + server-only@0.0.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + sharp@0.34.1: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.1 + '@img/sharp-darwin-x64': 0.34.1 + '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-linux-arm': 0.34.1 + '@img/sharp-linux-arm64': 0.34.1 + '@img/sharp-linux-s390x': 0.34.1 + '@img/sharp-linux-x64': 0.34.1 + '@img/sharp-linuxmusl-arm64': 0.34.1 + '@img/sharp-linuxmusl-x64': 0.34.1 + '@img/sharp-wasm32': 0.34.1 + '@img/sharp-win32-ia32': 0.34.1 + '@img/sharp-win32-x64': 0.34.1 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + snakecase-keys@8.0.1: + dependencies: + map-obj: 4.3.0 + snake-case: 3.0.4 + type-fest: 4.40.1 + + source-map-js@1.2.1: {} + + stable-hash@0.0.5: {} + + std-env@3.9.0: {} + + streamsearch@1.1.0: {} + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.23.9 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + styled-jsx@5.1.6(react@19.1.0): + dependencies: + client-only: 0.0.1 + react: 19.1.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + swr@2.3.3(react@19.1.0): + dependencies: + dequal: 2.0.3 + react: 19.1.0 + use-sync-external-store: 1.5.0(react@19.1.0) + + tailwindcss@4.1.5: {} + + tapable@2.2.1: {} + + tinyglobby@0.2.13: + dependencies: + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@0.0.3: {} + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@4.40.1: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@5.8.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.19.8: {} + + unrs-resolver@1.7.2: + dependencies: + napi-postinstall: 0.2.3 + optionalDependencies: + '@unrs/resolver-binding-darwin-arm64': 1.7.2 + '@unrs/resolver-binding-darwin-x64': 1.7.2 + '@unrs/resolver-binding-freebsd-x64': 1.7.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.7.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.7.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.7.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.7.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.7.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.7.2 + '@unrs/resolver-binding-linux-x64-musl': 1.7.2 + '@unrs/resolver-binding-wasm32-wasi': 1.7.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.7.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.7.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.7.2 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-sync-external-store@1.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + ws@8.18.1: {} + + yocto-queue@0.1.0: {} diff --git a/examples/clerk/postcss.config.mjs b/examples/clerk/postcss.config.mjs new file mode 100644 index 00000000000..c7bcb4b1ee1 --- /dev/null +++ b/examples/clerk/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/examples/clerk/public/file.svg b/examples/clerk/public/file.svg new file mode 100644 index 00000000000..004145cddf3 --- /dev/null +++ b/examples/clerk/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/clerk/public/globe.svg b/examples/clerk/public/globe.svg new file mode 100644 index 00000000000..567f17b0d7c --- /dev/null +++ b/examples/clerk/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/clerk/public/next.svg b/examples/clerk/public/next.svg new file mode 100644 index 00000000000..5174b28c565 --- /dev/null +++ b/examples/clerk/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/clerk/public/vercel.svg b/examples/clerk/public/vercel.svg new file mode 100644 index 00000000000..77053960334 --- /dev/null +++ b/examples/clerk/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/clerk/public/window.svg b/examples/clerk/public/window.svg new file mode 100644 index 00000000000..b2b2a44f6eb --- /dev/null +++ b/examples/clerk/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/clerk/supabase/.gitignore b/examples/clerk/supabase/.gitignore new file mode 100644 index 00000000000..a3ad88055b7 --- /dev/null +++ b/examples/clerk/supabase/.gitignore @@ -0,0 +1,4 @@ +# Supabase +.branches +.temp +.env diff --git a/examples/clerk/supabase/config.toml b/examples/clerk/supabase/config.toml new file mode 100644 index 00000000000..ff6288b4ba3 --- /dev/null +++ b/examples/clerk/supabase/config.toml @@ -0,0 +1,161 @@ +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "clerk" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. public and storage are always included. +schemas = ["public", "storage", "graphql_public"] +# Extra schemas to add to the search_path of every request. public is always included. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 15 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv6) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = true +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }} ." + +# Use pre-defined map of phone number to OTP for testing. +[auth.sms.test_otp] +# 4152127777 = "123456" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +[auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" + +[analytics] +enabled = false +port = 54327 +vector_port = 54328 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/examples/clerk/supabase/migrations/20250501155648_setup_database.sql b/examples/clerk/supabase/migrations/20250501155648_setup_database.sql new file mode 100644 index 00000000000..535224a8ae4 --- /dev/null +++ b/examples/clerk/supabase/migrations/20250501155648_setup_database.sql @@ -0,0 +1,35 @@ +create table secured_table( + id uuid primary key default uuid_generate_v4(), + created_at timestamp with time zone default now(), + organization_id text not null +); + +alter table secured_table +enable row level security; + +create policy "Only organization admins can insert in table" +on secured_table +for insert +to authenticated +with check ( + (((select auth.jwt()->>'org_role') = 'org:admin') or ((select auth.jwt()->'o'->>'rol') = 'admin')) + and + (organization_id = (select coalesce(auth.jwt()->>'org_id', auth.jwt()->'o'->>'id'))) +); + +create policy "Users can select from their own organization" +on secured_table +for select +to authenticated +using ( + (organization_id = (select coalesce(auth.jwt()->>'org_id', auth.jwt()->'o'->>'id'))) +); + +create policy "Only users that have passed second factor verification can read from table" +on secured_table +as restrictive +for select +to authenticated +using ( + ((select auth.jwt()->'fva'->>1) != '-1') +); diff --git a/examples/clerk/supabase/seed.sql b/examples/clerk/supabase/seed.sql new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/clerk/tsconfig.json b/examples/clerk/tsconfig.json new file mode 100644 index 00000000000..d8b93235f20 --- /dev/null +++ b/examples/clerk/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From 5718684991a908f054fa44aa10ea21e688f939f7 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Fri, 2 May 2025 11:25:35 -0400 Subject: [PATCH 79/86] chore (content api): generate types from root schema (#35283) Ensure that resolvers are properly typed by adding a script to generate types from the root schema. --- apps/docs/.gitignore | 1 + apps/docs/codegen.ts | 13 + apps/docs/package.json | 13 +- apps/docs/resources/rootSchema.ts | 7 +- apps/docs/scripts/graphqlSchema.ts | 27 + pnpm-lock.yaml | 1548 +++++++++++++++++++++++++++- 6 files changed, 1586 insertions(+), 23 deletions(-) create mode 100644 apps/docs/codegen.ts create mode 100644 apps/docs/scripts/graphqlSchema.ts diff --git a/apps/docs/.gitignore b/apps/docs/.gitignore index 6eeb1dafe26..157f35a66e2 100644 --- a/apps/docs/.gitignore +++ b/apps/docs/.gitignore @@ -20,6 +20,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +**/__generated__/**/* **/*/generated/**/* !**/*/generated/.gitkeep !**/*/generated/**/.gitkeep diff --git a/apps/docs/codegen.ts b/apps/docs/codegen.ts new file mode 100644 index 00000000000..6b52a4cc4c5 --- /dev/null +++ b/apps/docs/codegen.ts @@ -0,0 +1,13 @@ +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = { + overwrite: true, + schema: './__generated__/schema.graphql', + generates: { + '__generated__/graphql.ts': { + plugins: ['typescript', 'typescript-resolvers'], + }, + }, +} + +export default config diff --git a/apps/docs/package.json b/apps/docs/package.json index b145e54337f..2e1b2d463f7 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -4,11 +4,11 @@ "private": true, "scripts": { "preinstall": "npx only-allow pnpm", - "predev": "pnpm run codegen:references && pnpm run codegen:examples", + "predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples", "dev": "concurrently --kill-others \"next dev --port 3001\" \"pnpm run dev:watch:troubleshooting\"", "dev:watch:troubleshooting": "node ./scripts/troubleshooting/watch.mjs", "dev:secrets:pull": "AWS_PROFILE=supabase-dev node ../../scripts/getSecrets.js -n local/docs", - "prebuild": "pnpm run codegen:references && pnpm run codegen:examples", + "prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples", "build": "next build", "build:analyze": "ANALYZE=true next build", "build:sitemap": "tsx ./internals/generate-sitemap.ts", @@ -17,6 +17,7 @@ "start": "next start", "lint": "next lint", "lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml", + "pretypecheck": "pnpm run codegen:graphql", "typecheck": "tsc --noEmit", "pretest": "pnpm run codegen:examples", "test": "vitest --exclude \"**/*.smoke.test.ts\"", @@ -27,9 +28,10 @@ "last-changed": "tsx scripts/last-changed.ts", "last-changed:reset": "pnpm run last-changed -- --reset", "codegen:examples": "cp -r ../../examples ./examples", - "codegen:references": "tsx features/docs/Reference.generated.script.ts", "codemod:frontmatter": "node ./scripts/codemod/mdx-meta.mjs && prettier --write \"content/**/*.mdx\"", - "clean": "rimraf node_modules features/docs/generated examples" + "codegen:graphql": "tsx ./scripts/graphqlSchema.ts && graphql-codegen --config codegen.ts", + "codegen:references": "tsx features/docs/Reference.generated.script.ts", + "clean": "rimraf node_modules features/docs/generated examples __generated__" }, "dependencies": { "@har-sdk/openapi-sampler": "^2.2.0", @@ -114,6 +116,9 @@ "devDependencies": { "@aws-sdk/client-secrets-manager": "^3.410.0", "@graphiql/toolkit": "^0.9.1", + "@graphql-codegen/cli": "5.0.5", + "@graphql-codegen/typescript": "4.1.6", + "@graphql-codegen/typescript-resolvers": "4.5.0", "@supabase/supa-mdx-lint": "0.2.6-alpha", "@types/common-tags": "^1.8.4", "@types/estree": "1.0.5", diff --git a/apps/docs/resources/rootSchema.ts b/apps/docs/resources/rootSchema.ts index 05c5d6d0f1b..ac79f0457ff 100644 --- a/apps/docs/resources/rootSchema.ts +++ b/apps/docs/resources/rootSchema.ts @@ -5,10 +5,13 @@ import { GraphQLString, printSchema, } from 'graphql' +import { RootQueryTypeResolvers } from '~/__generated__/graphql' -const GRAPHQL_FIELD_INTROSPECT = 'schema' +const GRAPHQL_FIELD_INTROSPECT = 'schema' as const -async function resolveIntrospect() { +type IntrospectResolver = RootQueryTypeResolvers[typeof GRAPHQL_FIELD_INTROSPECT] + +const resolveIntrospect: IntrospectResolver = async () => { const schema = printSchema(rootGraphQLSchema) return schema } diff --git a/apps/docs/scripts/graphqlSchema.ts b/apps/docs/scripts/graphqlSchema.ts new file mode 100644 index 00000000000..17b03215ed7 --- /dev/null +++ b/apps/docs/scripts/graphqlSchema.ts @@ -0,0 +1,27 @@ +import { printSchema } from 'graphql' +import fs from 'node:fs' +import path from 'node:path' +import { rootGraphQLSchema } from '../resources/rootSchema' + +async function generateGraphQLSchema() { + try { + const schemaString = printSchema(rootGraphQLSchema) + + const outputDir = path.resolve(__dirname, '../__generated__') + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }) + } + + const outputPath = path.resolve(outputDir, 'schema.graphql') + fs.writeFileSync(outputPath, schemaString) + + console.log(`✅ Successfully generated GraphQL schema at ${outputPath}`) + } catch (error) { + console.error('🚨 Error generating GraphQL schema:', error) + process.exit(1) + } +} + +if (require.main === module) { + generateGraphQLSchema() +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b294548e12..02fe9ee795f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -365,7 +365,7 @@ importers: version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: ^1.19.1 - version: 1.19.1(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)) + version: 1.19.1(next@14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)) openai: specifier: ^4.20.1 version: 4.71.1(encoding@0.1.13)(zod@3.23.8) @@ -453,7 +453,16 @@ importers: version: 3.468.0 '@graphiql/toolkit': specifier: ^0.9.1 - version: 0.9.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0) + version: 0.9.1(@types/node@20.12.11)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0) + '@graphql-codegen/cli': + specifier: 5.0.5 + version: 5.0.5(@parcel/watcher@2.5.1)(@types/node@20.12.11)(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0)(supports-color@8.1.1)(typescript@5.5.2) + '@graphql-codegen/typescript': + specifier: 4.1.6 + version: 4.1.6(encoding@0.1.13)(graphql@16.10.0) + '@graphql-codegen/typescript-resolvers': + specifier: 4.5.0 + version: 4.5.0(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0) '@supabase/supa-mdx-lint': specifier: 0.2.6-alpha version: 0.2.6-alpha @@ -513,7 +522,7 @@ importers: version: 13.2.2 graphiql: specifier: ^3.8.3 - version: 3.8.3(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.8.3(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) openapi-types: specifier: ^12.1.3 version: 12.1.3 @@ -2261,6 +2270,12 @@ packages: resolution: {integrity: sha512-C90iyzm/jLV7Lomv2UzwWUzRv9WZr1oRsFRKsX5HjQL4EXrbi9H/RtBkjCP+NF+ABZXUKpAa4F1dkoTaea4zHg==} hasBin: true + '@ardatan/relay-compiler@12.0.3': + resolution: {integrity: sha512-mBDFOGvAoVlWaWqs3hm1AciGHSQE1rqFc/liZTyYz/Oek9yZdT5H26pH2zAFuEiTiBVPPyMuqf5VjOFPI2DGsQ==} + hasBin: true + peerDependencies: + graphql: '*' + '@aws-crypto/crc32@3.0.0': resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} @@ -2509,6 +2524,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-import-assertions@7.26.0': + resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.25.9': resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} engines: {node: '>=6.9.0'} @@ -2801,6 +2822,18 @@ packages: '@emotion/unitless@0.8.0': resolution: {integrity: sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==} + '@envelop/core@5.2.3': + resolution: {integrity: sha512-KfoGlYD/XXQSc3BkM1/k15+JQbkQ4ateHazeZoWl9P71FsLTDXSjGy6j7QqfhpIDSbxNISqhPMfZHYSbDFOofQ==} + engines: {node: '>=18.0.0'} + + '@envelop/instrumentation@1.0.0': + resolution: {integrity: sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==} + engines: {node: '>=18.0.0'} + + '@envelop/types@5.2.1': + resolution: {integrity: sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==} + engines: {node: '>=18.0.0'} + '@esbuild-plugins/node-resolve@0.2.2': resolution: {integrity: sha512-+t5FdX3ATQlb53UFDBRb4nqjYBz492bIrnVWvpQHpzZlu9BQL5HasMZhqc409ygUwOWCXZhrWr6NyZ6T6Y+cxw==} peerDependencies: @@ -3137,6 +3170,9 @@ packages: '@fastify/ajv-compiler@3.6.0': resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==} + '@fastify/busboy@3.1.1': + resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} + '@fastify/cors@9.0.1': resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==} @@ -3207,6 +3243,242 @@ packages: graphql-ws: optional: true + '@graphql-codegen/add@5.0.3': + resolution: {integrity: sha512-SxXPmramkth8XtBlAHu4H4jYcYXM/o3p01+psU+0NADQowA8jtYkK6MW5rV6T+CxkEaNZItfSmZRPgIuypcqnA==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/cli@5.0.5': + resolution: {integrity: sha512-9p9SI5dPhJdyU+O6p1LUqi5ajDwpm6pUhutb1fBONd0GZltLFwkgWFiFtM6smxkYXlYVzw61p1kTtwqsuXO16w==} + engines: {node: '>=16'} + hasBin: true + peerDependencies: + '@parcel/watcher': ^2.1.0 + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + '@parcel/watcher': + optional: true + + '@graphql-codegen/client-preset@4.8.0': + resolution: {integrity: sha512-IVtTl7GsPMbQihk5+l5fDYksnPPOoC52sKxzquyIyuecZLEB7W3nNLV29r6+y+tjXTRPA774FR7CHGA2adzhjw==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-sock: ^1.0.0 + + '@graphql-codegen/core@4.0.2': + resolution: {integrity: sha512-IZbpkhwVqgizcjNiaVzNAzm/xbWT6YnGgeOLwVjm4KbJn3V2jchVtuzHH09G5/WkkLSk2wgbXNdwjM41JxO6Eg==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/gql-tag-operations@4.0.17': + resolution: {integrity: sha512-2pnvPdIG6W9OuxkrEZ6hvZd142+O3B13lvhrZ48yyEBh2ujtmKokw0eTwDHtlXUqjVS0I3q7+HB2y12G/m69CA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/plugin-helpers@5.1.0': + resolution: {integrity: sha512-Y7cwEAkprbTKzVIe436TIw4w03jorsMruvCvu0HJkavaKMQbWY+lQ1RIuROgszDbxAyM35twB5/sUvYG5oW+yg==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/schema-ast@4.1.0': + resolution: {integrity: sha512-kZVn0z+th9SvqxfKYgztA6PM7mhnSZaj4fiuBWvMTqA+QqQ9BBed6Pz41KuD/jr0gJtnlr2A4++/0VlpVbCTmQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/typed-document-node@5.1.1': + resolution: {integrity: sha512-Bp/BrMZDKRwzuVeLv+pSljneqONM7gqu57ZaV34Jbncu2hZWMRDMfizTKghoEwwZbRCYYfJO9tA0sYVVIfI1kg==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/typescript-operations@4.6.0': + resolution: {integrity: sha512-/EltSdE/uPoEAblRTVLABVDhsrE//Kl3pCflyG1PWl4gWL9/OzQXYGjo6TF6bPMVn/QBWoO0FeboWf+bk84SXA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-sock: ^1.0.0 + + '@graphql-codegen/typescript-resolvers@4.5.0': + resolution: {integrity: sha512-+zLe+xzATHTfi4NG7FyM+RiTzFmDqh23McauCnJ9w8n11PMtUHfneu54iryJrJO3u/exzpwkQFOZcIxRYEI4Tg==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-sock: ^1.0.0 + + '@graphql-codegen/typescript@4.1.6': + resolution: {integrity: sha512-vpw3sfwf9A7S+kIUjyFxuvrywGxd4lmwmyYnnDVjVE4kSQ6Td3DpqaPTy8aNQ6O96vFoi/bxbZS2BW49PwSUUA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-codegen/visitor-plugin-common@5.8.0': + resolution: {integrity: sha512-lC1E1Kmuzi3WZUlYlqB4fP6+CvbKH9J+haU1iWmgsBx5/sO2ROeXJG4Dmt8gP03bI2BwjiwV5WxCEMlyeuzLnA==} + engines: {node: '>=16'} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@graphql-hive/signal@1.0.0': + resolution: {integrity: sha512-RiwLMc89lTjvyLEivZ/qxAC5nBHoS2CtsWFSOsN35sxG9zoo5Z+JsFHM8MlvmO9yt+MJNIyC5MLE1rsbOphlag==} + engines: {node: '>=18.0.0'} + + '@graphql-tools/apollo-engine-loader@8.0.20': + resolution: {integrity: sha512-m5k9nXSyjq31yNsEqDXLyykEjjn3K3Mo73oOKI+Xjy8cpnsgbT4myeUJIYYQdLrp7fr9Y9p7ZgwT5YcnwmnAbA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/batch-execute@9.0.15': + resolution: {integrity: sha512-qlWUl6yi87FU5WvyJ0uD81R4Y30oQIuW3mJCjOrEvifyT+f/rEqSZFOhYrofYoZAoTcwqOhy6WgH+b9+AtRYjA==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/code-file-loader@8.1.20': + resolution: {integrity: sha512-GzIbjjWJIc04KWnEr8VKuPe0FA2vDTlkaeub5p4lLimljnJ6C0QSkOyCUnFmsB9jetQcHm0Wfmn/akMnFUG+wA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/delegate@10.2.17': + resolution: {integrity: sha512-z+LpZrTQCEXA4fbdJcSsvhaMqT4xi/O8B0mP30ENGyTbSfa20QamOQx9jgCiw2ii/ucwxfGMhygwlpZG36EU4w==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/documents@1.0.1': + resolution: {integrity: sha512-aweoMH15wNJ8g7b2r4C4WRuJxZ0ca8HtNO54rkye/3duxTkW4fGBEutCx03jCIr5+a1l+4vFJNP859QnAVBVCA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-common@0.0.4': + resolution: {integrity: sha512-SEH/OWR+sHbknqZyROCFHcRrbZeUAyjCsgpVWCRjqjqRbiJiXq6TxNIIOmpXgkrXWW/2Ev4Wms6YSGJXjdCs6Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-graphql-ws@2.0.5': + resolution: {integrity: sha512-gI/D9VUzI1Jt1G28GYpvm5ckupgJ5O8mi5Y657UyuUozX34ErfVdZ81g6oVcKFQZ60LhCzk7jJeykK48gaLhDw==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-http@1.3.3': + resolution: {integrity: sha512-LIy+l08/Ivl8f8sMiHW2ebyck59JzyzO/yF9SFS4NH6MJZUezA1xThUXCDIKhHiD56h/gPojbkpcFvM2CbNE7A==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor-legacy-ws@1.1.17': + resolution: {integrity: sha512-TvltY6eL4DY1Vt66Z8kt9jVmNcI+WkvVPQZrPbMCM3rv2Jw/sWvSwzUBezRuWX0sIckMifYVh23VPcGBUKX/wg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/executor@1.4.7': + resolution: {integrity: sha512-U0nK9jzJRP9/9Izf1+0Gggd6K6RNRsheFo1gC/VWzfnsr0qjcOSS9qTjY0OTC5iTPt4tQ+W5Zpw/uc7mebI6aA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/git-loader@8.0.24': + resolution: {integrity: sha512-ypLC9N2bKNC0QNbrEBTbWKwbV607f7vK2rSGi9uFeGr8E29tWplo6or9V/+TM0ZfIkUsNp/4QX/zKTgo8SbwQg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/github-loader@8.0.20': + resolution: {integrity: sha512-Icch8bKZ1iP3zXCB9I0ded1hda9NPskSSalw7ZM21kXvLiOR5nZhdqPF65gCFkIKo+O4NR4Bp51MkKj+wl+vpg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/graphql-file-loader@8.0.19': + resolution: {integrity: sha512-kyEZL4rRJ5LelfCXL3GLgbMiu5Zd7memZaL8ZxPXGI7DA8On1e5IVBH3zZJwf7LzhjSVnPaHM7O/bRzGvTbXzQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/graphql-tag-pluck@8.3.19': + resolution: {integrity: sha512-LEw/6IYOUz48HjbWntZXDCzSXsOIM1AyWZrlLoJOrA8QAlhFd8h5Tny7opCypj8FO9VvpPFugWoNDh5InPOEQA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/import@7.0.18': + resolution: {integrity: sha512-1tw1/1QLB0n5bPWfIrhCRnrHIlbMvbwuifDc98g4FPhJ7OXD+iUQe+IpmD5KHVwYWXWhZOuJuq45DfV/WLNq3A==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/json-file-loader@8.0.18': + resolution: {integrity: sha512-JjjIxxewgk8HeMR3npR3YbOkB7fxmdgmqB9kZLWdkRKBxrRXVzhryyq+mhmI0Evzt6pNoHIc3vqwmSctG2sddg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/load@8.1.0': + resolution: {integrity: sha512-OGfOm09VyXdNGJS/rLqZ6ztCiG2g6AMxhwtET8GZXTbnjptFc17GtKwJ3Jv5w7mjJ8dn0BHydvIuEKEUK4ciYw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/merge@9.0.24': + resolution: {integrity: sha512-NzWx/Afl/1qHT3Nm1bghGG2l4jub28AdvtG11PoUlmjcIjnFBJMv4vqL0qnxWe8A82peWo4/TkVdjJRLXwgGEw==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/optimize@2.0.0': + resolution: {integrity: sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/prisma-loader@8.0.17': + resolution: {integrity: sha512-fnuTLeQhqRbA156pAyzJYN0KxCjKYRU5bz1q/SKOwElSnAU4k7/G1kyVsWLh7fneY78LoMNH5n+KlFV8iQlnyg==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/relay-operation-optimizer@7.0.19': + resolution: {integrity: sha512-xnjLpfzw63yIX1bo+BVh4j1attSwqEkUbpJ+HAhdiSUa3FOQFfpWgijRju+3i87CwhjBANqdTZbcsqLT1hEXig==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/schema@10.0.23': + resolution: {integrity: sha512-aEGVpd1PCuGEwqTXCStpEkmheTHNdMayiIKH1xDWqYp9i8yKv9FRDgkGrY4RD8TNxnf7iII+6KOBGaJ3ygH95A==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/url-loader@8.0.31': + resolution: {integrity: sha512-QGP3py6DAdKERHO5D38Oi+6j+v0O3rkBbnLpyOo87rmIRbwE6sOkL5JeHegHs7EEJ279fBX6lMt8ry0wBMGtyA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/utils@10.8.6': + resolution: {integrity: sha512-Alc9Vyg0oOsGhRapfL3xvqh1zV8nKoFUdtLhXX7Ki4nClaIJXckrA86j+uxEuG3ic6j4jlM1nvcWXRn/71AVLQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/wrap@10.0.35': + resolution: {integrity: sha512-qBga3wo7+GqY+ClGexiyRz9xgy1RWozZryTuGX8usGWPa4wKi/tJS4rKWQQesgB3Fh//SZUCRA5u2nwZaZQw1Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@gregnr/postgres-meta@0.82.0-dev.2': resolution: {integrity: sha512-RLCMcshNBZi1FBA60PfQTLCvfuYmNXVzRe147j5mlOIXvq+o9K/pJgm49GOEBBYfvH+bwcs3w/mBRCh7BNveng==} engines: {node: '>=20', npm: '>=9'} @@ -5485,6 +5757,9 @@ packages: resolution: {integrity: sha512-Lzea25WqwxVK5+aCiq/pr7lUFdsZPYSqNzl05Z4jEtuP1DEIxJNG31ID75dZt30pPtyxjaa/dBuccruYlYflzw==} engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@repeaterjs/repeater@3.0.6': + resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} + '@resvg/resvg-wasm@2.4.0': resolution: {integrity: sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg==} engines: {node: '>= 10'} @@ -6970,6 +7245,22 @@ packages: '@webgpu/types@0.1.49': resolution: {integrity: sha512-NMmS8/DofhH/IFeW+876XrHVWel+J/vdcFCHLDqeJgkH9x0DeiwjVd8LcBdaxdG/T7Rf8VUAYsA8X1efMzLjRQ==} + '@whatwg-node/disposablestack@0.0.6': + resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/fetch@0.10.6': + resolution: {integrity: sha512-6uzhO2aQ757p3bSHcemA8C4pqEXuyBqyGAM7cYpO0c6/igRMV9As9XL0W12h5EPYMclgr7FgjmbVQBoWEdJ/yA==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/node-fetch@0.7.18': + resolution: {integrity: sha512-IxKdVWfZYasGiyxBcsROxq6FmDQu3MNNiOYJ/yqLKhe+Qq27IIWsK7ItbjS2M9L5aM5JxjWkIS7JDh7wnsn+CQ==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/promise-helpers@1.3.1': + resolution: {integrity: sha512-D+OwTEunoQhVHVToD80dPhfz9xgPLqJyEA3F5jCRM14A2u8tBBQVdZekqfqx6ZAfZ+POT4Hb0dn601UKMsvADw==} + engines: {node: '>=16.0.0'} + '@xobotyi/scrollbar-width@1.9.5': resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} @@ -7263,6 +7554,9 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + assert-plus@1.0.0: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} @@ -7278,6 +7572,10 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + astring@1.8.6: resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} hasBin: true @@ -7299,6 +7597,10 @@ packages: resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} engines: {node: '>=4'} + auto-bind@4.0.0: + resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} + engines: {node: '>=8'} + autolinker@0.28.1: resolution: {integrity: sha512-zQAFO1Dlsn69eXaO6+7YZc+v84aquQKbwpzCE3L0stj56ERn9hutFxPopViLjo9G+rWwjozRhgS5KJ25Xy19cQ==} @@ -7391,6 +7693,9 @@ packages: bintrees@1.0.2: resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bl@5.1.0: resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} @@ -7424,6 +7729,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-crc32@1.0.0: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} @@ -7435,6 +7743,9 @@ packages: resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} engines: {node: '>=4'} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -7531,6 +7842,9 @@ packages: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case-all@1.0.15: + resolution: {integrity: sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==} + change-case@4.1.2: resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} @@ -7558,6 +7872,9 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -7622,6 +7939,10 @@ packages: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + cli-cursor@4.0.0: resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -7630,6 +7951,14 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -7896,6 +8225,15 @@ packages: typescript: optional: true + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -7923,6 +8261,13 @@ packages: resolution: {integrity: sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg==} hasBin: true + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cross-inspect@1.0.1: + resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==} + engines: {node: '>=16.0.0'} + cross-spawn@6.0.6: resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} engines: {node: '>=4.8'} @@ -8098,6 +8443,9 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + dataloader@2.2.3: + resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} + date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -8255,6 +8603,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dependency-graph@0.11.0: + resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} + engines: {node: '>= 0.6.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -8271,6 +8623,10 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} @@ -8393,6 +8749,10 @@ packages: resolution: {integrity: sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==} engines: {node: '>=4'} + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -8905,6 +9265,10 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + extsprintf@1.4.1: resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} engines: {'0': node >=0.6.0} @@ -9001,6 +9365,15 @@ packages: fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fbjs-css-vars@1.0.2: + resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} + + fbjs@3.0.5: + resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + fdir@6.4.3: resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} peerDependencies: @@ -9019,6 +9392,10 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -9352,6 +9729,16 @@ packages: react: ^16.8.0 || ^17 || ^18 react-dom: ^16.8.0 || ^17 || ^18 + graphql-config@5.1.4: + resolution: {integrity: sha512-ObdBeL3ycddHrNbFvhGZ12pK8jUzWvvyN2A+6ij3XrtLH/KrkXt+BboEAEgXmeUrTcD5RjJnz8IZu3Cgc/oX/w==} + engines: {node: '>= 16.0.0'} + peerDependencies: + cosmiconfig-toml-loader: ^1.0.0 + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + cosmiconfig-toml-loader: + optional: true + graphql-language-service@5.2.0: resolution: {integrity: sha512-o/ZgTS0pBxWm3hSF4+6GwiV1//DxzoLWEbS38+jqpzzy1d/QXBidwQuVYTOksclbtOJZ3KR/tZ8fi/tI6VpVMg==} hasBin: true @@ -9364,6 +9751,23 @@ packages: peerDependencies: graphql: ^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2 + graphql-request@6.1.0: + resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} + peerDependencies: + graphql: 14 - 16 + + graphql-sock@1.0.1: + resolution: {integrity: sha512-gSA0CXdNMvNlpEnH2GY1//SUY7laDsAn51sDm4yh6TTH5UkfbNINydyUAoMHHkAaCaOLNXELQmu3GVcSOw4twg==} + hasBin: true + peerDependencies: + graphql: 15.x || 16.x || 17.x + + graphql-tag@2.12.6: + resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-validation-complexity@0.4.2: resolution: {integrity: sha512-4tzmN/70a06c2JH5fvISkoLX6oBDpqK22cvr2comge3HZHtBLD3n5Sl6MnQYMVhQqKGlpZWcCgD00MnyKNzYYg==} peerDependencies: @@ -9375,6 +9779,22 @@ packages: peerDependencies: graphql: '>=0.11 <=16' + graphql-ws@6.0.4: + resolution: {integrity: sha512-8b4OZtNOvv8+NZva8HXamrc0y1jluYC0+13gdh7198FKjVzXyTvVc95DCwGzaKEfn3YuWZxUqjJlHe3qKM/F2g==} + engines: {node: '>=20'} + peerDependencies: + '@fastify/websocket': ^10 || ^11 + graphql: ^15.10.1 || ^16 + uWebSockets.js: ^20 + ws: ^8 + peerDependenciesMeta: + '@fastify/websocket': + optional: true + uWebSockets.js: + optional: true + ws: + optional: true + graphql@16.10.0: resolution: {integrity: sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -9658,6 +10078,10 @@ packages: hyphenate-style-name@1.0.4: resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -9683,6 +10107,10 @@ packages: immutability-helper@3.1.1: resolution: {integrity: sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==} + immutable@3.7.6: + resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} + engines: {node: '>=0.8.0'} + immutable@4.3.4: resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} @@ -9690,6 +10118,10 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} + import-from@4.0.0: + resolution: {integrity: sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==} + engines: {node: '>=12.2'} + import-in-the-middle@1.13.1: resolution: {integrity: sha512-k2V9wNm9B+ysuelDTHjI9d5KPc4l8zAZTGqj+pcynvWkypZd857ryzN8jNC7Pg2YZXNMJcHRPpaDyCBbNyVRpA==} @@ -9740,6 +10172,10 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 + inquirer@8.2.6: + resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + engines: {node: '>=12.0.0'} + internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} @@ -9751,6 +10187,9 @@ packages: intersection-observer@0.10.0: resolution: {integrity: sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ==} + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ioredis@5.6.0: resolution: {integrity: sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==} engines: {node: '>=12.22.0'} @@ -9769,6 +10208,10 @@ packages: iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-absolute@1.0.0: + resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} + engines: {node: '>=0.10.0'} + is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} @@ -9886,6 +10329,10 @@ packages: engines: {node: '>=14.16'} hasBin: true + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + is-interactive@2.0.0: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} @@ -9893,6 +10340,9 @@ packages: is-lambda@1.0.1: resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + is-lower-case@2.0.2: + resolution: {integrity: sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==} + is-map@2.0.2: resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} @@ -9963,6 +10413,10 @@ packages: resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} engines: {node: '>=12'} + is-relative@1.0.0: + resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} + engines: {node: '>=0.10.0'} + is-set@2.0.2: resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} @@ -9990,10 +10444,21 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} + is-unc-path@1.0.0: + resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} + engines: {node: '>=0.10.0'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} + is-upper-case@2.0.2: + resolution: {integrity: sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==} + is-weakmap@2.0.1: resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} @@ -10007,6 +10472,10 @@ packages: resolution: {integrity: sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==} engines: {node: '>=12.13'} + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -10048,6 +10517,11 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -10226,6 +10700,10 @@ packages: json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json-to-pretty-yaml@1.2.2: + resolution: {integrity: sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==} + engines: {node: '>= 0.2.0'} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -10356,6 +10834,15 @@ packages: resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} hasBin: true + listr2@4.0.5: + resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} + engines: {node: '>=12'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + load-json-file@4.0.0: resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} engines: {node: '>=4'} @@ -10425,6 +10912,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.template@4.5.0: resolution: {integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==} deprecated: This package is deprecated. Use https://socket.dev/npm/package/eta instead. @@ -10441,10 +10931,18 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + log-symbols@5.1.0: resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} engines: {node: '>=12'} + log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} @@ -10461,6 +10959,9 @@ packages: loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + lower-case-first@2.0.2: + resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -10530,6 +11031,10 @@ packages: resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} engines: {node: '>= 10'} + map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} @@ -11194,6 +11699,9 @@ packages: typescript: optional: true + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -11388,6 +11896,9 @@ packages: engines: {node: '>= 10.12.0'} hasBin: true + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-mock-http@1.0.0: resolution: {integrity: sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ==} @@ -11429,6 +11940,10 @@ packages: resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -11666,10 +12181,18 @@ packages: resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + ora@6.3.1: resolution: {integrity: sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + outdent@0.8.0: resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} @@ -11719,6 +12242,10 @@ packages: parse-entities@4.0.1: resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} + parse-filepath@1.0.2: + resolution: {integrity: sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==} + engines: {node: '>=0.8'} + parse-json@4.0.0: resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} engines: {node: '>=4'} @@ -11779,6 +12306,14 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-root-regex@0.1.2: + resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} + engines: {node: '>=0.10.0'} + + path-root@0.1.1: + resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} + engines: {node: '>=0.10.0'} + path-scurry@1.11.1: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} @@ -12181,6 +12716,9 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -12744,6 +13282,9 @@ packages: rehype-stringify@10.0.0: resolution: {integrity: sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ==} + relay-runtime@12.0.0: + resolution: {integrity: sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==} + remark-code-import@1.2.0: resolution: {integrity: sha512-fgwLruqlZbVOIhCJFjY+JDwPZhA4/eK3InJzN8Ox8UDdtudpG212JwtRj6la+lAzJU7JmSEyewZSukVZdknt3Q==} engines: {node: '>= 12'} @@ -12805,9 +13346,18 @@ packages: engines: {node: '>= 0.10.0'} hasBin: true + remedial@1.0.8: + resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==} + remove-accents@0.4.2: resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} + remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + + remove-trailing-spaces@1.0.9: + resolution: {integrity: sha512-xzG7w5IRijvIkHIjDk65URsJJ7k4J95wmcArY5PRcmjldIOl7oTvG8+X2Ag690R7SfwiOcHrWZKVc1Pp5WIOzA==} + rename-keys@1.2.0: resolution: {integrity: sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==} engines: {node: '>= 0.8.0'} @@ -12866,6 +13416,10 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + restore-cursor@4.0.0: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -12942,6 +13496,10 @@ packages: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -13007,6 +13565,9 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + scuid@1.1.0: + resolution: {integrity: sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==} + scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} @@ -13081,6 +13642,9 @@ packages: resolution: {integrity: sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==} engines: {node: '>=11.0'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -13155,6 +13719,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + signedsource@1.0.0: + resolution: {integrity: sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==} + simple-git@3.24.0: resolution: {integrity: sha512-QqAKee9Twv+3k8IFOFfPB2hnk6as6Y6ACUpwCtQvRYBAes23Wv3SZlHVobAzqcE8gfsisCvPw3HGW3HYM+VYYw==} @@ -13187,6 +13754,14 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + slugify@1.4.7: resolution: {integrity: sha512-tf+h5W1IrjNm/9rKKj0JU2MDMruiopx0jjVA5zCdBtcGjfp0+c5rHw/zADLC3IeKlGHtVbHtpfzvYA0OYT+HKg==} engines: {node: '>=8.0.0'} @@ -13282,6 +13857,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sponge-case@1.0.1: + resolution: {integrity: sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -13381,6 +13959,9 @@ packages: strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-env-interpolation@1.0.1: + resolution: {integrity: sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -13577,6 +14158,9 @@ packages: resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} hasBin: true + swap-case@2.0.2: + resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==} + swiper@11.0.7: resolution: {integrity: sha512-cDfglW1B6uSmB6eB6pNmzDTNLmZtu5bWWa1vak0RU7fOI9qHjMzl7gVBvYSl34b0RU2N11HxxETJqQ5LeqI1cA==} engines: {node: '>= 4.7.0'} @@ -13597,6 +14181,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + sync-fetch@0.6.0-2: + resolution: {integrity: sha512-c7AfkZ9udatCuAy9RSfiGPpeOKKUAUK5e1cXadLOGUjasdxqYqAK0jTNkM/FSEyJ3a5Ra27j/tw/PS0qLmaF/A==} + engines: {node: '>=18'} + system-architecture@0.1.0: resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} engines: {node: '>=18'} @@ -13699,6 +14287,13 @@ packages: through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + timeout-signal@2.0.0: + resolution: {integrity: sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==} + engines: {node: '>=16'} + tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} @@ -13740,6 +14335,13 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + title-case@3.0.3: + resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -13825,6 +14427,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-log@2.2.7: + resolution: {integrity: sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==} + ts-morph@18.0.0: resolution: {integrity: sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==} @@ -13992,6 +14597,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + ua-parser-js@1.0.40: + resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==} + hasBin: true + uc.micro@1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} @@ -14015,6 +14624,10 @@ packages: unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + unc-path-regex@0.1.2: + resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} + engines: {node: '>=0.10.0'} + uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} @@ -14142,6 +14755,10 @@ packages: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} + unixify@1.0.0: + resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} + engines: {node: '>=0.10.0'} + unplugin-utils@0.2.4: resolution: {integrity: sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==} engines: {node: '>=18.12.0'} @@ -14260,6 +14877,9 @@ packages: url-template@2.0.8: resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + urlpattern-polyfill@10.0.0: + resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -14560,6 +15180,10 @@ packages: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + whatwg-url@11.0.0: resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} engines: {node: '>=12'} @@ -14863,6 +15487,22 @@ snapshots: '@antfu/ni@23.3.1': {} + '@ardatan/relay-compiler@12.0.3(encoding@0.1.13)(graphql@16.10.0)': + dependencies: + '@babel/generator': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/runtime': 7.26.10 + chalk: 4.1.2 + fb-watchman: 2.0.2 + graphql: 16.10.0 + immutable: 3.7.6 + invariant: 2.2.4 + nullthrows: 1.1.1 + relay-runtime: 12.0.0(encoding@0.1.13) + signedsource: 1.0.0 + transitivePeerDependencies: + - encoding + '@aws-crypto/crc32@3.0.0': dependencies: '@aws-crypto/util': 3.0.0 @@ -15396,6 +16036,11 @@ snapshots: '@babel/core': 7.26.10(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.26.5 + '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.10(supports-color@8.1.1))': + dependencies: + '@babel/core': 7.26.10(supports-color@8.1.1) + '@babel/helper-plugin-utils': 7.26.5 + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.10(supports-color@8.1.1))': dependencies: '@babel/core': 7.26.10(supports-color@8.1.1) @@ -15922,6 +16567,23 @@ snapshots: '@emotion/unitless@0.8.0': {} + '@envelop/core@5.2.3': + dependencies: + '@envelop/instrumentation': 1.0.0 + '@envelop/types': 5.2.1 + '@whatwg-node/promise-helpers': 1.3.1 + tslib: 2.8.1 + + '@envelop/instrumentation@1.0.0': + dependencies: + '@whatwg-node/promise-helpers': 1.3.1 + tslib: 2.8.1 + + '@envelop/types@5.2.1': + dependencies: + '@whatwg-node/promise-helpers': 1.3.1 + tslib: 2.8.1 + '@esbuild-plugins/node-resolve@0.2.2(esbuild@0.25.2)(supports-color@8.1.1)': dependencies: '@types/resolve': 1.20.6 @@ -16117,6 +16779,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.12.0) fast-uri: 2.4.0 + '@fastify/busboy@3.1.1': {} + '@fastify/cors@9.0.1': dependencies: fastify-plugin: 4.5.1 @@ -16192,9 +16856,9 @@ snapshots: - '@types/react-dom' - graphql-ws - '@graphiql/react@0.28.2(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@graphiql/react@0.28.2(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@graphiql/toolkit': 0.11.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0) + '@graphiql/toolkit': 0.11.1(@types/node@20.12.11)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0) '@headlessui/react': 1.7.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -16221,13 +16885,13 @@ snapshots: - '@types/react-dom' - graphql-ws - '@graphiql/toolkit@0.11.1(@types/node@20.12.11)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)': + '@graphiql/toolkit@0.11.1(@types/node@20.12.11)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0)': dependencies: '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 graphql: 16.10.0 meros: 1.3.0(@types/node@20.12.11) optionalDependencies: - graphql-ws: 5.14.1(graphql@16.10.0) + graphql-ws: 6.0.4(graphql@16.10.0)(ws@8.18.1) transitivePeerDependencies: - '@types/node' @@ -16241,6 +16905,471 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@graphiql/toolkit@0.9.1(@types/node@20.12.11)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0)': + dependencies: + '@n1ru4l/push-pull-async-iterable-iterator': 3.2.0 + graphql: 16.10.0 + meros: 1.3.0(@types/node@20.12.11) + optionalDependencies: + graphql-ws: 6.0.4(graphql@16.10.0)(ws@8.18.1) + transitivePeerDependencies: + - '@types/node' + + '@graphql-codegen/add@5.0.3(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.6.2 + + '@graphql-codegen/cli@5.0.5(@parcel/watcher@2.5.1)(@types/node@20.12.11)(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0)(supports-color@8.1.1)(typescript@5.5.2)': + dependencies: + '@babel/generator': 7.27.0 + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + '@graphql-codegen/client-preset': 4.8.0(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0) + '@graphql-codegen/core': 4.0.2(graphql@16.10.0) + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-tools/apollo-engine-loader': 8.0.20(graphql@16.10.0) + '@graphql-tools/code-file-loader': 8.1.20(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/git-loader': 8.0.24(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/github-loader': 8.0.20(@types/node@20.12.11)(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/graphql-file-loader': 8.0.19(graphql@16.10.0) + '@graphql-tools/json-file-loader': 8.0.18(graphql@16.10.0) + '@graphql-tools/load': 8.1.0(graphql@16.10.0) + '@graphql-tools/prisma-loader': 8.0.17(@types/node@20.12.11)(encoding@0.1.13)(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/url-loader': 8.0.31(@types/node@20.12.11)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@whatwg-node/fetch': 0.10.6 + chalk: 4.1.2 + cosmiconfig: 8.3.6(typescript@5.5.2) + debounce: 1.2.1 + detect-indent: 6.1.0 + graphql: 16.10.0 + graphql-config: 5.1.4(@types/node@20.12.11)(graphql@16.10.0)(typescript@5.5.2) + inquirer: 8.2.6 + is-glob: 4.0.3 + jiti: 1.21.7 + json-to-pretty-yaml: 1.2.2 + listr2: 4.0.5 + log-symbols: 4.1.0 + micromatch: 4.0.8 + shell-quote: 1.8.1 + string-env-interpolation: 1.0.1 + ts-log: 2.2.7 + tslib: 2.8.1 + yaml: 2.4.5 + yargs: 17.7.2 + optionalDependencies: + '@parcel/watcher': 2.5.1 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - cosmiconfig-toml-loader + - encoding + - enquirer + - graphql-sock + - supports-color + - typescript + - uWebSockets.js + - utf-8-validate + + '@graphql-codegen/client-preset@4.8.0(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0)': + dependencies: + '@babel/helper-plugin-utils': 7.26.5 + '@babel/template': 7.27.0 + '@graphql-codegen/add': 5.0.3(graphql@16.10.0) + '@graphql-codegen/gql-tag-operations': 4.0.17(encoding@0.1.13)(graphql@16.10.0) + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-codegen/typed-document-node': 5.1.1(encoding@0.1.13)(graphql@16.10.0) + '@graphql-codegen/typescript': 4.1.6(encoding@0.1.13)(graphql@16.10.0) + '@graphql-codegen/typescript-operations': 4.6.0(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(encoding@0.1.13)(graphql@16.10.0) + '@graphql-tools/documents': 1.0.1(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) + graphql: 16.10.0 + graphql-sock: 1.0.1(graphql@16.10.0) + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/core@4.0.2(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-tools/schema': 10.0.23(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.6.2 + + '@graphql-codegen/gql-tag-operations@4.0.17(encoding@0.1.13)(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(encoding@0.1.13)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + auto-bind: 4.0.0 + graphql: 16.10.0 + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/plugin-helpers@5.1.0(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + change-case-all: 1.0.15 + common-tags: 1.8.2 + graphql: 16.10.0 + import-from: 4.0.0 + lodash: 4.17.21 + tslib: 2.6.2 + + '@graphql-codegen/schema-ast@4.1.0(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.6.2 + + '@graphql-codegen/typed-document-node@5.1.1(encoding@0.1.13)(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(encoding@0.1.13)(graphql@16.10.0) + auto-bind: 4.0.0 + change-case-all: 1.0.15 + graphql: 16.10.0 + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/typescript-operations@4.6.0(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-codegen/typescript': 4.1.6(encoding@0.1.13)(graphql@16.10.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(encoding@0.1.13)(graphql@16.10.0) + auto-bind: 4.0.0 + graphql: 16.10.0 + graphql-sock: 1.0.1(graphql@16.10.0) + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/typescript-resolvers@4.5.0(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-codegen/typescript': 4.1.6(encoding@0.1.13)(graphql@16.10.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(encoding@0.1.13)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + auto-bind: 4.0.0 + graphql: 16.10.0 + graphql-sock: 1.0.1(graphql@16.10.0) + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/typescript@4.1.6(encoding@0.1.13)(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-codegen/schema-ast': 4.1.0(graphql@16.10.0) + '@graphql-codegen/visitor-plugin-common': 5.8.0(encoding@0.1.13)(graphql@16.10.0) + auto-bind: 4.0.0 + graphql: 16.10.0 + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-codegen/visitor-plugin-common@5.8.0(encoding@0.1.13)(graphql@16.10.0)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.10.0) + '@graphql-tools/optimize': 2.0.0(graphql@16.10.0) + '@graphql-tools/relay-operation-optimizer': 7.0.19(encoding@0.1.13)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + auto-bind: 4.0.0 + change-case-all: 1.0.15 + dependency-graph: 0.11.0 + graphql: 16.10.0 + graphql-tag: 2.12.6(graphql@16.10.0) + parse-filepath: 1.0.2 + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + + '@graphql-hive/signal@1.0.0': {} + + '@graphql-tools/apollo-engine-loader@8.0.20(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@whatwg-node/fetch': 0.10.6 + graphql: 16.10.0 + sync-fetch: 0.6.0-2 + tslib: 2.8.1 + + '@graphql-tools/batch-execute@9.0.15(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@whatwg-node/promise-helpers': 1.3.1 + dataloader: 2.2.3 + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/code-file-loader@8.1.20(graphql@16.10.0)(supports-color@8.1.1)': + dependencies: + '@graphql-tools/graphql-tag-pluck': 8.3.19(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + globby: 11.1.0 + graphql: 16.10.0 + tslib: 2.8.1 + unixify: 1.0.0 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/delegate@10.2.17(graphql@16.10.0)': + dependencies: + '@graphql-tools/batch-execute': 9.0.15(graphql@16.10.0) + '@graphql-tools/executor': 1.4.7(graphql@16.10.0) + '@graphql-tools/schema': 10.0.23(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/promise-helpers': 1.3.1 + dataloader: 2.2.3 + dset: 3.1.4 + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/documents@1.0.1(graphql@16.10.0)': + dependencies: + graphql: 16.10.0 + lodash.sortby: 4.7.0 + tslib: 2.8.1 + + '@graphql-tools/executor-common@0.0.4(graphql@16.10.0)': + dependencies: + '@envelop/core': 5.2.3 + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + + '@graphql-tools/executor-graphql-ws@2.0.5(graphql@16.10.0)': + dependencies: + '@graphql-tools/executor-common': 0.0.4(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@whatwg-node/disposablestack': 0.0.6 + graphql: 16.10.0 + graphql-ws: 6.0.4(graphql@16.10.0)(ws@8.18.1) + isomorphic-ws: 5.0.0(ws@8.18.1) + tslib: 2.8.1 + ws: 8.18.1 + transitivePeerDependencies: + - '@fastify/websocket' + - bufferutil + - uWebSockets.js + - utf-8-validate + + '@graphql-tools/executor-http@1.3.3(@types/node@20.12.11)(graphql@16.10.0)': + dependencies: + '@graphql-hive/signal': 1.0.0 + '@graphql-tools/executor-common': 0.0.4(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/fetch': 0.10.6 + '@whatwg-node/promise-helpers': 1.3.1 + graphql: 16.10.0 + meros: 1.3.0(@types/node@20.12.11) + tslib: 2.8.1 + transitivePeerDependencies: + - '@types/node' + + '@graphql-tools/executor-legacy-ws@1.1.17(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@types/ws': 8.5.10 + graphql: 16.10.0 + isomorphic-ws: 5.0.0(ws@8.18.1) + tslib: 2.8.1 + ws: 8.18.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@graphql-tools/executor@1.4.7(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/promise-helpers': 1.3.1 + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/git-loader@8.0.24(graphql@16.10.0)(supports-color@8.1.1)': + dependencies: + '@graphql-tools/graphql-tag-pluck': 8.3.19(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + tslib: 2.8.1 + unixify: 1.0.0 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/github-loader@8.0.20(@types/node@20.12.11)(graphql@16.10.0)(supports-color@8.1.1)': + dependencies: + '@graphql-tools/executor-http': 1.3.3(@types/node@20.12.11)(graphql@16.10.0) + '@graphql-tools/graphql-tag-pluck': 8.3.19(graphql@16.10.0)(supports-color@8.1.1) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@whatwg-node/fetch': 0.10.6 + '@whatwg-node/promise-helpers': 1.3.1 + graphql: 16.10.0 + sync-fetch: 0.6.0-2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@types/node' + - supports-color + + '@graphql-tools/graphql-file-loader@8.0.19(graphql@16.10.0)': + dependencies: + '@graphql-tools/import': 7.0.18(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + globby: 11.1.0 + graphql: 16.10.0 + tslib: 2.8.1 + unixify: 1.0.0 + + '@graphql-tools/graphql-tag-pluck@8.3.19(graphql@16.10.0)(supports-color@8.1.1)': + dependencies: + '@babel/core': 7.26.10(supports-color@8.1.1) + '@babel/parser': 7.27.0 + '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.10(supports-color@8.1.1)) + '@babel/traverse': 7.27.0(supports-color@8.1.1) + '@babel/types': 7.27.0 + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@graphql-tools/import@7.0.18(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + resolve-from: 5.0.0 + tslib: 2.8.1 + + '@graphql-tools/json-file-loader@8.0.18(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + globby: 11.1.0 + graphql: 16.10.0 + tslib: 2.8.1 + unixify: 1.0.0 + + '@graphql-tools/load@8.1.0(graphql@16.10.0)': + dependencies: + '@graphql-tools/schema': 10.0.23(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + p-limit: 3.1.0 + tslib: 2.8.1 + + '@graphql-tools/merge@9.0.24(graphql@16.10.0)': + dependencies: + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/optimize@2.0.0(graphql@16.10.0)': + dependencies: + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/prisma-loader@8.0.17(@types/node@20.12.11)(encoding@0.1.13)(graphql@16.10.0)(supports-color@8.1.1)': + dependencies: + '@graphql-tools/url-loader': 8.0.31(@types/node@20.12.11)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@types/js-yaml': 4.0.6 + '@whatwg-node/fetch': 0.10.6 + chalk: 4.1.2 + debug: 4.4.0(supports-color@8.1.1) + dotenv: 16.4.7 + graphql: 16.10.0 + graphql-request: 6.1.0(encoding@0.1.13)(graphql@16.10.0) + http-proxy-agent: 7.0.2(supports-color@8.1.1) + https-proxy-agent: 7.0.6(supports-color@8.1.1) + jose: 5.2.1 + js-yaml: 4.1.0 + lodash: 4.17.21 + scuid: 1.1.0 + tslib: 2.8.1 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - encoding + - supports-color + - uWebSockets.js + - utf-8-validate + + '@graphql-tools/relay-operation-optimizer@7.0.19(encoding@0.1.13)(graphql@16.10.0)': + dependencies: + '@ardatan/relay-compiler': 12.0.3(encoding@0.1.13)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.8.1 + transitivePeerDependencies: + - encoding + + '@graphql-tools/schema@10.0.23(graphql@16.10.0)': + dependencies: + '@graphql-tools/merge': 9.0.24(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/url-loader@8.0.31(@types/node@20.12.11)(graphql@16.10.0)': + dependencies: + '@graphql-tools/executor-graphql-ws': 2.0.5(graphql@16.10.0) + '@graphql-tools/executor-http': 1.3.3(@types/node@20.12.11)(graphql@16.10.0) + '@graphql-tools/executor-legacy-ws': 1.1.17(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@graphql-tools/wrap': 10.0.35(graphql@16.10.0) + '@types/ws': 8.5.10 + '@whatwg-node/fetch': 0.10.6 + '@whatwg-node/promise-helpers': 1.3.1 + graphql: 16.10.0 + isomorphic-ws: 5.0.0(ws@8.18.1) + sync-fetch: 0.6.0-2 + tslib: 2.8.1 + ws: 8.18.1 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - uWebSockets.js + - utf-8-validate + + '@graphql-tools/utils@10.8.6(graphql@16.10.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) + '@whatwg-node/promise-helpers': 1.3.1 + cross-inspect: 1.0.1 + dset: 3.1.4 + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-tools/wrap@10.0.35(graphql@16.10.0)': + dependencies: + '@graphql-tools/delegate': 10.2.17(graphql@16.10.0) + '@graphql-tools/schema': 10.0.23(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + '@whatwg-node/promise-helpers': 1.3.1 + graphql: 16.10.0 + tslib: 2.8.1 + + '@graphql-typed-document-node/core@3.2.0(graphql@16.10.0)': + dependencies: + graphql: 16.10.0 + '@gregnr/postgres-meta@0.82.0-dev.2(encoding@0.1.13)(supports-color@8.1.1)': dependencies: '@fastify/cors': 9.0.1 @@ -16914,7 +18043,7 @@ snapshots: dependencies: agent-base: 7.1.1(supports-color@8.1.1) http-proxy-agent: 7.0.2(supports-color@8.1.1) - https-proxy-agent: 7.0.4(supports-color@8.1.1) + https-proxy-agent: 7.0.6(supports-color@8.1.1) lru-cache: 10.4.3 socks-proxy-agent: 8.0.3(supports-color@8.1.1) transitivePeerDependencies: @@ -18911,6 +20040,8 @@ snapshots: - ajv - supports-color + '@repeaterjs/repeater@3.0.6': {} + '@resvg/resvg-wasm@2.4.0': {} '@rollup/plugin-alias@5.1.1(rollup@4.38.0)': @@ -21200,6 +22331,27 @@ snapshots: '@webgpu/types@0.1.49': {} + '@whatwg-node/disposablestack@0.0.6': + dependencies: + '@whatwg-node/promise-helpers': 1.3.1 + tslib: 2.8.1 + + '@whatwg-node/fetch@0.10.6': + dependencies: + '@whatwg-node/node-fetch': 0.7.18 + urlpattern-polyfill: 10.0.0 + + '@whatwg-node/node-fetch@0.7.18': + dependencies: + '@fastify/busboy': 3.1.1 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/promise-helpers': 1.3.1 + tslib: 2.8.1 + + '@whatwg-node/promise-helpers@1.3.1': + dependencies: + tslib: 2.8.1 + '@xobotyi/scrollbar-width@1.9.5': {} '@xtuc/ieee754@1.2.0': {} @@ -21527,6 +22679,8 @@ snapshots: is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 + asap@2.0.6: {} + assert-plus@1.0.0: {} assertion-error@2.0.1: {} @@ -21537,6 +22691,8 @@ snapshots: dependencies: tslib: 2.8.1 + astral-regex@2.0.0: {} + astring@1.8.6: {} async-sema@3.1.1: {} @@ -21549,6 +22705,8 @@ snapshots: attr-accept@2.2.5: {} + auto-bind@4.0.0: {} + autolinker@0.28.1: dependencies: gulp-header: 1.8.12 @@ -21655,6 +22813,12 @@ snapshots: bintrees@1.0.2: {} + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + bl@5.1.0: dependencies: buffer: 6.0.3 @@ -21703,12 +22867,21 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + buffer-crc32@1.0.0: {} buffer-from@1.1.2: {} buffer-writer@2.0.0: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -21847,6 +23020,19 @@ snapshots: chalk@5.4.1: {} + change-case-all@1.0.15: + dependencies: + change-case: 4.1.2 + is-lower-case: 2.0.2 + is-upper-case: 2.0.2 + lower-case: 2.0.2 + lower-case-first: 2.0.2 + sponge-case: 1.0.1 + swap-case: 2.0.2 + title-case: 3.0.3 + upper-case: 2.0.2 + upper-case-first: 2.0.2 + change-case@4.1.2: dependencies: camel-case: 4.1.2 @@ -21878,6 +23064,8 @@ snapshots: character-reference-invalid@2.0.1: {} + chardet@0.7.0: {} + check-error@2.1.1: {} cheerio-select@2.1.0: @@ -21957,12 +23145,23 @@ snapshots: cli-boxes@3.0.0: {} + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + cli-cursor@4.0.0: dependencies: restore-cursor: 4.0.0 cli-spinners@2.9.2: {} + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + + cli-width@3.0.0: {} + cli-width@4.1.0: {} client-only@0.0.1: {} @@ -22238,6 +23437,15 @@ snapshots: optionalDependencies: typescript: 5.5.2 + cosmiconfig@9.0.0(typescript@5.5.2): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.5.2 + crc-32@1.2.2: {} crc32-stream@6.0.0: @@ -22258,6 +23466,16 @@ snapshots: cronstrue@2.50.0: {} + cross-fetch@3.2.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + cross-inspect@1.0.1: + dependencies: + tslib: 2.8.1 + cross-spawn@6.0.6: dependencies: nice-try: 1.0.5 @@ -22443,6 +23661,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + dataloader@2.2.3: {} + date-fns@2.30.0: dependencies: '@babel/runtime': 7.26.10 @@ -22555,6 +23775,8 @@ snapshots: depd@2.0.0: {} + dependency-graph@0.11.0: {} + dequal@2.0.3: {} derive-valtio@0.1.0(valtio@1.12.0(@types/react@18.3.3)(react@18.3.1)): @@ -22565,6 +23787,8 @@ snapshots: destroy@1.2.0: {} + detect-indent@6.1.0: {} + detect-libc@1.0.3: {} detect-libc@2.0.3: {} @@ -22669,6 +23893,8 @@ snapshots: drange@1.1.1: {} + dset@3.1.4: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -23352,6 +24578,12 @@ snapshots: extend@3.0.2: {} + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + extsprintf@1.4.1: {} fast-content-type-parse@1.1.0: {} @@ -23463,6 +24695,24 @@ snapshots: dependencies: format: 0.2.2 + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fbjs-css-vars@1.0.2: {} + + fbjs@3.0.5(encoding@0.1.13): + dependencies: + cross-fetch: 3.2.0(encoding@0.1.13) + fbjs-css-vars: 1.0.2 + loose-envify: 1.4.0 + object-assign: 4.1.1 + promise: 7.3.1 + setimmediate: 1.0.5 + ua-parser-js: 1.0.40 + transitivePeerDependencies: + - encoding + fdir@6.4.3(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -23476,6 +24726,10 @@ snapshots: fflate@0.8.2: {} + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + file-entry-cache@6.0.1: dependencies: flat-cache: 3.1.0 @@ -23830,7 +25084,7 @@ snapshots: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.3.2 + fast-glob: 3.3.3 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 @@ -23864,9 +25118,9 @@ snapshots: graphemer@1.4.0: {} - graphiql@3.8.3(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + graphiql@3.8.3(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@graphiql/react': 0.28.2(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@5.14.1(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@graphiql/react': 0.28.2(@codemirror/language@6.11.0)(@types/node@20.12.11)(@types/react-dom@18.3.0)(@types/react@18.3.3)(graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) graphql: 16.10.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -23877,6 +25131,28 @@ snapshots: - '@types/react-dom' - graphql-ws + graphql-config@5.1.4(@types/node@20.12.11)(graphql@16.10.0)(typescript@5.5.2): + dependencies: + '@graphql-tools/graphql-file-loader': 8.0.19(graphql@16.10.0) + '@graphql-tools/json-file-loader': 8.0.18(graphql@16.10.0) + '@graphql-tools/load': 8.1.0(graphql@16.10.0) + '@graphql-tools/merge': 9.0.24(graphql@16.10.0) + '@graphql-tools/url-loader': 8.0.31(@types/node@20.12.11)(graphql@16.10.0) + '@graphql-tools/utils': 10.8.6(graphql@16.10.0) + cosmiconfig: 9.0.0(typescript@5.5.2) + graphql: 16.10.0 + jiti: 2.4.2 + minimatch: 10.0.1 + string-env-interpolation: 1.0.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - typescript + - uWebSockets.js + - utf-8-validate + graphql-language-service@5.2.0(graphql@16.10.0): dependencies: graphql: 16.10.0 @@ -23890,6 +25166,23 @@ snapshots: nullthrows: 1.1.1 vscode-languageserver-types: 3.17.5 + graphql-request@6.1.0(encoding@0.1.13)(graphql@16.10.0): + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) + cross-fetch: 3.2.0(encoding@0.1.13) + graphql: 16.10.0 + transitivePeerDependencies: + - encoding + + graphql-sock@1.0.1(graphql@16.10.0): + dependencies: + graphql: 16.10.0 + + graphql-tag@2.12.6(graphql@16.10.0): + dependencies: + graphql: 16.10.0 + tslib: 2.8.1 + graphql-validation-complexity@0.4.2(graphql@16.10.0): dependencies: graphql: 16.10.0 @@ -23899,6 +25192,12 @@ snapshots: dependencies: graphql: 16.10.0 + graphql-ws@6.0.4(graphql@16.10.0)(ws@8.18.1): + dependencies: + graphql: 16.10.0 + optionalDependencies: + ws: 8.18.1 + graphql@16.10.0: {} graphql@16.8.1: {} @@ -24310,7 +25609,7 @@ snapshots: http-proxy-agent@7.0.2(supports-color@8.1.1): dependencies: - agent-base: 7.1.1(supports-color@8.1.1) + agent-base: 7.1.3 debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -24383,6 +25682,10 @@ snapshots: hyphenate-style-name@1.0.4: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -24400,6 +25703,8 @@ snapshots: immutability-helper@3.1.1: {} + immutable@3.7.6: {} + immutable@4.3.4: {} import-fresh@3.3.0: @@ -24407,6 +25712,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-from@4.0.0: {} + import-in-the-middle@1.13.1: dependencies: acorn: 8.14.1 @@ -24449,6 +25756,24 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + inquirer@8.2.6: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + internal-slot@1.0.7: dependencies: es-errors: 1.3.0 @@ -24459,6 +25784,10 @@ snapshots: intersection-observer@0.10.0: {} + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ioredis@5.6.0(supports-color@8.1.1): dependencies: '@ioredis/commands': 1.2.0 @@ -24484,6 +25813,11 @@ snapshots: iron-webcrypto@1.2.1: {} + is-absolute@1.0.0: + dependencies: + is-relative: 1.0.0 + is-windows: 1.0.2 + is-alphabetical@1.0.4: {} is-alphabetical@2.0.1: {} @@ -24584,10 +25918,16 @@ snapshots: dependencies: is-docker: 3.0.0 + is-interactive@1.0.0: {} + is-interactive@2.0.0: {} is-lambda@1.0.1: {} + is-lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + is-map@2.0.2: {} is-module@1.0.0: {} @@ -24640,6 +25980,10 @@ snapshots: is-regexp@3.1.0: {} + is-relative@1.0.0: + dependencies: + is-unc-path: 1.0.0 + is-set@2.0.2: {} is-shared-array-buffer@1.0.3: @@ -24662,8 +26006,18 @@ snapshots: dependencies: which-typed-array: 1.1.15 + is-unc-path@1.0.0: + dependencies: + unc-path-regex: 0.1.2 + + is-unicode-supported@0.1.0: {} + is-unicode-supported@1.3.0: {} + is-upper-case@2.0.2: + dependencies: + tslib: 2.8.1 + is-weakmap@2.0.1: {} is-weakref@1.0.2: @@ -24677,6 +26031,8 @@ snapshots: is-what@4.1.15: {} + is-windows@1.0.2: {} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 @@ -24707,6 +26063,10 @@ snapshots: isobject@3.0.1: {} + isomorphic-ws@5.0.0(ws@8.18.1): + dependencies: + ws: 8.18.1 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -24903,6 +26263,11 @@ snapshots: json-stringify-safe@5.0.1: {} + json-to-pretty-yaml@1.2.2: + dependencies: + remedial: 1.0.8 + remove-trailing-spaces: 1.0.9 + json5@1.0.2: dependencies: minimist: 1.2.8 @@ -25059,6 +26424,17 @@ snapshots: untun: 0.1.3 uqr: 0.1.2 + listr2@4.0.5: + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.4.1 + rxjs: 7.8.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + load-json-file@4.0.0: dependencies: graceful-fs: 4.2.11 @@ -25127,6 +26503,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash.sortby@4.7.0: {} + lodash.template@4.5.0: dependencies: lodash._reinterpolate: 3.0.0 @@ -25145,11 +26523,23 @@ snapshots: lodash@4.17.21: {} + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + log-symbols@5.1.0: dependencies: chalk: 5.4.1 is-unicode-supported: 1.3.0 + log-update@4.0.0: + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + long@5.2.3: {} longest-streak@3.1.0: {} @@ -25162,6 +26552,10 @@ snapshots: loupe@3.1.3: {} + lower-case-first@2.0.2: + dependencies: + tslib: 2.8.1 + lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -25259,6 +26653,8 @@ snapshots: - bluebird - supports-color + map-cache@0.2.2: {} + mark.js@8.11.1: {} markdown-extensions@1.1.1: {} @@ -26498,6 +27894,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + mute-stream@0.0.8: {} + mute-stream@1.0.0: {} mute-stream@2.0.0: {} @@ -26833,6 +28231,8 @@ snapshots: - bluebird - supports-color + node-int64@0.4.0: {} + node-mock-http@1.0.0: {} node-pty@1.0.0: @@ -26878,6 +28278,10 @@ snapshots: semver: 7.7.1 validate-npm-package-license: 3.0.4 + normalize-path@2.1.1: + dependencies: + remove-trailing-separator: 1.1.0 + normalize-path@3.0.0: {} normalize-range@0.1.2: {} @@ -26940,7 +28344,7 @@ snapshots: number-flow@0.3.7: {} - nuqs@1.19.1(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)): + nuqs@1.19.1(next@14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)): dependencies: mitt: 3.0.1 next: 14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0) @@ -27164,6 +28568,18 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + ora@6.3.1: dependencies: chalk: 5.4.1 @@ -27176,6 +28592,8 @@ snapshots: strip-ansi: 7.1.0 wcwidth: 1.0.1 + os-tmpdir@1.0.2: {} + outdent@0.8.0: {} outvariant@1.4.0: {} @@ -27236,6 +28654,12 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse-filepath@1.0.2: + dependencies: + is-absolute: 1.0.0 + map-cache: 0.2.2 + path-root: 0.1.1 + parse-json@4.0.0: dependencies: error-ex: 1.3.2 @@ -27296,6 +28720,12 @@ snapshots: path-parse@1.0.7: {} + path-root-regex@0.1.2: {} + + path-root@0.1.1: + dependencies: + path-root-regex: 0.1.2 + path-scurry@1.11.1: dependencies: lru-cache: 10.4.3 @@ -27687,6 +29117,10 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 + promise@7.3.1: + dependencies: + asap: 2.0.6 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -28411,6 +29845,14 @@ snapshots: hast-util-to-html: 9.0.0 unified: 11.0.5 + relay-runtime@12.0.0(encoding@0.1.13): + dependencies: + '@babel/runtime': 7.26.10 + fbjs: 3.0.5(encoding@0.1.13) + invariant: 2.2.4 + transitivePeerDependencies: + - encoding + remark-code-import@1.2.0: dependencies: strip-indent: 4.0.0 @@ -28559,8 +30001,14 @@ snapshots: argparse: 1.0.10 autolinker: 0.28.1 + remedial@1.0.8: {} + remove-accents@0.4.2: {} + remove-trailing-separator@1.1.0: {} + + remove-trailing-spaces@1.0.9: {} + rename-keys@1.2.0: {} repeat-element@1.1.4: {} @@ -28609,6 +30057,11 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + restore-cursor@4.0.0: dependencies: onetime: 5.1.2 @@ -28688,6 +30141,8 @@ snapshots: run-applescript@7.0.0: {} + run-async@2.4.1: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -28723,8 +30178,7 @@ snapshots: safe-stable-stringify@2.5.0: {} - safer-buffer@2.1.2: - optional: true + safer-buffer@2.1.2: {} sass@1.72.0: dependencies: @@ -28773,6 +30227,8 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 + scuid@1.1.0: {} + scule@1.3.0: {} section-matter@1.0.0: @@ -28864,6 +30320,8 @@ snapshots: is-plain-object: 2.0.4 is-primitive: 3.0.1 + setimmediate@1.0.5: {} + setprototypeof@1.2.0: {} shadcn@2.4.0-canary.16(@types/node@22.13.14)(supports-color@8.1.1)(typescript@5.5.2): @@ -28996,6 +30454,8 @@ snapshots: signal-exit@4.1.0: {} + signedsource@1.0.0: {} + simple-git@3.24.0(supports-color@8.1.1): dependencies: '@kwsites/file-exists': 1.1.1(supports-color@8.1.1) @@ -29040,6 +30500,18 @@ snapshots: slash@5.1.0: {} + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + slugify@1.4.7: {} slugify@1.6.6: {} @@ -29065,7 +30537,7 @@ snapshots: socks-proxy-agent@8.0.3(supports-color@8.1.1): dependencies: - agent-base: 7.1.1(supports-color@8.1.1) + agent-base: 7.1.3 debug: 4.4.0(supports-color@8.1.1) socks: 2.8.3 transitivePeerDependencies: @@ -29128,6 +30600,10 @@ snapshots: split2@4.2.0: {} + sponge-case@1.0.1: + dependencies: + tslib: 2.8.1 + sprintf-js@1.0.3: {} sprintf-js@1.1.3: {} @@ -29227,6 +30703,8 @@ snapshots: strict-event-emitter@0.5.1: {} + string-env-interpolation@1.0.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -29478,6 +30956,10 @@ snapshots: transitivePeerDependencies: - encoding + swap-case@2.0.2: + dependencies: + tslib: 2.8.1 + swiper@11.0.7: {} swr@2.2.5(react@18.3.1): @@ -29495,6 +30977,12 @@ snapshots: symbol-tree@3.2.4: optional: true + sync-fetch@0.6.0-2: + dependencies: + node-fetch: 3.3.2 + timeout-signal: 2.0.0 + whatwg-mimetype: 4.0.0 + system-architecture@0.1.0: {} tailwind-merge@1.14.0: {} @@ -29660,6 +31148,10 @@ snapshots: readable-stream: 2.3.8 xtend: 4.0.2 + through@2.3.8: {} + + timeout-signal@2.0.0: {} + tiny-case@1.0.3: {} tiny-inflate@1.0.3: {} @@ -29690,6 +31182,14 @@ snapshots: tinyspy@3.0.2: {} + title-case@3.0.3: + dependencies: + tslib: 2.8.1 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + to-fast-properties@2.0.0: {} to-gatsby-remark-plugin@0.1.0: @@ -29759,6 +31259,8 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-log@2.2.7: {} + ts-morph@18.0.0: dependencies: '@ts-morph/common': 0.19.0 @@ -29948,6 +31450,8 @@ snapshots: typescript@5.5.2: {} + ua-parser-js@1.0.40: {} + uc.micro@1.0.6: {} uc.micro@2.1.0: {} @@ -29968,6 +31472,8 @@ snapshots: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 + unc-path-regex@0.1.2: {} + uncrypto@0.1.3: {} unctx@2.4.1: @@ -30156,6 +31662,10 @@ snapshots: universalify@2.0.0: {} + unixify@1.0.0: + dependencies: + normalize-path: 2.1.1 + unplugin-utils@0.2.4: dependencies: pathe: 2.0.3 @@ -30250,6 +31760,8 @@ snapshots: url-template@2.0.8: {} + urlpattern-polyfill@10.0.0: {} + use-callback-ref@1.3.3(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 @@ -30834,6 +32346,8 @@ snapshots: whatwg-mimetype@3.0.0: optional: true + whatwg-mimetype@4.0.0: {} + whatwg-url@11.0.0: dependencies: tr46: 3.0.0 @@ -30991,7 +32505,7 @@ snapshots: yargs@17.7.2: dependencies: cliui: 8.0.1 - escalade: 3.1.2 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 From 9016b84657c1c82066e33bc69c0edac16597472d Mon Sep 17 00:00:00 2001 From: Max Holmberg Date: Fri, 2 May 2025 17:44:56 +0200 Subject: [PATCH 80/86] Remove + character from code blocks (#35414) --- .../getting-started/tutorials/with-swift.mdx | 132 +++++++++--------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx b/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx index cb41c4408c4..1705ea759e4 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-swift.mdx @@ -367,9 +367,9 @@ enum TransferError: Error { <$CodeTabs> ```swift name=ProfileView.swift -+ import PhotosUI -+ import Storage -+ import Supabase +import PhotosUI +import Storage +import Supabase import SwiftUI struct ProfileView: View { @@ -379,34 +379,34 @@ struct ProfileView: View { @State var isLoading = false -+ @State var imageSelection: PhotosPickerItem? -+ @State var avatarImage: AvatarImage? + @State var imageSelection: PhotosPickerItem? + @State var avatarImage: AvatarImage? var body: some View { NavigationStack { Form { -+ Section { -+ HStack { -+ Group { -+ if let avatarImage { -+ avatarImage.image.resizable() -+ } else { -+ Color.clear -+ } -+ } -+ .scaledToFit() -+ .frame(width: 80, height: 80) -+ -+ Spacer() -+ -+ PhotosPicker(selection: $imageSelection, matching: .images) { -+ Image(systemName: "pencil.circle.fill") -+ .symbolRenderingMode(.multicolor) -+ .font(.system(size: 30)) -+ .foregroundColor(.accentColor) -+ } -+ } -+ } + Section { + HStack { + Group { + if let avatarImage { + avatarImage.image.resizable() + } else { + Color.clear + } + } + .scaledToFit() + .frame(width: 80, height: 80) + + Spacer() + + PhotosPicker(selection: $imageSelection, matching: .images) { + Image(systemName: "pencil.circle.fill") + .symbolRenderingMode(.multicolor) + .font(.system(size: 30)) + .foregroundColor(.accentColor) + } + } + } Section { TextField("Username", text: $username) @@ -440,10 +440,10 @@ struct ProfileView: View { } } }) -+ .onChange(of: imageSelection) { _, newValue in -+ guard let newValue else { return } -+ loadTransferable(from: newValue) -+ } + .onChange(of: imageSelection) { _, newValue in + guard let newValue else { return } + loadTransferable(from: newValue) + } } .task { await getInitialProfile() @@ -467,9 +467,9 @@ struct ProfileView: View { fullName = profile.fullName ?? "" website = profile.website ?? "" -+ if let avatarURL = profile.avatarURL, !avatarURL.isEmpty { -+ try await downloadImage(path: avatarURL) -+ } + if let avatarURL = profile.avatarURL, !avatarURL.isEmpty { + try await downloadImage(path: avatarURL) + } } catch { debugPrint(error) @@ -481,7 +481,7 @@ struct ProfileView: View { isLoading = true defer { isLoading = false } do { -+ let imageURL = try await uploadImage() + let imageURL = try await uploadImage() let currentUser = try await supabase.auth.session.user @@ -489,7 +489,7 @@ struct ProfileView: View { username: username, fullName: fullName, website: website, -+ avatarURL: imageURL + avatarURL: imageURL ) try await supabase @@ -503,36 +503,36 @@ struct ProfileView: View { } } -+ private func loadTransferable(from imageSelection: PhotosPickerItem) { -+ Task { -+ do { -+ avatarImage = try await imageSelection.loadTransferable(type: AvatarImage.self) -+ } catch { -+ debugPrint(error) -+ } -+ } -+ } -+ -+ private func downloadImage(path: String) async throws { -+ let data = try await supabase.storage.from("avatars").download(path: path) -+ avatarImage = AvatarImage(data: data) -+ } -+ -+ private func uploadImage() async throws -> String? { -+ guard let data = avatarImage?.data else { return nil } -+ -+ let filePath = "\(UUID().uuidString).jpeg" -+ -+ try await supabase.storage -+ .from("avatars") -+ .upload( -+ filePath, -+ data: data, -+ options: FileOptions(contentType: "image/jpeg") -+ ) -+ -+ return filePath -+ } + private func loadTransferable(from imageSelection: PhotosPickerItem) { + Task { + do { + avatarImage = try await imageSelection.loadTransferable(type: AvatarImage.self) + } catch { + debugPrint(error) + } + } + } + + private func downloadImage(path: String) async throws { + let data = try await supabase.storage.from("avatars").download(path: path) + avatarImage = AvatarImage(data: data) + } + + private func uploadImage() async throws -> String? { + guard let data = avatarImage?.data else { return nil } + + let filePath = "\(UUID().uuidString).jpeg" + + try await supabase.storage + .from("avatars") + .upload( + filePath, + data: data, + options: FileOptions(contentType: "image/jpeg") + ) + + return filePath + } } ``` From f611fa11d949c19fb5d404b4bebe4d96b80cba74 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Fri, 2 May 2025 12:03:41 -0400 Subject: [PATCH 81/86] feat (content api): add schemas and models for search results and guides (#35284) Add models and GraphQL interface definitions for search results and guides (representing Markdown documents such as tutorials, etc.). These aren't connected to anything yet, but putting them in a separate PR to keep the review short and relatively simple. Towards DOCS-214 --- apps/docs/features/helpers.misc.ts | 17 + .../globalSearch/globalSearchInterface.ts | 5 + .../globalSearch/globalSearchSchema.ts | 22 ++ apps/docs/resources/guide/guideModel.ts | 37 ++ apps/docs/resources/guide/guideSchema.ts | 56 +++ apps/docs/resources/utils/connections.ts | 357 ++++++++++++++++++ 6 files changed, 494 insertions(+) create mode 100644 apps/docs/features/helpers.misc.ts create mode 100644 apps/docs/resources/globalSearch/globalSearchInterface.ts create mode 100644 apps/docs/resources/globalSearch/globalSearchSchema.ts create mode 100644 apps/docs/resources/guide/guideModel.ts create mode 100644 apps/docs/resources/guide/guideSchema.ts create mode 100644 apps/docs/resources/utils/connections.ts diff --git a/apps/docs/features/helpers.misc.ts b/apps/docs/features/helpers.misc.ts new file mode 100644 index 00000000000..127958425cd --- /dev/null +++ b/apps/docs/features/helpers.misc.ts @@ -0,0 +1,17 @@ +/** + * Creates a short random identifier + */ +export function nanoId( + /** + * The length of the identifier to generate + */ + length?: number +) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + const l = length || 12 + let id = '' + for (let i = 0; i < l; i++) { + id += chars.charAt(Math.floor(Math.random() * chars.length)) + } + return id +} diff --git a/apps/docs/resources/globalSearch/globalSearchInterface.ts b/apps/docs/resources/globalSearch/globalSearchInterface.ts new file mode 100644 index 00000000000..ecf1bab91a6 --- /dev/null +++ b/apps/docs/resources/globalSearch/globalSearchInterface.ts @@ -0,0 +1,5 @@ +export interface SearchResultInterface { + title?: string + href?: string + content?: string +} diff --git a/apps/docs/resources/globalSearch/globalSearchSchema.ts b/apps/docs/resources/globalSearch/globalSearchSchema.ts new file mode 100644 index 00000000000..e62dcd06b8d --- /dev/null +++ b/apps/docs/resources/globalSearch/globalSearchSchema.ts @@ -0,0 +1,22 @@ +import { GraphQLInterfaceType, GraphQLString } from 'graphql' + +export const GRAPHQL_FIELD_SEARCH_GLOBAL = 'searchDocs' + +export const GraphQLInterfaceTypeSearchResult = new GraphQLInterfaceType({ + name: 'SearchResult', + description: 'Document that matches a search query', + fields: { + title: { + type: GraphQLString, + description: 'The title of the matching result', + }, + href: { + type: GraphQLString, + description: 'The URL of the matching result', + }, + content: { + type: GraphQLString, + description: 'The full content of the matching result', + }, + }, +}) diff --git a/apps/docs/resources/guide/guideModel.ts b/apps/docs/resources/guide/guideModel.ts new file mode 100644 index 00000000000..a1ba712600f --- /dev/null +++ b/apps/docs/resources/guide/guideModel.ts @@ -0,0 +1,37 @@ +import { SearchResultInterface } from '../globalSearch/globalSearchInterface' + +export class GuideModel implements SearchResultInterface { + public title?: string + public href?: string + public content?: string + public subsections: Array + + constructor({ + title, + href, + content, + subsections, + }: { + title?: string + href?: string + content?: string + subsections?: Array<{ title?: string; href?: string; content?: string }> + }) { + this.title = title + this.href = href + this.content = content + this.subsections = subsections?.map((subsection) => new SubsectionModel(subsection)) ?? [] + } +} + +export class SubsectionModel { + public title?: string + public href?: string + public content?: string + + constructor({ title, href, content }: { title?: string; href?: string; content?: string }) { + this.title = title + this.href = href + this.content = content + } +} diff --git a/apps/docs/resources/guide/guideSchema.ts b/apps/docs/resources/guide/guideSchema.ts new file mode 100644 index 00000000000..546ac1a3fd3 --- /dev/null +++ b/apps/docs/resources/guide/guideSchema.ts @@ -0,0 +1,56 @@ +import { GraphQLObjectType, GraphQLString } from 'graphql' +import { GraphQLInterfaceTypeSearchResult } from '../globalSearch/globalSearchSchema' +import { createCollectionType, GraphQLCollectionBuilder } from '../utils/connections' +import { GuideModel, SubsectionModel } from './guideModel' + +export const GraphQLObjectTypeSubsection = new GraphQLObjectType({ + name: 'Subsection', + isTypeOf: (value: unknown) => value instanceof SubsectionModel, + description: 'A content chunk taken from a larger document in the Supabase docs', + fields: { + title: { + type: GraphQLString, + description: 'The title of the subsection', + }, + href: { + type: GraphQLString, + description: 'The URL of the subsection', + }, + content: { + type: GraphQLString, + description: 'The content of the subsection', + }, + }, +}) + +export const GraphQLObjectTypeGuide = new GraphQLObjectType({ + name: 'Guide', + interfaces: [GraphQLInterfaceTypeSearchResult], + isTypeOf: (value: unknown) => value instanceof GuideModel, + description: + 'A document containing content from the Supabase docs. This is a guide, which might describe a concept, or explain the steps for using or implementing a feature.', + fields: { + title: { + type: GraphQLString, + description: 'The title of the document', + }, + href: { + type: GraphQLString, + description: 'The URL of the document', + }, + content: { + type: GraphQLString, + description: + 'The full content of the document, including all subsections (both those matching and not matching any query string) and possibly more content', + }, + subsections: { + type: createCollectionType(GraphQLObjectTypeSubsection, { + skipPageInfo: true, + description: 'A collection of content chunks from a larger document in the Supabase docs.', + }), + description: + 'The subsections of the document. If the document is returned from a search match, only matching content chunks are returned. For the full content of the original document, use the content field in the parent Guide.', + resolve: (node: GuideModel) => GraphQLCollectionBuilder.create({ items: node.subsections }), + }, + }, +}) diff --git a/apps/docs/resources/utils/connections.ts b/apps/docs/resources/utils/connections.ts new file mode 100644 index 00000000000..19ca714cfc3 --- /dev/null +++ b/apps/docs/resources/utils/connections.ts @@ -0,0 +1,357 @@ +import { + GraphQLBoolean, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + type GraphQLOutputType, + GraphQLString, +} from 'graphql' +import { nanoId } from '~/features/helpers.misc' + +/** + * Extracts the name from a a GraphQLOutputType. + */ +function extractNodeTypeName( + /** + * The GraphQL Output Type to extract the name from. + */ + nodeType: GraphQLOutputType +): string | undefined { + if ('name' in nodeType) { + return nodeType.name + } else { + return `AnonymousNode(${nanoId()})` + } +} + +/** + * Creates an Edge type for a specific node type. An Edge type wraps the node + * alongside an optional associated cursor, for example: + * + * ``` + * { + * edges: [ + * { + * node: { + * id: '123', + * name: 'John Doe' + * }, + * cursor: 'YXJyYXljb25uZWN0aW9uOjE=' + * } + * ] + * } + * ``` + */ +function createEdgeType( + nodeType: GraphQLOutputType, + { + name, + skipCursor = false, + }: { + /** + * The name of the created Edge node. If not provided, defaults to + * NameOfInnerNodeEdge. + */ + name?: string + /** + * Whether to skip the cursor field. + */ + skipCursor?: boolean + } = {} +): GraphQLObjectType { + const edgeName = name || `${extractNodeTypeName(nodeType)}Edge` + + return new GraphQLObjectType({ + name: edgeName, + description: `An edge in a collection of ${extractNodeTypeName(nodeType)}s`, + fields: { + node: { + type: new GraphQLNonNull(nodeType), + description: `The ${extractNodeTypeName(nodeType)} at the end of the edge`, + }, + ...(!skipCursor && { + cursor: { + type: new GraphQLNonNull(GraphQLString), + description: 'A cursor for use in pagination', + }, + }), + }, + }) +} + +/** + * Standard GraphQL type for pagination information on connections + */ +const PageInfoType = new GraphQLObjectType({ + name: 'PageInfo', + description: 'Pagination information for a collection', + fields: { + hasNextPage: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'Whether there are more items after the current page', + }, + hasPreviousPage: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'Whether there are more items before the current page', + }, + startCursor: { + type: GraphQLString, + description: 'Cursor pointing to the start of the current page', + }, + endCursor: { + type: GraphQLString, + description: 'Cursor pointing to the end of the current page', + }, + }, +}) + +/** + * Creates a Collection type for a specific node type. A collection type + * represents a list of nodes with pagination information, and has the shape: + * + * ``` + * { + * edges: [ + * { + * node: { ... }, + * cursor: '...' + * }, + * ... + * ], + * nodes: [ ... ], + * pageInfo: { + * hasNextPage: true, + * hasPreviousPage: false, + * startCursor: '...', + * endCursor: '...' + * } + * } + * ``` + */ +export function createCollectionType( + nodeType: GraphQLOutputType, + { + name, + description, + skipPageInfo = false, + }: { + /** + * The name of the generated collection. + * + * If omitted, defaults to NameOfInnerNodeCollection. + */ + name?: string + /** + * A description of the collection that will be outputted in the generated + * schema as documentation. + */ + description?: string + /** + * Whether to skip the pageInfo field. + */ + skipPageInfo?: boolean + } = {} +): GraphQLObjectType { + const collectionName = name || `${extractNodeTypeName(nodeType)}Collection` + const edgeType = createEdgeType(nodeType, { skipCursor: skipPageInfo }) + + return new GraphQLObjectType({ + name: collectionName, + description: description || `A collection of ${extractNodeTypeName(nodeType)}s`, + fields: { + edges: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(edgeType))), + description: 'A list of edges containing nodes in this collection', + }, + nodes: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(nodeType))), + description: 'The nodes in this collection, directly accessible', + }, + ...(!skipPageInfo && { + pageInfo: { + type: new GraphQLNonNull(PageInfoType), + description: 'Pagination information', + }, + }), + totalCount: { + type: new GraphQLNonNull(GraphQLInt), + description: 'The total count of items available in this collection', + }, + }, + }) +} + +/** + * Interface for standard pagination arguments for a GraphQL connection + */ +interface IPaginationArgs { + first?: number | null + after?: string | null + last?: number | null + before?: string | null +} + +/** + * Standard pagination arguments for a GraphQL connection + */ +export const paginationArgs = { + first: { + type: GraphQLInt, + description: 'Returns the first n elements from the list', + }, + after: { + type: GraphQLString, + description: 'Returns elements that come after the specified cursor', + }, + last: { + type: GraphQLInt, + description: 'Returns the last n elements from the list', + }, + before: { + type: GraphQLString, + description: 'Returns elements that come before the specified cursor', + }, +} + +/** + * Interface for a fetch definition used to fetch a collection of items for a + * GraphQL query. Takes standard pagination args and returns standard page + * information. + */ +interface CollectionFetch { + fetch: ( + args?: IPaginationArgs & { + additionalArgs?: FetchArgs + } + ) => Promise<{ + items: Array + totalCount: number + hasNextPage?: boolean + hasPreviousPage?: boolean + }> + args?: IPaginationArgs & { + additionalArgs?: FetchArgs + } + getCursor?: (item: ItemType, idx?: number) => string + items?: never +} + +/** + * Interface for parameters used to build a collection of items from an array + * in memory + */ +interface CollectionInMemory { + items: Array + args?: IPaginationArgs + fetch?: never + getCursor?: never +} + +/** + * Union type for parameters to build a collection. Can be a remote collection + * that needs to be fetched or a local one in memory. + */ +type CollectionBuildArgs = + | CollectionFetch + | CollectionInMemory + +export class GraphQLCollectionBuilder { + static async create( + options: CollectionBuildArgs + ) { + const { fetch, args = {}, getCursor, items } = options + + if (items) { + return GraphQLCollectionBuilder.paginateArray({ items, args }) + } + + const result = await fetch(args) + const { items: fetchedItems, totalCount, hasNextPage = false, hasPreviousPage = false } = result + const edges = fetchedItems.map((item) => { + return { node: item, cursor: getCursor(item) } + }) + + return { + edges, + nodes: fetchedItems, + totalCount, + pageInfo: { + hasNextPage, + hasPreviousPage, + startCursor: edges.length > 0 ? edges[0].cursor : null, + endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null, + }, + } + } + + private static paginateArray({ items, args }: CollectionInMemory) { + const getCursor = (_item: T, idx: number) => String(idx) + const allEdges = items.map((item, idx) => { + return { node: item, cursor: getCursor(item, idx) } + }) + + const { edges, hasPreviousPage, hasNextPage } = getRequestedSlice(allEdges, args) + + return { + edges, + nodes: edges.map((edge) => edge.node), + totalCount: allEdges.length, + pageInfo: { + hasNextPage, + hasPreviousPage, + startCursor: edges.length > 0 ? edges[0].cursor : null, + endCursor: edges.length > 0 ? edges[edges.length - 1].cursor : null, + }, + } + } +} + +interface TruncatedPageInfo { + edges: Array + hasNextPage: boolean + hasPreviousPage: boolean +} + +function getRequestedSlice( + allEdges: Array, + pageArgs: IPaginationArgs +): TruncatedPageInfo { + let hasPreviousPage = false + let hasNextPage = false + + let beforeIndex = allEdges.length + let afterIndex = -1 + + const requestedBefore = toNumber(pageArgs.before) + if (requestedBefore && requestedBefore >= 0 && requestedBefore < beforeIndex) { + beforeIndex = requestedBefore + hasNextPage = true + } + const requestedAfter = toNumber(pageArgs.after) + if (requestedAfter && requestedAfter >= 0) { + afterIndex = requestedAfter + hasPreviousPage = true + } + + let edges = allEdges.slice(afterIndex + 1, beforeIndex) + + if (pageArgs.first >= 0 && edges.length > pageArgs.first) { + edges = edges.slice(0, pageArgs.first) + hasNextPage = true + } else if (pageArgs.last >= 0 && edges.length > pageArgs.last) { + edges = edges.slice(edges.length - pageArgs.last) + hasPreviousPage = true + } + + return { + edges, + hasNextPage, + hasPreviousPage, + } +} + +function toNumber(value: string): number | undefined { + const num = Number(value) + return Number.isNaN(num) ? undefined : num +} From 9ee61be79d8d89231920018095e9ecd2368e4b15 Mon Sep 17 00:00:00 2001 From: Ahmad Khan <163046609+ahmadkhan100@users.noreply.github.com> Date: Sat, 3 May 2025 10:03:14 -0700 Subject: [PATCH 82/86] doc fixes (#35435) --- .../local-development/declarative-database-schemas.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/docs/content/guides/local-development/declarative-database-schemas.mdx b/apps/docs/content/guides/local-development/declarative-database-schemas.mdx index 37b2b085342..6787f77ba8a 100644 --- a/apps/docs/content/guides/local-development/declarative-database-schemas.mdx +++ b/apps/docs/content/guides/local-development/declarative-database-schemas.mdx @@ -73,8 +73,8 @@ supabase db diff -f create_employees_table - - The new migration file will be used when starting the database locally so you can validate your schema using the local Dashboard. + + Start the local database and then apply the migration file to see your schema changes in the local Dashboard. @@ -82,7 +82,7 @@ supabase db diff -f create_employees_table <$CodeTabs> ```bash name=Terminal -supabase start +supabase start && supabase migration up ``` From 7bf04545d8120b7fcb36cad0184605d3ad5d013a Mon Sep 17 00:00:00 2001 From: Tyler Date: Sun, 4 May 2025 09:28:35 +0900 Subject: [PATCH 83/86] docs: Add some missing descriptions to the CLI reference docs (#35434) * Add description for supabase-config-push * docs: Add descriptions to a few cli commands and command groups * delete the unnecessary note about linking a project * fix/add cli-latest file to gitignore --- .gitignore | 3 ++ apps/docs/spec/cli_v1_commands.yaml | 52 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/.gitignore b/.gitignore index b6ce235b350..e7cd51f7258 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,6 @@ gcloud.json # sitemaps # apps/www/public/*.xml # apps/docs/public/*.xml + +# CLI version file +.temp/cli-latest diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index 47e47d7fa86..77a8a503195 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -904,6 +904,17 @@ commands: - id: supabase-secrets title: supabase secrets summary: Manage Supabase secrets + description: | + Provides tools for managing environment variables and secrets for your Supabase project. + + This command group allows you to set, unset, and list secrets that are securely stored and + made available to Edge Functions as environment variables. + + Secrets management through the CLI is useful for: + - Setting environment-specific configuration + - Managing sensitive credentials securely + + Secrets can be set individually or loaded from .env files for convenience. tags: - management-api links: [] @@ -958,6 +969,15 @@ commands: - id: supabase-projects title: supabase projects summary: Manage Supabase projects + description: | + Provides tools for creating and managing your Supabase projects. + + This command group allows you to list all projects in your organizations, create new projects, + delete existing projects, and retrieve API keys. These operations help you manage your Supabase + infrastructure programmatically without using the dashboard. + + Project management via CLI is especially useful for automation scripts and when you need to + provision environments in a repeatable way. tags: - management-api links: [] @@ -2492,6 +2512,17 @@ commands: - id: supabase-gen-types title: supabase gen types summary: Generate types from Postgres schema + description: | + Automatically generates type definitions based on your Postgres database schema. + + This command connects to your database (local or remote) and generates typed definitions + that match your database tables, views, and stored procedures. By default, it generates TypeScript + definitions, but also supports Go and Swift. + + Generated types give you type safety and autocompletion when working with your database in code, + helping prevent runtime errors and improving developer experience. + + The types respect relationships, constraints, and custom types defined in your database schema. tags: [] links: [] usage: supabase gen types [flags] @@ -2571,6 +2602,14 @@ commands: - id: supabase-functions title: supabase functions summary: Manage Supabase Edge functions + description: | + Supabase Edge Functions are server-less functions that run close to your users. + + Edge Functions allow you to execute custom server-side code without deploying or scaling a traditional server. + They're ideal for handling webhooks, custom API endpoints, data validation, and serving personalized content. + + Edge Functions are written in TypeScript and run on Deno compatible edge runtime, which is a secure runtime + with no package management needed, fast cold starts, and built-in security. tags: - management-api links: [] @@ -2656,6 +2695,15 @@ commands: - id: supabase-functions-new title: supabase functions new summary: Create a new Function locally + description: | + Creates a new Edge Function with boilerplate code in the `supabase/functions` directory. + + This command generates a starter TypeScript file with the necessary Deno imports and a + basic function structure. The function is created as a new directory with the name you specify, + containing an `index.ts` file with the function code. + + After creating the function, you can edit it locally and then use `supabase functions serve` + to test it before deploying with `supabase functions deploy`. tags: [] links: [] usage: supabase functions new @@ -3384,6 +3432,10 @@ commands: - id: supabase-config-push title: supabase config push summary: Pushes local config.toml to the linked project + description: | + Updates the configurations of a linked Supabase project with the local `supabase/config.toml` file. + + This command allows you to manage project configuration as code by defining settings locally and then pushing them to your remote project. tags: [] links: [] usage: supabase config push From 691ab75cb1bfebf1527e0792bdef7e19c0fd2af8 Mon Sep 17 00:00:00 2001 From: divit <27228526+delgado3d@users.noreply.github.com> Date: Sun, 4 May 2025 16:08:01 +0530 Subject: [PATCH 84/86] feat: change default us region to us east 2 (#35452) --- apps/studio/data/misc/get-default-region-query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/data/misc/get-default-region-query.ts b/apps/studio/data/misc/get-default-region-query.ts index 9ec4175bb42..d5e5a81ce10 100644 --- a/apps/studio/data/misc/get-default-region-query.ts +++ b/apps/studio/data/misc/get-default-region-query.ts @@ -12,7 +12,7 @@ import type { CloudProvider } from 'shared-data' import { AWS_REGIONS, FLY_REGIONS } from 'shared-data' const RESTRICTED_POOL = [ - 'WEST_US', + 'EAST_US_2', 'CENTRAL_EU', 'NORTH_EU', 'WEST_EU', From 3c3fe9b5f18c3d7cb7644a425840a17c2fb9cea5 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 5 May 2025 11:27:29 +0800 Subject: [PATCH 85/86] Chore/fix papercuts 020525 (#35427) * Fix giant icon in roles edit button * Fix portals for edge functions and realtime inspector * Set refetchInterval to 2000 in DiskManagementForm if requested_modification in data * Fix --- .../components/interfaces/Database/Roles/RoleRow.tsx | 11 +++++------ .../interfaces/DiskManagement/DiskManagementForm.tsx | 4 +++- .../Realtime/Inspector/ChooseChannelPopover/index.tsx | 2 +- apps/studio/pages/project/[ref]/functions/new.tsx | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx b/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx index 7314623ab07..1e4e15b6cac 100644 --- a/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx +++ b/apps/studio/components/interfaces/Database/Roles/RoleRow.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { toast } from 'sonner' import { Button, + cn, Collapsible, DropdownMenu, DropdownMenuContent, @@ -70,7 +71,7 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { canBypassRls, }} onSubmit={onSaveChanges} - className={[ + className={cn( 'bg-surface-100', 'hover:bg-overlay-hover', 'data-open:bg-selection', @@ -80,8 +81,8 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { '-space-y-px overflow-hidden', 'border border-t-0 first:border-t first:!mt-0 hover:border-t hover:-mt-[1px] shadow transition hover:z-50', 'first:rounded-tl first:rounded-tr', - 'last:rounded-bl last:rounded-br', - ].join(' ')} + 'last:rounded-bl last:rounded-br' + )} > {({ values, initialValues, handleReset }: any) => { const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) @@ -133,9 +134,7 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => { {!disabled && ( - + - +
{config.channelName.length === 0 ? ( <> diff --git a/apps/studio/pages/project/[ref]/functions/new.tsx b/apps/studio/pages/project/[ref]/functions/new.tsx index 26931123ec3..03d47e91046 100644 --- a/apps/studio/pages/project/[ref]/functions/new.tsx +++ b/apps/studio/pages/project/[ref]/functions/new.tsx @@ -281,7 +281,7 @@ const NewFunctionPage = () => { Templates - + From 2b419c70a1826b37bf20d898e296298564aa4f39 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Mon, 5 May 2025 13:18:48 +0800 Subject: [PATCH 86/86] chore: add usercentrics for consent management (#35384) * chore: add usercentrics for consent management * client component to make next.js happy * address feedback * move consent state to common * fix import * ensure page events are sent correctly * add feature flag provider to ui library site * fix ui lib 500 error * skip in test env --------- Co-authored-by: Joshen Lim Co-authored-by: Jordi Enric --- apps/docs/features/helpers.consent.tsx | 15 -- .../features/telemetry/telemetry.client.tsx | 4 +- apps/docs/package.json | 2 +- .../Account/Preferences/AnalyticsSettings.tsx | 15 +- apps/studio/components/ui/GroupsTelemetry.tsx | 9 - .../send-groups-identify-mutation.ts | 7 +- .../telemetry/send-groups-reset-mutation.ts | 7 +- apps/studio/lib/posthog.ts | 7 +- apps/studio/lib/telemetry.tsx | 22 +++ apps/studio/next.config.js | 7 +- apps/studio/package.json | 2 +- apps/studio/pages/_app.tsx | 17 +- apps/studio/sentry.client.config.ts | 9 +- apps/studio/state/app-state.ts | 11 -- .../app/(app)/telemetry-wrapper.tsx | 4 +- apps/ui-library/app/Providers.tsx | 1 - apps/ui-library/app/layout.tsx | 20 ++- apps/www/pages/_app.tsx | 4 +- packages/common/consent-state.ts | 109 ++++++++++++ packages/common/constants/local-storage.ts | 1 + packages/common/feature-flags.tsx | 21 ++- packages/common/index.tsx | 1 + packages/common/package.json | 4 +- packages/common/telemetry.tsx | 66 +++++--- packages/ui-patterns/ConsentToast/index.tsx | 4 +- .../ui-patterns/PrivacySettings/index.tsx | 157 ++++++++++-------- .../ui-patterns/PromoToast/PromoToast.tsx | 7 +- packages/ui-patterns/consent.tsx | 59 +++++++ packages/ui-patterns/index.tsx | 2 +- packages/ui-patterns/package.json | 2 +- packages/ui-patterns/shared/consent.tsx | 134 --------------- pnpm-lock.yaml | 69 +++++--- pnpm-workspace.yaml | 1 + turbo.json | 5 +- 34 files changed, 443 insertions(+), 362 deletions(-) delete mode 100644 apps/docs/features/helpers.consent.tsx create mode 100644 apps/studio/lib/telemetry.tsx create mode 100644 packages/common/consent-state.ts create mode 100644 packages/ui-patterns/consent.tsx delete mode 100644 packages/ui-patterns/shared/consent.tsx diff --git a/apps/docs/features/helpers.consent.tsx b/apps/docs/features/helpers.consent.tsx deleted file mode 100644 index fb4025cbd4d..00000000000 --- a/apps/docs/features/helpers.consent.tsx +++ /dev/null @@ -1,15 +0,0 @@ -'use client' - -import { useEffect } from 'react' -import { useConsent } from 'ui-patterns/ConsentToast' - -/** - * Helper client component to trigger the consent toast. It has to be a client component. - */ -export const TriggerConsentToast = () => { - const { triggerConsentToast } = useConsent() - useEffect(() => { - setTimeout(() => triggerConsentToast(), 1000) - }, [triggerConsentToast]) - return null -} diff --git a/apps/docs/features/telemetry/telemetry.client.tsx b/apps/docs/features/telemetry/telemetry.client.tsx index ee1853b8d74..54b492e7989 100644 --- a/apps/docs/features/telemetry/telemetry.client.tsx +++ b/apps/docs/features/telemetry/telemetry.client.tsx @@ -1,11 +1,11 @@ 'use client' import { IS_PLATFORM, PageTelemetry as PageTelemetryImpl } from 'common' -import { useConsent } from 'ui-patterns/ConsentToast' +import { useConsentToast } from 'ui-patterns/consent' import { API_URL } from '~/lib/constants' const PageTelemetry = () => { - const { hasAcceptedConsent } = useConsent() + const { hasAcceptedConsent } = useConsentToast() return ( { - const snap = useAppStateSnapshot() + const { hasAccepted, acceptAll, denyAll } = useConsentState() const { mutate: sendReset } = useSendResetMutation() const onToggleOptIn = () => { - const value = !snap.isOptedInTelemetry ? 'true' : 'false' - snap.setIsOptedInTelemetry(value === 'true') - if (value === 'false') sendReset() + if (hasAccepted) { + denyAll() + sendReset() + } else { + acceptAll() + } } return ( Analytics}> { const encoder = new TextEncoder() @@ -29,7 +28,6 @@ const GroupsTelemetry = ({ hasAcceptedConsent }: { hasAcceptedConsent: boolean } const user = useUser() const router = useRouter() const { ref, slug } = useParams() - const snap = useAppStateSnapshot() const organization = useSelectedOrganization() const previousPathname = usePrevious(router.pathname) @@ -41,13 +39,6 @@ const GroupsTelemetry = ({ hasAcceptedConsent }: { hasAcceptedConsent: boolean } const referrer = typeof document !== 'undefined' ? document?.referrer : '' useTelemetryCookie({ hasAcceptedConsent, title, referrer }) - useEffect(() => { - if (hasAcceptedConsent) { - snap.setIsOptedInTelemetry(hasAcceptedConsent) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hasAcceptedConsent]) - useEffect(() => { // don't set the sentry user id if the user hasn't logged in (so that Sentry errors show null user id instead of anonymous id) if (!user?.id) { diff --git a/apps/studio/data/telemetry/send-groups-identify-mutation.ts b/apps/studio/data/telemetry/send-groups-identify-mutation.ts index 6ad7e4a534c..07d8b316fc9 100644 --- a/apps/studio/data/telemetry/send-groups-identify-mutation.ts +++ b/apps/studio/data/telemetry/send-groups-identify-mutation.ts @@ -1,7 +1,7 @@ import { useMutation, UseMutationOptions } from '@tanstack/react-query' import { components } from 'api-types' -import { LOCAL_STORAGE_KEYS } from 'common' +import { hasConsented } from 'common' import { handleError, post } from 'data/fetchers' import { IS_PLATFORM } from 'lib/constants' import type { ResponseError } from 'types' @@ -9,10 +9,7 @@ import type { ResponseError } from 'types' export type SendGroupsIdentifyVariables = components['schemas']['TelemetryGroupsIdentityBody'] export async function sendGroupsIdentify({ body }: { body: SendGroupsIdentifyVariables }) { - const consent = - (typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null) === 'true' + const consent = hasConsented() if (!consent || !IS_PLATFORM) return undefined diff --git a/apps/studio/data/telemetry/send-groups-reset-mutation.ts b/apps/studio/data/telemetry/send-groups-reset-mutation.ts index 28805372b52..6f8524547f4 100644 --- a/apps/studio/data/telemetry/send-groups-reset-mutation.ts +++ b/apps/studio/data/telemetry/send-groups-reset-mutation.ts @@ -1,7 +1,7 @@ import { useMutation, UseMutationOptions } from '@tanstack/react-query' import { components } from 'api-types' -import { LOCAL_STORAGE_KEYS } from 'common' +import { hasConsented } from 'common' import { handleError, post } from 'data/fetchers' import { IS_PLATFORM } from 'lib/constants' import type { ResponseError } from 'types' @@ -9,10 +9,7 @@ import type { ResponseError } from 'types' export type SendGroupsResetVariables = components['schemas']['TelemetryGroupsResetBody'] export async function sendGroupsReset({ body }: { body: SendGroupsResetVariables }) { - const consent = - (typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null) === 'true' + const consent = hasConsented() if (!consent || !IS_PLATFORM) return undefined diff --git a/apps/studio/lib/posthog.ts b/apps/studio/lib/posthog.ts index c3560627bc2..96735b0b77f 100644 --- a/apps/studio/lib/posthog.ts +++ b/apps/studio/lib/posthog.ts @@ -1,15 +1,12 @@ import { components } from 'api-types' -import { LOCAL_STORAGE_KEYS } from 'common' +import { hasConsented } from 'common' import { handleError, post } from 'data/fetchers' import { IS_PLATFORM } from './constants' type TrackFeatureFlagVariables = components['schemas']['TelemetryFeatureFlagBodyDto'] export async function trackFeatureFlag(body: TrackFeatureFlagVariables) { - const consent = - (typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null) === 'true' + const consent = hasConsented() if (!consent || !IS_PLATFORM) return undefined const { data, error } = await post(`/platform/telemetry/feature-flags/track`, { body }) diff --git a/apps/studio/lib/telemetry.tsx b/apps/studio/lib/telemetry.tsx new file mode 100644 index 00000000000..84b40e143db --- /dev/null +++ b/apps/studio/lib/telemetry.tsx @@ -0,0 +1,22 @@ +import { PageTelemetry } from 'common' +import GroupsTelemetry from 'components/ui/GroupsTelemetry' +import { API_URL, IS_PLATFORM } from 'lib/constants' +import { useConsentToast } from 'ui-patterns/consent' + +export function Telemetry() { + // Although this is "technically" breaking the rules of hooks + // IS_PLATFORM never changes within a session, so this won't cause any issues + // eslint-disable-next-line react-hooks/rules-of-hooks + const { hasAcceptedConsent } = IS_PLATFORM ? useConsentToast() : { hasAcceptedConsent: true } + + return ( + <> + + + + ) +} diff --git a/apps/studio/next.config.js b/apps/studio/next.config.js index 7a6aeb0bbaf..68523a09843 100644 --- a/apps/studio/next.config.js +++ b/apps/studio/next.config.js @@ -56,14 +56,17 @@ const SUPABASE_ASSETS_URL = ? 'https://frontend-assets.supabase.green' : 'https://frontend-assets.supabase.com' +const USERCENTRICS_URLS = 'https://*.usercentrics.eu' +const USERCENTRICS_APP_URL = 'https://app.usercentrics.eu' + // used by vercel live preview const PUSHER_URL = 'https://*.pusher.com' const PUSHER_URL_WS = 'wss://*.pusher.com' -const DEFAULT_SRC_URLS = `${API_URL} ${SUPABASE_URL} ${GOTRUE_URL} ${SUPABASE_LOCAL_PROJECTS_URL_WS} ${SUPABASE_PROJECTS_URL} ${SUPABASE_PROJECTS_URL_WS} ${HCAPTCHA_SUBDOMAINS_URL} ${CONFIGCAT_URL} ${STRIPE_SUBDOMAINS_URL} ${STRIPE_NETWORK_URL} ${CLOUDFLARE_URL} ${ONE_ONE_ONE_ONE_URL} ${VERCEL_INSIGHTS_URL} ${GITHUB_API_URL} ${GITHUB_USER_CONTENT_URL} ${SUPABASE_ASSETS_URL}` +const DEFAULT_SRC_URLS = `${API_URL} ${SUPABASE_URL} ${GOTRUE_URL} ${SUPABASE_LOCAL_PROJECTS_URL_WS} ${SUPABASE_PROJECTS_URL} ${SUPABASE_PROJECTS_URL_WS} ${HCAPTCHA_SUBDOMAINS_URL} ${CONFIGCAT_URL} ${STRIPE_SUBDOMAINS_URL} ${STRIPE_NETWORK_URL} ${CLOUDFLARE_URL} ${ONE_ONE_ONE_ONE_URL} ${VERCEL_INSIGHTS_URL} ${GITHUB_API_URL} ${GITHUB_USER_CONTENT_URL} ${SUPABASE_ASSETS_URL} ${USERCENTRICS_URLS}` const SCRIPT_SRC_URLS = `${CLOUDFLARE_CDN_URL} ${HCAPTCHA_JS_URL} ${STRIPE_JS_URL} ${SUPABASE_ASSETS_URL}` const FRAME_SRC_URLS = `${HCAPTCHA_ASSET_URL} ${STRIPE_JS_URL}` -const IMG_SRC_URLS = `${SUPABASE_URL} ${SUPABASE_COM_URL} ${SUPABASE_PROJECTS_URL} ${GITHUB_USER_AVATAR_URL} ${GOOGLE_USER_AVATAR_URL} ${SUPABASE_ASSETS_URL}` +const IMG_SRC_URLS = `${SUPABASE_URL} ${SUPABASE_COM_URL} ${SUPABASE_PROJECTS_URL} ${GITHUB_USER_AVATAR_URL} ${GOOGLE_USER_AVATAR_URL} ${SUPABASE_ASSETS_URL} ${USERCENTRICS_APP_URL}` const STYLE_SRC_URLS = `${CLOUDFLARE_CDN_URL} ${SUPABASE_ASSETS_URL}` const FONT_SRC_URLS = `${CLOUDFLARE_CDN_URL} ${SUPABASE_ASSETS_URL}` diff --git a/apps/studio/package.json b/apps/studio/package.json index 393216104e5..90203f3af6c 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -125,7 +125,7 @@ "ui-patterns": "workspace:*", "use-debounce": "^7.0.1", "uuid": "^9.0.1", - "valtio": "^1.12.0", + "valtio": "catalog:", "yup": "^1.4.0", "yup-password": "^0.3.0", "zod": "^3.22.4", diff --git a/apps/studio/pages/_app.tsx b/apps/studio/pages/_app.tsx index 6f00417d4d1..82dde900ed4 100644 --- a/apps/studio/pages/_app.tsx +++ b/apps/studio/pages/_app.tsx @@ -31,7 +31,7 @@ import { NuqsAdapter } from 'nuqs/adapters/next/pages' import { ErrorInfo } from 'react' import { ErrorBoundary } from 'react-error-boundary' -import { FeatureFlagProvider, PageTelemetry, ThemeProvider, useThemeSandbox } from 'common' +import { FeatureFlagProvider, ThemeProvider, useThemeSandbox } from 'common' import MetaFaviconsPagesRouter from 'common/MetaFavicons/pages-router' import { RouteValidationWrapper } from 'components/interfaces/App' import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext' @@ -41,18 +41,17 @@ import FeaturePreviewModal from 'components/interfaces/App/FeaturePreview/Featur import { MonacoThemeProvider } from 'components/interfaces/App/MonacoThemeProvider' import { GenerateSql } from 'components/interfaces/SqlGenerator/SqlGenerator' import { ErrorBoundaryState } from 'components/ui/ErrorBoundaryState' -import GroupsTelemetry from 'components/ui/GroupsTelemetry' import { useRootQueryClient } from 'data/query-client' import { customFont, sourceCodePro } from 'fonts' import { AuthProvider } from 'lib/auth' import { getFlags as getConfigCatFlags } from 'lib/configcat' import { API_URL, BASE_PATH, IS_PLATFORM } from 'lib/constants' import { ProfileProvider } from 'lib/profile' +import { Telemetry } from 'lib/telemetry' import HCaptchaLoadedStore from 'stores/hcaptcha-loaded-store' import { AppPropsWithLayout } from 'types' import { SonnerToaster, TooltipProvider } from 'ui' import { CommandProvider } from 'ui-patterns/CommandMenu' -import { useConsent } from 'ui-patterns/ConsentToast' dayjs.extend(customParseFormat) dayjs.extend(utc) @@ -93,11 +92,6 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { useThemeSandbox() - // Although this is "technically" breaking the rules of hooks - // IS_PLATFORM never changes within a session, so this won't cause any issues - // eslint-disable-next-line react-hooks/rules-of-hooks - const { hasAcceptedConsent } = IS_PLATFORM ? useConsent() : { hasAcceptedConsent: true } - const isTestEnv = process.env.NEXT_PUBLIC_NODE_ENV === 'test' return ( @@ -146,12 +140,7 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { - - + {!isTestEnv && } {!isTestEnv && ( diff --git a/apps/studio/sentry.client.config.ts b/apps/studio/sentry.client.config.ts index 44cc043b029..f107f699cb2 100644 --- a/apps/studio/sentry.client.config.ts +++ b/apps/studio/sentry.client.config.ts @@ -3,8 +3,8 @@ // https://docs.sentry.io/platforms/javascript/guides/nextjs/ import * as Sentry from '@sentry/nextjs' +import { hasConsented } from 'common' import { IS_PLATFORM } from 'common/constants/environment' -import { LOCAL_STORAGE_KEYS } from 'common/constants/local-storage' import { match } from 'path-to-regexp' // This is a workaround to ignore hCaptcha related errors. @@ -26,12 +26,9 @@ Sentry.init({ // Setting this option to true will print useful information to the console while you're setting up Sentry. debug: false, beforeSend(event, hint) { - const consent = - typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null + const consent = hasConsented() - if (IS_PLATFORM && consent === 'true') { + if (IS_PLATFORM && consent) { // Ignore invalid URL events for 99% of the time because it's using up a lot of quota. const isInvalidUrlEvent = (hint.originalException as any)?.message?.includes( `Failed to construct 'URL': Invalid URL` diff --git a/apps/studio/state/app-state.ts b/apps/studio/state/app-state.ts index 84438cfba36..186f4078c61 100644 --- a/apps/studio/state/app-state.ts +++ b/apps/studio/state/app-state.ts @@ -1,6 +1,5 @@ import { proxy, snapshot, subscribe, useSnapshot } from 'valtio' -import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common' import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries' import { LOCAL_STORAGE_KEYS } from 'lib/constants' @@ -52,7 +51,6 @@ const getInitialState = () => { activeDocsSection: ['introduction'], docsLanguage: 'js', showProjectApiDocs: false, - isOptedInTelemetry: false, showEnableBranchingModal: false, showFeaturePreviewModal: false, selectedFeaturePreview: '', @@ -84,7 +82,6 @@ const getInitialState = () => { activeDocsSection: ['introduction'], docsLanguage: 'js', showProjectApiDocs: false, - isOptedInTelemetry: false, showEnableBranchingModal: false, showFeaturePreviewModal: false, selectedFeaturePreview: '', @@ -124,14 +121,6 @@ export const appState = proxy({ appState.docsLanguage = value }, - isOptedInTelemetry: false, - setIsOptedInTelemetry: (value: boolean | null) => { - appState.isOptedInTelemetry = value === null ? false : value - if (typeof window !== 'undefined' && value !== null) { - localStorage.setItem(COMMON_LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT, value.toString()) - } - }, - showEnableBranchingModal: false, setShowEnableBranchingModal: (value: boolean) => { appState.showEnableBranchingModal = value diff --git a/apps/ui-library/app/(app)/telemetry-wrapper.tsx b/apps/ui-library/app/(app)/telemetry-wrapper.tsx index 5460e46156b..124d9c5477a 100644 --- a/apps/ui-library/app/(app)/telemetry-wrapper.tsx +++ b/apps/ui-library/app/(app)/telemetry-wrapper.tsx @@ -2,10 +2,10 @@ import { API_URL } from '@/lib/constants' import { IS_PLATFORM, PageTelemetry } from 'common' -import { useConsent } from 'ui-patterns/ConsentToast' +import { useConsentToast } from 'ui-patterns/consent' export const TelemetryWrapper = () => { - const { hasAcceptedConsent } = useConsent() + const { hasAcceptedConsent } = useConsentToast() return ( - - {children} - - + + + {children} + + + ) diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx index dbb1b0e053f..982d2721748 100644 --- a/apps/www/pages/_app.tsx +++ b/apps/www/pages/_app.tsx @@ -16,7 +16,7 @@ import Head from 'next/head' import { useRouter } from 'next/router' import { SonnerToaster, themes, TooltipProvider } from 'ui' import { CommandProvider } from 'ui-patterns/CommandMenu' -import { useConsent } from 'ui-patterns/ConsentToast' +import { useConsentToast } from 'ui-patterns/consent' import MetaFaviconsPagesRouter, { DEFAULT_FAVICON_ROUTE, @@ -28,7 +28,7 @@ import useDarkLaunchWeeks from '../hooks/useDarkLaunchWeeks' export default function App({ Component, pageProps }: AppProps) { const router = useRouter() - const { hasAcceptedConsent } = useConsent() + const { hasAcceptedConsent } = useConsentToast() useThemeSandbox() diff --git a/packages/common/consent-state.ts b/packages/common/consent-state.ts new file mode 100644 index 00000000000..10b5b467992 --- /dev/null +++ b/packages/common/consent-state.ts @@ -0,0 +1,109 @@ +import type Usercentrics from '@usercentrics/cmp-browser-sdk' +import type { BaseCategory, UserDecision } from '@usercentrics/cmp-browser-sdk' +import { proxy, snapshot, useSnapshot } from 'valtio' +import { LOCAL_STORAGE_KEYS } from './constants' + +export const consentState = proxy({ + // Usercentrics state + UC: null as Usercentrics | null, + categories: null as BaseCategory[] | null, + + // Our state + showConsentToast: false, + hasConsented: false, + acceptAll: () => { + if (!consentState.UC) return + const previousConsentValue = consentState.hasConsented + + consentState.hasConsented = true + consentState.showConsentToast = false + + consentState.UC.acceptAllServices() + .then(() => { + consentState.categories = consentState.UC?.getCategoriesBaseInfo() ?? null + }) + .catch(() => { + consentState.hasConsented = previousConsentValue + consentState.showConsentToast = true + }) + }, + denyAll: () => { + if (!consentState.UC) return + const previousConsentValue = consentState.hasConsented + + consentState.hasConsented = false + consentState.showConsentToast = false + + consentState.UC.denyAllServices() + .then(() => { + consentState.categories = consentState.UC?.getCategoriesBaseInfo() ?? null + }) + .catch(() => { + consentState.showConsentToast = previousConsentValue + }) + }, + updateServices: (decisions: UserDecision[]) => { + if (!consentState.UC) return + + consentState.showConsentToast = false + + consentState.UC.updateServices(decisions) + .then(() => { + consentState.hasConsented = consentState.UC?.areAllConsentsAccepted() ?? false + consentState.categories = consentState.UC?.getCategoriesBaseInfo() ?? null + }) + .catch(() => { + consentState.showConsentToast = true + }) + }, +}) + +async function initUserCentrics() { + if (process.env.NODE_ENV === 'test') return + + const { default: Usercentrics } = await import('@usercentrics/cmp-browser-sdk') + + const UC = new Usercentrics(process.env.NEXT_PUBLIC_USERCENTRICS_RULESET_ID!, { + rulesetId: process.env.NEXT_PUBLIC_USERCENTRICS_RULESET_ID, + useRulesetId: true, + }) + + const initialUIValues = await UC.init() + + consentState.UC = UC + const hasConsented = UC.areAllConsentsAccepted() + + // 0 = first layer, aka show consent toast + consentState.showConsentToast = initialUIValues.initialLayer === 0 + consentState.hasConsented = hasConsented + consentState.categories = UC.getCategoriesBaseInfo() + + // If the user has previously consented (before usercentrics), accept all services + if (!hasConsented && localStorage?.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) === 'true') { + consentState.acceptAll() + localStorage.removeItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) + } +} + +// Usercentrics is not available on the server +if (typeof window !== 'undefined') { + initUserCentrics() +} + +// Public API for consent + +export function hasConsented() { + return snapshot(consentState).hasConsented +} + +export function useConsentState() { + const snap = useSnapshot(consentState) + + return { + hasAccepted: snap.hasConsented, + categories: snap.categories as BaseCategory[] | null, + acceptAll: snap.acceptAll, + denyAll: snap.denyAll, + updateServices: snap.updateServices, + } +} diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index 2aa48c9d1ad..f65187df517 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -1,4 +1,5 @@ export const LOCAL_STORAGE_KEYS = { + /** @deprecated – we're using usercentrics instead to handle telemetry consent */ TELEMETRY_CONSENT: 'supabase-consent-ph', TELEMETRY_DATA: 'supabase-telemetry-data', HIDE_PROMO_TOAST: 'supabase-hide-promo-toast-lw13-d1', diff --git a/packages/common/feature-flags.tsx b/packages/common/feature-flags.tsx index 8e85b519fd4..d05507c030a 100644 --- a/packages/common/feature-flags.tsx +++ b/packages/common/feature-flags.tsx @@ -5,7 +5,7 @@ import { createContext, PropsWithChildren, useContext, useEffect, useState } fro import { components } from 'api-types' import { useUser } from './auth' -import { LOCAL_STORAGE_KEYS } from './constants' +import { hasConsented } from './consent-state' import { get, post } from './fetchWrappers' import { ensurePlatformSuffix } from './helpers' @@ -19,10 +19,7 @@ export async function getFeatureFlags(API_URL: string) { } export async function trackFeatureFlag(API_URL: string, body: TrackFeatureFlagVariables) { - const consent = - (typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null) === 'true' + const consent = hasConsented() if (!consent) return undefined await post(`${ensurePlatformSuffix(API_URL)}/telemetry/feature-flags/track`, { body }) @@ -83,15 +80,21 @@ export const FeatureFlagProvider = ({ let flagStore: FeatureFlagContextType = { configcat: {}, posthog: {} } - // Load PH flags - const flags = await getFeatureFlags(API_URL) + // Run both async operations in parallel + const [flags, flagValues] = await Promise.all([ + getFeatureFlags(API_URL), + typeof getConfigCatFlags === 'function' + ? getConfigCatFlags(user?.email) + : Promise.resolve([]), + ]) + + // Process PostHog flags if (flags) { flagStore.posthog = flags } - // Load ConfigCat flags + // Process ConfigCat flags if (typeof getConfigCatFlags === 'function') { - const flagValues = await getConfigCatFlags(user?.email) let overridesCookieValue: Record = {} try { const cookies = getCookies() diff --git a/packages/common/index.tsx b/packages/common/index.tsx index bb0ac6c5552..863f4ebbc46 100644 --- a/packages/common/index.tsx +++ b/packages/common/index.tsx @@ -1,4 +1,5 @@ export * from './auth' +export * from './consent-state' export * from './constants' export * from './database-types' export * from './gotrue' diff --git a/packages/common/package.json b/packages/common/package.json index 9acff427616..79f9e1be362 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -12,13 +12,15 @@ }, "dependencies": { "@types/dat.gui": "^0.7.12", + "@usercentrics/cmp-browser-sdk": "^4.42.0", "@vercel/flags": "^2.6.0", "api-types": "workspace:*", "config": "workspace:*", "dat.gui": "^0.7.9", "lodash": "^4.17.21", "next-themes": "^0.3.0", - "react-use": "^17.4.0" + "react-use": "^17.4.0", + "valtio": "catalog:" }, "devDependencies": { "@types/lodash": "4.17.5", diff --git a/packages/common/telemetry.tsx b/packages/common/telemetry.tsx index 51e9295aee1..d401d209d92 100644 --- a/packages/common/telemetry.tsx +++ b/packages/common/telemetry.tsx @@ -4,15 +4,19 @@ import { components } from 'api-types' import { useRouter } from 'next/compat/router' import { usePathname } from 'next/navigation' import { useCallback, useEffect, useRef } from 'react' +import { useLatest } from 'react-use' +import { useUser } from './auth' +import { hasConsented } from './consent-state' import { LOCAL_STORAGE_KEYS } from './constants' import { useFeatureFlags } from './feature-flags' import { post } from './fetchWrappers' import { ensurePlatformSuffix, isBrowser } from './helpers' import { useTelemetryCookie } from './hooks' import { TelemetryEvent } from './telemetry-constants' -import { useUser } from './auth' import { getSharedTelemetryData } from './telemetry-utils' +const { TELEMETRY_DATA } = LOCAL_STORAGE_KEYS + //--- // PAGE TELEMETRY //--- @@ -74,51 +78,67 @@ export const PageTelemetry = ({ const referrer = typeof document !== 'undefined' ? document?.referrer : '' useTelemetryCookie({ hasAcceptedConsent, title, referrer }) + const pathname = + pagesPathname ?? appPathname ?? (isBrowser ? window.location.pathname : undefined) + const pathnameRef = useLatest(pathname) + const featureFlagsRef = useLatest(featureFlags.posthog) + const sendPageTelemetry = useCallback(() => { if (!(enabled && hasAcceptedConsent)) return Promise.resolve() - return handlePageTelemetry( - API_URL, - pagesPathname ?? appPathname ?? undefined, - featureFlags.posthog - ).catch((e) => { + return handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current).catch((e) => { console.error('Problem sending telemetry page:', e) }) - }, [API_URL, pagesPathname, appPathname, hasAcceptedConsent, featureFlags.posthog]) + }, [API_URL, enabled, hasAcceptedConsent]) const sendPageLeaveTelemetry = useCallback(() => { if (!(enabled && hasAcceptedConsent)) return Promise.resolve() - return handlePageTelemetry( - API_URL, - pagesPathname ?? appPathname ?? undefined, - featureFlags.posthog - ).catch((e) => { + return handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current).catch((e) => { console.error('Problem sending telemetry page-leave:', e) }) - }, [pagesPathname, appPathname, hasAcceptedConsent, featureFlags.posthog]) + }, [API_URL, enabled, hasAcceptedConsent]) + // Handle initial page telemetry event const hasSentInitialPageTelemetryRef = useRef(false) - useEffect(() => { // Send page telemetry on first page load // Waiting for router ready before sending page_view // if not the path will be dynamic route instead of the browser url if ( (router?.isReady ?? true) && + hasAcceptedConsent && featureFlags.hasLoaded && !hasSentInitialPageTelemetryRef.current ) { - sendPageTelemetry() + const cookies = document.cookie.split(';') + const telemetryCookie = cookies.find((cookie) => cookie.trim().startsWith(TELEMETRY_DATA)) + if (telemetryCookie) { + try { + const encodedData = telemetryCookie.split('=')[1] + const telemetryData = JSON.parse(decodeURIComponent(encodedData)) + handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current, telemetryData) + // remove the telemetry cookie + document.cookie = `${TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` + } catch (error) { + console.error('Invalid telemetry data:', error) + } + } else { + handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current) + } + hasSentInitialPageTelemetryRef.current = true } - }, [router?.isReady, featureFlags.hasLoaded]) + }, [router?.isReady, hasAcceptedConsent, featureFlags.hasLoaded]) useEffect(() => { // For pages router if (router === null) return function handleRouteChange() { + // Wait until we've sent the initial page telemetry event + if (!hasSentInitialPageTelemetryRef.current) return + sendPageTelemetry() } @@ -134,7 +154,8 @@ export const PageTelemetry = ({ // For app router if (router !== null) return - if (appPathname) { + // Wait until we've sent the initial page telemetry event + if (appPathname && !hasSentInitialPageTelemetryRef.current) { sendPageTelemetry() } }, [appPathname, router, sendPageTelemetry]) @@ -161,11 +182,7 @@ export const PageTelemetry = ({ type EventBody = components['schemas']['TelemetryEventBodyV2Dto'] export function sendTelemetryEvent(API_URL: string, event: TelemetryEvent, pathname?: string) { - const consent = - (typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null) === 'true' - + const consent = hasConsented() if (!consent) return const body: EventBody = { @@ -194,10 +211,7 @@ export function sendTelemetryEvent(API_URL: string, event: TelemetryEvent, pathn type IdentifyBody = components['schemas']['TelemetryIdentifyBodyV2'] export function sendTelemetryIdentify(API_URL: string, body: IdentifyBody) { - const consent = - (typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null) === 'true' + const consent = hasConsented() if (!consent) return Promise.resolve() diff --git a/packages/ui-patterns/ConsentToast/index.tsx b/packages/ui-patterns/ConsentToast/index.tsx index cdf639a05b1..aeb288b8f74 100644 --- a/packages/ui-patterns/ConsentToast/index.tsx +++ b/packages/ui-patterns/ConsentToast/index.tsx @@ -17,7 +17,7 @@ export const ConsentToast = ({ onAccept = noop, onOptOut = noop }: ConsentToastP

- We use first-party cookies to improve our services.{' '} + We use cookies to collect data and improve our services.{' '} ) } - -export { useConsent, useConsentValue } from '../shared/consent' diff --git a/packages/ui-patterns/PrivacySettings/index.tsx b/packages/ui-patterns/PrivacySettings/index.tsx index e1981ce5b66..ea1607b3fb7 100644 --- a/packages/ui-patterns/PrivacySettings/index.tsx +++ b/packages/ui-patterns/PrivacySettings/index.tsx @@ -1,40 +1,45 @@ 'use client' -import { handleResetTelemetry, LOCAL_STORAGE_KEYS } from 'common' +import { useConsentState } from 'common' import Link from 'next/link' -import { PropsWithChildren, useEffect, useState } from 'react' +import { PropsWithChildren, useState } from 'react' import { Modal, Toggle } from 'ui' -import { useConsentValue } from '../shared/consent' + +interface PrivacySettingsProps { + className?: string +} export const PrivacySettings = ({ children, ...props -}: PropsWithChildren<{ className?: string }>) => { +}: PropsWithChildren) => { const [isOpen, setIsOpen] = useState(false) - const { hasAccepted, handleConsent } = useConsentValue(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - const [telemetryValue, setTelemetryValue] = useState(hasAccepted) + const { categories, updateServices } = useConsentState() - // Every time the modal opens, sync state with localStorage - useEffect(() => { - setTelemetryValue(localStorage?.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) === 'true') - }, [isOpen]) + const [serviceConsentMap, setServiceConsentMap] = useState(() => new Map()) + + function handleServicesChange(services: { id: string; status: boolean }[]) { + let newServiceConsentMap = new Map(serviceConsentMap) + services.forEach((service) => { + newServiceConsentMap.set(service.id, service.status) + }) + setServiceConsentMap(newServiceConsentMap) + } const handleConfirmPreferences = () => { - handleConsent && handleConsent(telemetryValue ? 'true' : 'false') + const services = Array.from(serviceConsentMap.entries()).map(([id, status]) => ({ + serviceId: id, + status, + })) + updateServices(services) + setIsOpen(false) } const handleCancel = () => { - setTelemetryValue(hasAccepted) setIsOpen(false) } - const handleOptOutTelemetry = async () => { - // remove telemetry data from cookies - document.cookie = `${LOCAL_STORAGE_KEYS.TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/` - handleResetTelemetry(process.env.NEXT_PUBLIC_API_URL!) - } - return ( <>