From d5107e4ea26fb85781cebf06f4b17b927f736296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Tue, 1 Oct 2024 11:24:37 +0200 Subject: [PATCH 01/13] docs: Add section about dynamically generated redirect url (#29612) --- .../oauth-apps/build-a-supabase-integration.mdx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/docs/content/guides/platform/oauth-apps/build-a-supabase-integration.mdx b/apps/docs/content/guides/platform/oauth-apps/build-a-supabase-integration.mdx index 99ed57356c7..b64028b5ec4 100644 --- a/apps/docs/content/guides/platform/oauth-apps/build-a-supabase-integration.mdx +++ b/apps/docs/content/guides/platform/oauth-apps/build-a-supabase-integration.mdx @@ -65,6 +65,12 @@ Exchange the authorization code for an access and refresh token by calling [`POS - `redirect_uri`: This must be exactly the same URL used in the first step. - (Recommended) `code_verifier`: If you used the PKCE flow in the first step, include the code verifier as `code_verifier`. + + +If your application need to support dynamically generated Redirect URLs, check out [Handling Dynamic Redirect URLs](#handling-dynamic-redirect-urls) section below. + + + As per OAuth2 spec, provide the client id and client secret as basic auth header: - `client_id`: The unique client ID identifying your OAuth App. @@ -151,6 +157,11 @@ When creating a new project, you can either ask the user to provide a database p You can configure the user's [custom SMTP settings](https://supabase.com/docs/guides/auth/auth-smtp) using the [`/config/auth` endpoint](https://api.supabase.com/api/v1#/projects%20config/updateV1AuthConfig). +### Handling Dynamic Redirect URLs + +To handle multiple, dynamically generated redirect URLs within the same OAuth app, you can leverage the `state` query parameter. When starting the OAuth process, include the desired, encoded redirect URL in the `state` parameter. +Once authorization is complete, we will sends the `state` value back to your app. You can then verify its integrity and extract the correct redirect URL, decoding it and redirecting the user to the correct URL. + ## Current limitations Only some features are available until we roll out fine-grained access control. If you need full database access, you will need to prompt the user for their database password. From cb1e3a81708202f06f57584714c7adef2a2a2fd0 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Tue, 1 Oct 2024 11:29:37 +0200 Subject: [PATCH 02/13] chore: Prep work for `pnpm` (#29610) * Add all missing libraries in the packages/apps. * Add all missing Vitest imports to the tests. * Add some missing exports to ui. * Fix the admonition export. * Fix various minor bugs. * Migrate the ui package to use vitest. --- .../design-system/components/command-menu.tsx | 8 +- apps/design-system/components/copy-button.tsx | 13 ++- apps/design-system/lib/utils.ts | 8 +- apps/design-system/package.json | 11 ++- .../example/dropdown-menu-checkboxes.tsx | 4 +- .../default/example/input-otp-pattern.tsx | 4 +- .../default/example/tree-view-demo.tsx | 3 +- .../default/example/tree-view-directories.tsx | 3 +- .../default/example/tree-view-edit.tsx | 5 +- .../example/tree-view-multi-select.tsx | 5 +- apps/docs/components/index.tsx | 3 +- apps/docs/features/docs/MdxBase.shared.tsx | 7 +- .../features/docs/Reference.mdx.client.tsx | 2 +- apps/docs/lib/mdx/plugins/remarkAdmonition.ts | 2 +- apps/docs/next-env.d.ts | 1 + apps/docs/package.json | 17 +++- apps/docs/types/mdx.d.ts | 5 ++ .../pagination/Pagination.utils.test.ts | 1 + .../interfaces/Auth/Users/UserOverview.tsx | 3 +- .../CreateWarehouseCollection.tsx | 24 ++--- .../DiskManagement/DiskManagement.test.ts | 3 +- .../UpdateRolesPanel.utils.test.ts | 1 + .../SQLEditor/SQLEditor.utils.test.ts | 1 + .../Settings/API/PostgrestConfig.tsx | 4 +- .../Infrastructure/InfrastructureActivity.tsx | 3 +- .../TableGridEditor/TableEntity.utils.test.ts | 1 + apps/studio/package.json | 14 ++- apps/studio/pages/new/[slug].tsx | 2 +- .../components/Billing/TaxID.utils.test.ts | 1 + .../tests/components/CopyButton.test.tsx | 2 +- .../RemoveJSONTrailingComma.utils.test.ts | 1 + .../components/Editor/RowEditor.utils.test.ts | 4 +- .../Editor/SpreadsheetImport.utils.test.ts | 1 + .../tests/components/Grid/Grid.utils.test.ts | 1 + .../tests/components/ProjectDropdown.test.ts | 1 + .../components/Reports/ReportWidget.test.tsx | 1 + .../Storage/StorageSettings.utils.test.ts | 1 + .../components/ui/Charts/Charts.utils.test.ts | 3 +- apps/studio/tests/config/msw.test.ts | 1 + apps/studio/tests/config/router.test.tsx | 4 +- .../pages/projects/LogEventChart.test.tsx | 4 +- .../tests/pages/projects/LogTable.test.tsx | 6 +- .../pages/projects/Logs.Datepickers.test.tsx | 2 +- .../tests/pages/projects/Logs.utils.test.ts | 3 +- .../pages/projects/LogsPreviewer.test.tsx | 8 +- .../pages/projects/LogsQueryPanel.test.tsx | 4 +- .../projects/PreviewFilterPanel.test.tsx | 6 +- .../projects/logs-explorer/saved.test.tsx | 4 +- .../tests/pages/projects/logs-query.test.tsx | 4 +- .../projects/reports/api-report.test.tsx | 3 +- .../projects/reports/storage-report.test.tsx | 3 +- .../tests/unit/get-return-to-path.test.ts | 1 + apps/studio/tests/vitestSetup.ts | 4 +- apps/www/lib/mdx/mdxComponents.tsx | 2 +- apps/www/package.json | 10 ++- .../pages/partners/integrations/[slug].tsx | 9 +- package-lock.json | 87 ++++++++++++++++--- packages/ai-commands/package.json | 2 +- packages/common/package.json | 3 +- packages/icons/package.json | 8 +- packages/pg-meta/package.json | 2 +- packages/ui-patterns/TweetCard/index.tsx | 3 - packages/ui-patterns/admonition.tsx | 29 +++---- .../ui-patterns/form/Layout/FormLayout.tsx | 2 +- packages/ui-patterns/package.json | 13 ++- packages/ui/index.tsx | 1 - packages/ui/jest.config.ts | 15 ---- packages/ui/jest.setup.ts | 1 - packages/ui/package.json | 11 ++- .../ui/src/components/Alert/Alert.test.tsx | 2 + ...Breadcrumb.test.js => Breadcrumb.test.jsx} | 8 +- .../ui/src/components/Button/Button.test.tsx | 2 + .../{Checkbox.test.js => Checkbox.test.jsx} | 4 +- .../CustomHTMLElements.utils.test.tsx | 2 + packages/ui/src/components/Icon/Icon.test.js | 26 ------ .../ui/src/components/InputOld/Input.test.js | 20 ----- .../ui/src/components/InputOld/Input.test.jsx | 19 ++++ ...ctStyled.test.js => SelectStyled.test.jsx} | 10 ++- .../src/components/NavMenu/nav-menu.test.tsx | 7 +- .../ui/src/components/RadioOld/Radio.test.js | 19 ---- .../ui/src/components/RadioOld/Radio.test.jsx | 19 ++++ .../{Select.test.js => Select.test.jsx} | 6 +- .../SimpleCodeBlock/SimpleCodeBlock.tsx | 8 +- packages/ui/src/components/StatusIcon.tsx | 4 +- .../ui/src/components/shadcn/ui/dialog.tsx | 10 ++- .../components/shadcn/ui/dropdown-menu.tsx | 6 ++ .../ui/src/components/shadcn/ui/input-otp.tsx | 6 +- packages/ui/tsconfig.json | 2 +- packages/ui/vitest.config.ts | 10 +++ packages/ui/vitest.setup.ts | 27 ++++++ tests/package.json | 2 +- 91 files changed, 394 insertions(+), 254 deletions(-) create mode 100644 apps/docs/types/mdx.d.ts delete mode 100644 packages/ui/jest.config.ts delete mode 100644 packages/ui/jest.setup.ts rename packages/ui/src/components/Breadcrumb/{Breadcrumb.test.js => Breadcrumb.test.jsx} (74%) rename packages/ui/src/components/Checkbox/{Checkbox.test.js => Checkbox.test.jsx} (91%) delete mode 100644 packages/ui/src/components/Icon/Icon.test.js delete mode 100644 packages/ui/src/components/InputOld/Input.test.js create mode 100644 packages/ui/src/components/InputOld/Input.test.jsx rename packages/ui/src/components/Listbox/{SelectStyled.test.js => SelectStyled.test.jsx} (74%) delete mode 100644 packages/ui/src/components/RadioOld/Radio.test.js create mode 100644 packages/ui/src/components/RadioOld/Radio.test.jsx rename packages/ui/src/components/Select/{Select.test.js => Select.test.jsx} (58%) create mode 100644 packages/ui/vitest.config.ts create mode 100644 packages/ui/vitest.setup.ts diff --git a/apps/design-system/components/command-menu.tsx b/apps/design-system/components/command-menu.tsx index a770e33d315..1673e8f2ed0 100644 --- a/apps/design-system/components/command-menu.tsx +++ b/apps/design-system/components/command-menu.tsx @@ -1,15 +1,14 @@ 'use client' -import * as React from 'react' -import { useRouter } from 'next/navigation' -import { DialogProps } from '@radix-ui/react-alert-dialog' import { CircleIcon, LaptopIcon, MoonIcon, SunIcon } from 'lucide-react' import { useTheme } from 'next-themes' +import { useRouter } from 'next/navigation' +import * as React from 'react' import { docsConfig } from '@/config/docs' import { cn } from '@/lib/utils' -import { Button } from 'ui' import { + Button, CommandDialog, CommandEmpty_Shadcn_, CommandGroup_Shadcn_, @@ -17,6 +16,7 @@ import { CommandItem_Shadcn_, CommandList_Shadcn_, CommandSeparator_Shadcn_, + DialogProps, } from 'ui' export function CommandMenu({ ...props }: DialogProps) { diff --git a/apps/design-system/components/copy-button.tsx b/apps/design-system/components/copy-button.tsx index ed823d518c6..f8b5f76020d 100644 --- a/apps/design-system/components/copy-button.tsx +++ b/apps/design-system/components/copy-button.tsx @@ -1,15 +1,20 @@ 'use client' import * as React from 'react' -import { DropdownMenuTriggerProps } from '@radix-ui/react-dropdown-menu' // import { NpmCommands } from 'types/unist' // import { Event, trackEvent } from '@/lib/events' -import { cn } from 'ui' -import { Button } from 'ui' -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from 'ui' import { Check, Copy } from 'lucide-react' +import { + Button, + cn, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuTriggerProps, +} from 'ui' interface CopyButtonProps extends React.HTMLAttributes { value: string diff --git a/apps/design-system/lib/utils.ts b/apps/design-system/lib/utils.ts index 8be9377c7ea..74c32aaec28 100644 --- a/apps/design-system/lib/utils.ts +++ b/apps/design-system/lib/utils.ts @@ -1,10 +1,6 @@ -import * as React from 'react' -import { clsx, type ClassValue } from 'clsx' -import { twMerge } from 'tailwind-merge' +import { cn as uiCN } from 'ui' -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} +export const cn = uiCN export function formatDate(input: string | number): string { const date = new Date(input) diff --git a/apps/design-system/package.json b/apps/design-system/package.json index 4fed6e0f0ef..156079b3a62 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -13,6 +13,7 @@ "typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json" }, "dependencies": { + "@hookform/resolvers": "^3.1.1", "@radix-ui/react-alert-dialog": "^1.0.5", "contentlayer2": "0.4.6", "eslint-config-supabase": "*", @@ -23,20 +24,26 @@ "markdown-wasm": "^1.2.0", "next": "^14.2.10", "next-contentlayer2": "0.4.6", + "next-themes": "^0.3.0", "react": "^18.2.0", "react-docgen": "^7.0.3", "react-dom": "^18.2.0", + "react-hook-form": "^7.45.0", "react-inlinesvg": "^4.0.4", "react-wrap-balancer": "^1.1.0", + "recharts": "^2.12.7", "rehype-autolink-headings": "^7.1.0", "rehype-pretty-code": "^0.6.0", "rehype-slug": "^6.0.0", "remark": "^14.0.3", "remark-code-import": "^1.2.0", "remark-gfm": "^4.0.0", + "sonner": "^1.5.0", "ts-morph": "^22.0.0", "ui": "*", - "unist-util-visit": "^5.0.0" + "ui-patterns": "*", + "unist-util-visit": "^5.0.0", + "zod": "^3.22.4" }, "devDependencies": { "@shikijs/compat": "^1.1.7", @@ -52,7 +59,7 @@ "shiki": "^1.1.7", "tailwindcss": "^3.3.0", "tsconfig": "*", - "typescript": "^5.5.0", + "typescript": "~5.5.0", "unist-builder": "3.0.0" } } diff --git a/apps/design-system/registry/default/example/dropdown-menu-checkboxes.tsx b/apps/design-system/registry/default/example/dropdown-menu-checkboxes.tsx index 048d60d750d..788b3de460b 100644 --- a/apps/design-system/registry/default/example/dropdown-menu-checkboxes.tsx +++ b/apps/design-system/registry/default/example/dropdown-menu-checkboxes.tsx @@ -1,12 +1,12 @@ 'use client' import * as React from 'react' -import { DropdownMenuCheckboxItemProps } from '@radix-ui/react-dropdown-menu' -import { Button } from 'ui' import { + Button, DropdownMenu, DropdownMenuCheckboxItem, + DropdownMenuCheckboxItemProps, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, diff --git a/apps/design-system/registry/default/example/input-otp-pattern.tsx b/apps/design-system/registry/default/example/input-otp-pattern.tsx index ce136748fc7..2d605de5181 100644 --- a/apps/design-system/registry/default/example/input-otp-pattern.tsx +++ b/apps/design-system/registry/default/example/input-otp-pattern.tsx @@ -1,6 +1,4 @@ -import { REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp' - -import { InputOTP, InputOTPGroup, InputOTPSlot } from 'ui' +import { InputOTP, InputOTPGroup, InputOTPSlot, REGEXP_ONLY_DIGITS_AND_CHARS } from 'ui' export default function InputOTPPattern() { return ( diff --git a/apps/design-system/registry/default/example/tree-view-demo.tsx b/apps/design-system/registry/default/example/tree-view-demo.tsx index 1f9c3094695..bdf8cfc2708 100644 --- a/apps/design-system/registry/default/example/tree-view-demo.tsx +++ b/apps/design-system/registry/default/example/tree-view-demo.tsx @@ -1,5 +1,4 @@ -import { TreeView, TreeViewItem } from 'ui' -import { flattenTree } from 'react-accessible-treeview' +import { TreeView, TreeViewItem, flattenTree } from 'ui' export default function TreeViewDemo() { const data = { diff --git a/apps/design-system/registry/default/example/tree-view-directories.tsx b/apps/design-system/registry/default/example/tree-view-directories.tsx index 78071a743f3..5cce4213c75 100644 --- a/apps/design-system/registry/default/example/tree-view-directories.tsx +++ b/apps/design-system/registry/default/example/tree-view-directories.tsx @@ -1,5 +1,4 @@ -import { TreeView, TreeViewItem } from 'ui' -import { flattenTree } from 'react-accessible-treeview' +import { TreeView, TreeViewItem, flattenTree } from 'ui' export default function TreeViewDemo() { const data = { diff --git a/apps/design-system/registry/default/example/tree-view-edit.tsx b/apps/design-system/registry/default/example/tree-view-edit.tsx index 873583e8896..311cee5b0a3 100644 --- a/apps/design-system/registry/default/example/tree-view-edit.tsx +++ b/apps/design-system/registry/default/example/tree-view-edit.tsx @@ -1,12 +1,13 @@ import { useState } from 'react' -import { TreeView, TreeViewItem } from 'ui' -import { flattenTree } from 'react-accessible-treeview' import { ContextMenu_Shadcn_, ContextMenuContent_Shadcn_, ContextMenuItem_Shadcn_, ContextMenuSeparator_Shadcn_, ContextMenuTrigger_Shadcn_, + flattenTree, + TreeView, + TreeViewItem, } from 'ui' const data = { diff --git a/apps/design-system/registry/default/example/tree-view-multi-select.tsx b/apps/design-system/registry/default/example/tree-view-multi-select.tsx index 2a6665302d2..2487c8eecd1 100644 --- a/apps/design-system/registry/default/example/tree-view-multi-select.tsx +++ b/apps/design-system/registry/default/example/tree-view-multi-select.tsx @@ -1,12 +1,13 @@ import { useState } from 'react' -import { TreeView, TreeViewItem } from 'ui' -import { flattenTree } from 'react-accessible-treeview' import { ContextMenu_Shadcn_, ContextMenuContent_Shadcn_, ContextMenuItem_Shadcn_, ContextMenuSeparator_Shadcn_, ContextMenuTrigger_Shadcn_, + flattenTree, + TreeView, + TreeViewItem, } from 'ui' const data = { diff --git a/apps/docs/components/index.tsx b/apps/docs/components/index.tsx index f85151396d3..990db346ea6 100644 --- a/apps/docs/components/index.tsx +++ b/apps/docs/components/index.tsx @@ -4,7 +4,8 @@ // Basic UI things import Link from 'next/link' -import { Accordion, Admonition, Alert, Button, CodeBlock, Image, markdownComponents } from 'ui' +import { Accordion, Alert, Button, CodeBlock, Image, markdownComponents } from 'ui' +import { Admonition } from 'ui-patterns/admonition' import { GlassPanel } from 'ui-patterns/GlassPanel' import { IconPanel } from 'ui-patterns/IconPanel' import { TabPanel, Tabs } from '~/features/ui/Tabs' diff --git a/apps/docs/features/docs/MdxBase.shared.tsx b/apps/docs/features/docs/MdxBase.shared.tsx index e3f189e2718..3458d36ead9 100644 --- a/apps/docs/features/docs/MdxBase.shared.tsx +++ b/apps/docs/features/docs/MdxBase.shared.tsx @@ -1,9 +1,10 @@ import { ArrowDown, Check, X } from 'lucide-react' import Link from 'next/link' +import { Button, Image } from 'ui' +import { Admonition } from 'ui-patterns/admonition' import { GlassPanel } from 'ui-patterns/GlassPanel' import { IconPanel } from 'ui-patterns/IconPanel' import SqlToRest from 'ui-patterns/SqlToRest' -import { Admonition, Button, Image } from 'ui' import { Heading } from 'ui/src/components/CustomHTMLElements' import { AppleSecretGenerator } from '~/components/AppleSecretGenerator' import AuthProviders from '~/components/AuthProviders' @@ -21,8 +22,8 @@ import { HuggingFaceDeployment, KotlinProjectSetup, MigrationWarnings, - ProjectSetup, OAuthPkceFlow, + ProjectSetup, QuickstartDbSetup, QuickstartIntro, SocialProviderSettingsSupabase, @@ -37,7 +38,7 @@ import StepHikeCompact from '~/components/StepHikeCompact' import { CodeSampleWrapper } from '~/features/directives/CodeSample.client' import { Accordion, AccordionItem } from '~/features/ui/Accordion' import * as CH from '~/features/ui/CodeHike' -import { Tabs, TabPanel } from '~/features/ui/Tabs' +import { TabPanel, Tabs } from '~/features/ui/Tabs' const components = { Accordion, diff --git a/apps/docs/features/docs/Reference.mdx.client.tsx b/apps/docs/features/docs/Reference.mdx.client.tsx index c6bab0d9328..66ff1c58c12 100644 --- a/apps/docs/features/docs/Reference.mdx.client.tsx +++ b/apps/docs/features/docs/Reference.mdx.client.tsx @@ -11,7 +11,7 @@ import { MDXProvider } from '@mdx-js/react' import { type PropsWithChildren } from 'react' -import { Admonition } from 'ui' +import { Admonition } from 'ui-patterns/admonition' const components = { Admonition } diff --git a/apps/docs/lib/mdx/plugins/remarkAdmonition.ts b/apps/docs/lib/mdx/plugins/remarkAdmonition.ts index 13d076be064..9dce6c10d27 100644 --- a/apps/docs/lib/mdx/plugins/remarkAdmonition.ts +++ b/apps/docs/lib/mdx/plugins/remarkAdmonition.ts @@ -1,6 +1,6 @@ import type { Content, Paragraph, Root } from 'mdast' import type { MdxJsxFlowElement } from 'mdast-util-mdx' -import type { AdmonitionProps } from 'ui' +import type { AdmonitionProps } from 'ui-patterns/admonition' import type { Node } from 'unist' import { visit } from 'unist-util-visit' diff --git a/apps/docs/next-env.d.ts b/apps/docs/next-env.d.ts index 4f11a03dc6c..fd36f9494e2 100644 --- a/apps/docs/next-env.d.ts +++ b/apps/docs/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/docs/package.json b/apps/docs/package.json index 6b5b0285ba9..ae25f1419ea 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -36,12 +36,14 @@ "@octokit/graphql": "^7.0.2", "@octokit/plugin-paginate-graphql": "^4.0.0", "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-collapsible": "^1.0.3", "@supabase/auth-helpers-react": "^0.4.2", "@supabase/supabase-js": "^2.44.3", "@tailwindcss/container-queries": "^0.1.1", "@tanstack/react-query": "^5.13.4", "acorn": "^8.11.3", "acorn-typescript": "^1.4.13", + "ai-commands": "*", "common": "*", "common-tags": "^1.8.2", "config": "*", @@ -50,8 +52,10 @@ "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "hast-util-has-property": "^2.0.1", + "hast-util-to-html": "^9.0.0", "icons": "*", "isbot": "^5.1.2", + "js-yaml": "^3.14.1", "jsrsasign": "^11.0.0", "katex": "^0.16.10", "lodash": "^4.17.21", @@ -61,6 +65,8 @@ "mdast-util-from-markdown": "^1.2.0", "mdast-util-gfm": "^2.0.2", "mdast-util-mdx": "^2.0.0", + "mdast-util-mdx-jsx": "^2.0.0", + "mdast-util-to-hast": "^12.3.0", "mdast-util-to-markdown": "^1.5.0", "mdast-util-to-string": "^3.1.1", "micromark-extension-gfm": "^2.0.3", @@ -73,10 +79,12 @@ "openai": "^4.20.1", "openapi-fetch": "^0.8.2", "react": "^18.2.0", + "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.12", "react-intersection-observer": "^9.5.3", "react-markdown": "^8.0.3", + "react-use": "^17.5.0", "rehype-katex": "^7.0.0", "rehype-slug": "^5.1.0", "remark": "^14.0.2", @@ -91,16 +99,21 @@ "unist-builder": "^3.0.1", "unist-util-filter": "^4.0.1", "unist-util-visit": "^4.1.2", + "unist-util-visit-parents": "^5.0.0", "uuid": "^9.0.1", "valtio": "^1.12.0", - "yaml": "^2.4.5" + "yaml": "^2.4.5", + "zod": "^3.22.4" }, "devDependencies": { "@aws-sdk/client-secrets-manager": "^3.410.0", + "@types/estree": "1.0.5", "@types/hast": "^2.3.4", "@types/jsrsasign": "^10.5.12", + "@types/mdast": "^3.0.0", "@types/node": "^20.11.16", "@types/react": "^18.2.24", + "@types/react-copy-to-clipboard": "^5.0.4", "@types/unist": "^2.0.6", "api-types": "*", "cheerio": "^1.0.0-rc.12", @@ -115,7 +128,7 @@ "slugify": "^1.6.6", "tsconfig": "*", "tsx": "^4.6.2", - "typescript": "^5.5.0", + "typescript": "~5.5.0", "vite-tsconfig-paths": "^4.3.2", "vitest": "^2.0.2" } diff --git a/apps/docs/types/mdx.d.ts b/apps/docs/types/mdx.d.ts new file mode 100644 index 00000000000..a9831edba2e --- /dev/null +++ b/apps/docs/types/mdx.d.ts @@ -0,0 +1,5 @@ +// types/mdx.d.ts +declare module '*.mdx' { + let MDXComponent: (props) => JSX.Element + export default MDXComponent +} diff --git a/apps/studio/components/grid/components/footer/pagination/Pagination.utils.test.ts b/apps/studio/components/grid/components/footer/pagination/Pagination.utils.test.ts index dacae3c49be..837944df97d 100644 --- a/apps/studio/components/grid/components/footer/pagination/Pagination.utils.test.ts +++ b/apps/studio/components/grid/components/footer/pagination/Pagination.utils.test.ts @@ -1,3 +1,4 @@ +import { describe, test, expect } from 'vitest' import { formatEstimatedCount } from './Pagination.utils' describe('formatEstimatedCount', () => { diff --git a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx index 6a1119b13c4..ef4f296b09f 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx @@ -18,7 +18,8 @@ import { User } from 'data/auth/users-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { BASE_PATH } from 'lib/constants' import { timeout } from 'lib/helpers' -import { Admonition, Button, cn, Separator } from 'ui' +import { Button, cn, Separator } from 'ui' +import { Admonition } from 'ui-patterns/admonition' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { PROVIDERS_SCHEMAS } from '../AuthProvidersFormValidation' import { PANEL_PADDING } from './UserPanel' diff --git a/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseCollection.tsx b/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseCollection.tsx index ff5052996e0..08e637a2dce 100644 --- a/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseCollection.tsx +++ b/apps/studio/components/interfaces/DataWarehouse/CreateWarehouseCollection.tsx @@ -1,33 +1,25 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Info, PlusIcon } from 'lucide-react' import { useRouter } from 'next/router' import { useEffect } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' -import { FormMessage } from '@ui/components/shadcn/ui/form' -import { Input } from '@ui/components/shadcn/ui/input' import { useParams } from 'common' import { useCreateCollection } from 'data/analytics/warehouse-collections-create-mutation' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { - Admonition, - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, Button, FormControl_Shadcn_, FormField_Shadcn_, + FormMessage_Shadcn_, Form_Shadcn_, + Input_Shadcn_, Modal, - WarningIcon, } from 'ui' +import { Admonition } from 'ui-patterns/admonition' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan' -import { Alert } from '@ui/components/shadcn/ui/alert' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' export const CreateWarehouseCollectionModal = ({ open, @@ -133,7 +125,7 @@ export const CreateWarehouseCollectionModal = ({ render={({ field }) => ( - + )} @@ -149,7 +141,7 @@ export const CreateWarehouseCollectionModal = ({ >
- + days @@ -159,7 +151,7 @@ export const CreateWarehouseCollectionModal = ({ )} />
- + {plan?.id === 'free' && ( diff --git a/apps/studio/components/interfaces/DiskManagement/DiskManagement.test.ts b/apps/studio/components/interfaces/DiskManagement/DiskManagement.test.ts index 3de7b4d6c47..c89eaeb2be7 100644 --- a/apps/studio/components/interfaces/DiskManagement/DiskManagement.test.ts +++ b/apps/studio/components/interfaces/DiskManagement/DiskManagement.test.ts @@ -1,9 +1,10 @@ +import { describe, test, expect } from 'vitest' +import { DiskType } from './DiskManagement.constants' import { calculateDiskSizePrice, calculateIOPSPrice, calculateThroughputPrice, } from './DiskManagement.utils' -import { DiskType } from './DiskManagement.constants' describe('DiskManagement.utils.ts:calculateDiskSizePrice', () => { test('GP3 with 8GB to GP3 with 10GB for pro plan', () => { diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.test.ts b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.test.ts index dfa84b24f85..92d16d03733 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.test.ts +++ b/apps/studio/components/interfaces/Organization/TeamSettings/UpdateRolesPanel/UpdateRolesPanel.utils.test.ts @@ -1,3 +1,4 @@ +import { describe, test, expect } from 'vitest' import { deriveRoleChangeActions } from './UpdateRolesPanel.utils' // [Joshen] This is specifically testing the project scope changes diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts index 16a7f7e90a3..bea8f7a8f91 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts @@ -5,6 +5,7 @@ import { isUpdateWithoutWhere, suffixWithLimit, } from './SQLEditor.utils' +import { describe, test, expect, it } from 'vitest' describe('SQLEditor.utils.ts:checkIfAppendLimitRequired', () => { test('Should return false if limit passed is <= 0', () => { diff --git a/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx b/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx index ecea02f900d..b19b39ffae4 100644 --- a/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx +++ b/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx @@ -1,6 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { indexOf } from 'lodash' +import { ExternalLink, Lock } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' @@ -21,9 +22,7 @@ import { useProjectPostgrestConfigUpdateMutation } from 'data/config/project-pos import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSchemasQuery } from 'data/database/schemas-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { ExternalLink, Lock } from 'lucide-react' import { - Admonition, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, @@ -40,6 +39,7 @@ import { Switch, WarningIcon, } from 'ui' +import { Admonition } from 'ui-patterns/admonition' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { MultiSelector, diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx index a1c54c6259a..e573716ba1c 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx @@ -33,8 +33,9 @@ import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS } from 'lib/constants' import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants/metrics' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' +import { Button } from 'ui' +import { Admonition } from 'ui-patterns/admonition' import { INFRA_ACTIVITY_METRICS } from './Infrastructure.constants' -import { Admonition, Button } from 'ui' const NON_DEDICATED_IO_RESOURCES = [ 'ci_micro', diff --git a/apps/studio/components/interfaces/TableGridEditor/TableEntity.utils.test.ts b/apps/studio/components/interfaces/TableGridEditor/TableEntity.utils.test.ts index e1869ef36e2..ce126495d54 100644 --- a/apps/studio/components/interfaces/TableGridEditor/TableEntity.utils.test.ts +++ b/apps/studio/components/interfaces/TableGridEditor/TableEntity.utils.test.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest' import { formatTableRowsToSQL } from './TableEntity.utils' describe('TableEntity.utils: formatTableRowsToSQL', () => { diff --git a/apps/studio/package.json b/apps/studio/package.json index c569dc4a6c6..90c7af5eb7d 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -28,6 +28,7 @@ "@hcaptcha/react-hcaptcha": "^1.8.1", "@headlessui/react": "^1.7.17", "@heroicons/react": "^2.1.3", + "@hookform/resolvers": "^3.1.1", "@monaco-editor/react": "^4.6.0", "@next/bundle-analyzer": "^14.2.3", "@radix-ui/react-popover": "^1.0.7", @@ -36,7 +37,9 @@ "@stripe/react-stripe-js": "^2.5.0", "@stripe/stripe-js": "^3.0.5", "@supabase/auth-helpers-react": "^0.4.2", + "@supabase/auth-js": "2.64.2", "@supabase/pg-meta": "*", + "@supabase/realtime-js": "2.10.2", "@supabase/shared-types": "0.1.70", "@supabase/supabase-js": "^2.44.3", "@tanstack/react-query": "4.35.7", @@ -53,6 +56,7 @@ "config": "*", "configcat-js": "^7.0.0", "dayjs": "^1.11.10", + "dnd-core": "^16.0.1", "file-saver": "^2.0.5", "framer-motion": "^11.0.3", "generate-password-browser": "^1.1.0", @@ -64,6 +68,7 @@ "lodash": "^4.17.21", "lucide-react": "^0.436.0", "markdown-table": "^3.0.3", + "memoize-one": "^5.0.1", "mime-db": "^1.53.0", "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", @@ -81,6 +86,7 @@ "react": "^18.2.0", "react-beautiful-dnd": "^13.1.0", "react-contexify": "^5.0.0", + "react-copy-to-clipboard": "^5.1.0", "react-csv": "^2.0.3", "react-data-grid": "7.0.0-beta.41", "react-datepicker": "^4.18.0", @@ -89,18 +95,22 @@ "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", "react-grid-layout": "^1.4.2", + "react-hook-form": "^7.45.0", "react-inlinesvg": "^4.0.4", "react-intersection-observer": "^9.5.3", "react-markdown": "^8.0.3", "react-simple-maps": "4.0.0-beta.6", "react-tracked": "^1.7.11", + "react-use": "^17.5.0", "react-virtualized-auto-sizer": "^1.0.20", "react-window": "^1.8.6", "react-window-infinite-loader": "^1.0.7", "reactflow": "^11.10.1", "recharts": "^2.8.0", + "remark-gfm": "^3.0.1", "semver": "^6.3.1", "shared-data": "*", + "sonner": "^1.5.0", "sql-formatter": "^13.1.0", "sqlstring": "^2.3.2", "tus-js-client": "^4.1.0", @@ -112,6 +122,7 @@ "vitest": "^1.6.0", "yup": "^1.4.0", "yup-password": "^0.3.0", + "zod": "^3.22.4", "zxcvbn": "^4.4.2" }, "devDependencies": { @@ -134,6 +145,7 @@ "@types/randombytes": "^2.0.3", "@types/react": "^18.2.24", "@types/react-beautiful-dnd": "^13.1.2", + "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-csv": "^1.1.2", "@types/react-datepicker": "^4.3.4", "@types/react-dom": "^18.2.8", @@ -160,6 +172,6 @@ "prettier": "^4.0.0-alpha.8", "storybook-dark-mode": "^3.0.1", "tailwindcss": "^3.4.1", - "typescript": "^5.5.0" + "typescript": "~5.5.0" } } diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index f9f8d5fec69..434d49e5bd4 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -49,7 +49,6 @@ import passwordStrength from 'lib/password-strength' import type { CloudProvider } from 'shared-data' import type { NextPageWithLayout } from 'types' import { - Admonition, Badge, Button, CollapsibleContent_Shadcn_, @@ -76,6 +75,7 @@ import { TableRow, cn, } from 'ui' +import { Admonition } from 'ui-patterns/admonition' import { Input } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { InfoTooltip } from 'ui-patterns/info-tooltip' diff --git a/apps/studio/tests/components/Billing/TaxID.utils.test.ts b/apps/studio/tests/components/Billing/TaxID.utils.test.ts index f5cec98e7f8..68908eddbf1 100644 --- a/apps/studio/tests/components/Billing/TaxID.utils.test.ts +++ b/apps/studio/tests/components/Billing/TaxID.utils.test.ts @@ -1,4 +1,5 @@ import { sanitizeTaxIdValue } from 'components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils' +import { describe, test, expect } from 'vitest' /** * We're sanitizing EU tax ids. Stripe expects a prefixed tax id (ATU12345678), diff --git a/apps/studio/tests/components/CopyButton.test.tsx b/apps/studio/tests/components/CopyButton.test.tsx index df237596201..d7773d740a1 100644 --- a/apps/studio/tests/components/CopyButton.test.tsx +++ b/apps/studio/tests/components/CopyButton.test.tsx @@ -1,4 +1,4 @@ -import { vi } from 'vitest' +import { expect, test, vi } from 'vitest' import { screen } from '@testing-library/dom' import userEvent from '@testing-library/user-event' diff --git a/apps/studio/tests/components/Editor/RemoveJSONTrailingComma.utils.test.ts b/apps/studio/tests/components/Editor/RemoveJSONTrailingComma.utils.test.ts index 3dfe29b4d09..86b416e4f82 100644 --- a/apps/studio/tests/components/Editor/RemoveJSONTrailingComma.utils.test.ts +++ b/apps/studio/tests/components/Editor/RemoveJSONTrailingComma.utils.test.ts @@ -1,4 +1,5 @@ import { removeJSONTrailingComma } from 'lib/helpers' +import { describe, it, expect } from 'vitest' describe('removeJSONTrailingComma', () => { it('should handle an empty object', () => { diff --git a/apps/studio/tests/components/Editor/RowEditor.utils.test.ts b/apps/studio/tests/components/Editor/RowEditor.utils.test.ts index e4b550bb97c..280c0e02345 100644 --- a/apps/studio/tests/components/Editor/RowEditor.utils.test.ts +++ b/apps/studio/tests/components/Editor/RowEditor.utils.test.ts @@ -1,9 +1,9 @@ -import { vi } from 'vitest' +import { RowField } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types' import { generateRowObjectFromFields, parseValue, } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils' -import { RowField } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.types' +import { describe, expect, it, vi } from 'vitest' describe('parseValue', () => { it('should return null when originalValue is null', () => { diff --git a/apps/studio/tests/components/Editor/SpreadsheetImport.utils.test.ts b/apps/studio/tests/components/Editor/SpreadsheetImport.utils.test.ts index 40820ff298e..0660e7dfc82 100644 --- a/apps/studio/tests/components/Editor/SpreadsheetImport.utils.test.ts +++ b/apps/studio/tests/components/Editor/SpreadsheetImport.utils.test.ts @@ -1,4 +1,5 @@ import { inferColumnType } from 'components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils' +import { describe, test, expect } from 'vitest' describe('SpreadsheedImport.utils: inferColumnType', () => { test('should default column type to text if no rows to infer from', () => { diff --git a/apps/studio/tests/components/Grid/Grid.utils.test.ts b/apps/studio/tests/components/Grid/Grid.utils.test.ts index a12c9874a3e..b7456b0a8a9 100644 --- a/apps/studio/tests/components/Grid/Grid.utils.test.ts +++ b/apps/studio/tests/components/Grid/Grid.utils.test.ts @@ -1,4 +1,5 @@ import { formatFilterURLParams, formatSortURLParams } from 'components/grid/SupabaseGrid.utils' +import { describe, test, expect } from 'vitest' // Sort URL syntax: `column:order` describe('SupabaseGrid.utils: formatSortURLParams', () => { diff --git a/apps/studio/tests/components/ProjectDropdown.test.ts b/apps/studio/tests/components/ProjectDropdown.test.ts index 9b1bc77d991..66156018469 100644 --- a/apps/studio/tests/components/ProjectDropdown.test.ts +++ b/apps/studio/tests/components/ProjectDropdown.test.ts @@ -1,4 +1,5 @@ import { sanitizeRoute } from 'components/layouts/AppLayout/ProjectDropdown' +import { test, expect } from 'vitest' test('Should sanitize project routes correctly when switching projects by removing project specific parameters', () => { expect(sanitizeRoute('/project/[ref]', { ref: 'abc' })).toBe('/project/[ref]') diff --git a/apps/studio/tests/components/Reports/ReportWidget.test.tsx b/apps/studio/tests/components/Reports/ReportWidget.test.tsx index 17d73f5379b..e65533ae4a2 100644 --- a/apps/studio/tests/components/Reports/ReportWidget.test.tsx +++ b/apps/studio/tests/components/Reports/ReportWidget.test.tsx @@ -1,5 +1,6 @@ import { screen } from '@testing-library/react' import ReportWidget from 'components/interfaces/Reports/ReportWidget' +import { test } from 'vitest' import { render } from '../../helpers' test('static elements', async () => { diff --git a/apps/studio/tests/components/Storage/StorageSettings.utils.test.ts b/apps/studio/tests/components/Storage/StorageSettings.utils.test.ts index a24b85547d8..cdfc5f97e06 100644 --- a/apps/studio/tests/components/Storage/StorageSettings.utils.test.ts +++ b/apps/studio/tests/components/Storage/StorageSettings.utils.test.ts @@ -3,6 +3,7 @@ import { convertFromBytes, convertToBytes, } from 'components/to-be-cleaned/Storage/StorageSettings/StorageSettings.utils' +import { describe, test, expect } from 'vitest' describe('StorageSettings.utils: convertFromBytes', () => { test('should convert 1024 to 1KB', () => { diff --git a/apps/studio/tests/components/ui/Charts/Charts.utils.test.ts b/apps/studio/tests/components/ui/Charts/Charts.utils.test.ts index 1d7d4b2906f..f0f246586bb 100644 --- a/apps/studio/tests/components/ui/Charts/Charts.utils.test.ts +++ b/apps/studio/tests/components/ui/Charts/Charts.utils.test.ts @@ -1,10 +1,11 @@ +import { renderHook } from '@testing-library/react' import { isFloat, numberFormatter, precisionFormatter, useStacked, } from 'components/ui/Charts/Charts.utils' -import { renderHook } from '@testing-library/react' +import { test, expect } from 'vitest' test('isFloat', () => { expect(isFloat(123)).toBe(false) diff --git a/apps/studio/tests/config/msw.test.ts b/apps/studio/tests/config/msw.test.ts index 8475f3b2024..10c1224d3d3 100644 --- a/apps/studio/tests/config/msw.test.ts +++ b/apps/studio/tests/config/msw.test.ts @@ -1,4 +1,5 @@ import { API_URL } from 'lib/constants' +import { test, expect } from 'vitest' test('MSW works as expected', async () => { const res = await fetch(`${API_URL}/msw/test`) diff --git a/apps/studio/tests/config/router.test.tsx b/apps/studio/tests/config/router.test.tsx index 92f107b6d89..032968f7e76 100644 --- a/apps/studio/tests/config/router.test.tsx +++ b/apps/studio/tests/config/router.test.tsx @@ -1,7 +1,7 @@ import { render, screen, waitFor } from '@testing-library/react' -import { RouterComponent } from './router' -import { expect, suite, vi } from 'vitest' import { routerMock } from 'tests/mocks/router' +import { expect, suite, test } from 'vitest' +import { RouterComponent } from './router' suite('Router Mock', () => { test('Router mock works as expected', async () => { diff --git a/apps/studio/tests/pages/projects/LogEventChart.test.tsx b/apps/studio/tests/pages/projects/LogEventChart.test.tsx index bc488e2c289..32a81a4b82e 100644 --- a/apps/studio/tests/pages/projects/LogEventChart.test.tsx +++ b/apps/studio/tests/pages/projects/LogEventChart.test.tsx @@ -1,7 +1,7 @@ -import { vi } from 'vitest' +import { afterEach, beforeEach, test, vi } from 'vitest' -import LogEventChart from 'components/interfaces/Settings/Logs/LogEventChart' import { screen } from '@testing-library/react' +import LogEventChart from 'components/interfaces/Settings/Logs/LogEventChart' import { render } from '../../helpers' const { ResizeObserver } = window diff --git a/apps/studio/tests/pages/projects/LogTable.test.tsx b/apps/studio/tests/pages/projects/LogTable.test.tsx index 1f152ee3ea9..77708c57666 100644 --- a/apps/studio/tests/pages/projects/LogTable.test.tsx +++ b/apps/studio/tests/pages/projects/LogTable.test.tsx @@ -1,8 +1,8 @@ -import { describe, vi, test } from 'vitest' -import LogTable from 'components/interfaces/Settings/Logs/LogTable' -import { waitFor, screen, prettyDOM } from '@testing-library/react' +import { prettyDOM, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import LogTable from 'components/interfaces/Settings/Logs/LogTable' import dayjs from 'dayjs' +import { beforeAll, expect, test, vi } from 'vitest' import { render } from '../../helpers' import customParseFormat from 'dayjs/plugin/customParseFormat' import utc from 'dayjs/plugin/utc' diff --git a/apps/studio/tests/pages/projects/Logs.Datepickers.test.tsx b/apps/studio/tests/pages/projects/Logs.Datepickers.test.tsx index 7ab04d9a22c..2d80805e933 100644 --- a/apps/studio/tests/pages/projects/Logs.Datepickers.test.tsx +++ b/apps/studio/tests/pages/projects/Logs.Datepickers.test.tsx @@ -5,7 +5,7 @@ import { PREVIEWER_DATEPICKER_HELPERS } from 'components/interfaces/Settings/Log import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' -import { vi } from 'vitest' +import { expect, test, vi } from 'vitest' import { render } from '../../helpers' dayjs.extend(timezone) diff --git a/apps/studio/tests/pages/projects/Logs.utils.test.ts b/apps/studio/tests/pages/projects/Logs.utils.test.ts index 96893d1ed0e..5b60d52258a 100644 --- a/apps/studio/tests/pages/projects/Logs.utils.test.ts +++ b/apps/studio/tests/pages/projects/Logs.utils.test.ts @@ -1,8 +1,9 @@ import { - checkForWithClause, checkForILIKEClause, checkForWildcard, + checkForWithClause, } from 'components/interfaces/Settings/Logs/Logs.utils' +import { describe, test, expect } from 'vitest' describe('checkForWithClause', () => { test('basic queries', () => { diff --git a/apps/studio/tests/pages/projects/LogsPreviewer.test.tsx b/apps/studio/tests/pages/projects/LogsPreviewer.test.tsx index db23de14799..88303b214a2 100644 --- a/apps/studio/tests/pages/projects/LogsPreviewer.test.tsx +++ b/apps/studio/tests/pages/projects/LogsPreviewer.test.tsx @@ -1,18 +1,14 @@ -import { vi } from 'vitest' -import { act, findByRole, findByText, fireEvent, screen, waitFor } from '@testing-library/react' -import { wait } from '@testing-library/user-event/dist/utils' +import { findByText, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' import { useRouter } from 'next/router' +import { expect, test, vi } from 'vitest' dayjs.extend(utc) -import { useParams } from 'common' import { LogsTableName } from 'components/interfaces/Settings/Logs/Logs.constants' import LogsPreviewer from 'components/interfaces/Settings/Logs/LogsPreviewer' -import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { logDataFixture } from '../../fixtures' import { render } from '../../helpers' // [Joshen] There's gotta be a much better way to mock these things so that it applies for ALL tests diff --git a/apps/studio/tests/pages/projects/LogsQueryPanel.test.tsx b/apps/studio/tests/pages/projects/LogsQueryPanel.test.tsx index 60c4e48994d..c6b1138b9bd 100644 --- a/apps/studio/tests/pages/projects/LogsQueryPanel.test.tsx +++ b/apps/studio/tests/pages/projects/LogsQueryPanel.test.tsx @@ -1,8 +1,8 @@ -import { vi } from 'vitest' import { screen } from '@testing-library/react' +import { expect, test, vi } from 'vitest' -import { render } from 'tests/helpers' import LogsQueryPanel from 'components/interfaces/Settings/Logs/LogsQueryPanel' +import { render } from 'tests/helpers' test('run and clear', async () => { const mockRun = vi.fn() diff --git a/apps/studio/tests/pages/projects/PreviewFilterPanel.test.tsx b/apps/studio/tests/pages/projects/PreviewFilterPanel.test.tsx index 11ee6c7f42b..723a08eab8f 100644 --- a/apps/studio/tests/pages/projects/PreviewFilterPanel.test.tsx +++ b/apps/studio/tests/pages/projects/PreviewFilterPanel.test.tsx @@ -1,10 +1,6 @@ -import { vi } from 'vitest' import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' - -import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilterPanel' -import { render } from '../../helpers' -import { clickDropdown } from 'tests/helpers' +import { expect, test, vi } from 'vitest' test.skip('filter input change and submit', async () => { const mockFn = vi.fn() diff --git a/apps/studio/tests/pages/projects/logs-explorer/saved.test.tsx b/apps/studio/tests/pages/projects/logs-explorer/saved.test.tsx index 0e13815d088..33ce28fbc1d 100644 --- a/apps/studio/tests/pages/projects/logs-explorer/saved.test.tsx +++ b/apps/studio/tests/pages/projects/logs-explorer/saved.test.tsx @@ -1,7 +1,5 @@ // mock the fetch function -import { get } from 'lib/common/fetch' -import { LogsSavedPage } from 'pages/project/[ref]/logs/explorer/saved' -import { render } from 'tests/helpers' +import { test } from 'vitest' test('on load, refresh user content', async () => { // get.mockResolvedValue({}) diff --git a/apps/studio/tests/pages/projects/logs-query.test.tsx b/apps/studio/tests/pages/projects/logs-query.test.tsx index 787767d8666..90f3da6c4e8 100644 --- a/apps/studio/tests/pages/projects/logs-query.test.tsx +++ b/apps/studio/tests/pages/projects/logs-query.test.tsx @@ -1,10 +1,10 @@ -import { vi } from 'vitest' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import dayjs from 'dayjs' import { LogsExplorerPage } from 'pages/project/[ref]/logs/explorer/index' import { clickDropdown, render } from 'tests/helpers' import { routerMock } from 'tests/mocks/router' +import { beforeAll, describe, expect, test, vi } from 'vitest' // [Joshen] Am temporarily commenting out the breaking tests due to: // "TypeError: _fetch.get.mockReset is not a function" error from Jest @@ -21,7 +21,7 @@ beforeAll(() => { ...mod, IS_PLATFORM: true, useIsLoggedIn: vi.fn(), - useParams: jest.fn(() => ({ ref: 'projectRef' })), + useParams: vi.fn(() => ({ ref: 'projectRef' })), } }) vi.mock('lib/gotrue', () => ({ diff --git a/apps/studio/tests/pages/projects/reports/api-report.test.tsx b/apps/studio/tests/pages/projects/reports/api-report.test.tsx index 9deb62eb871..cc9feb0a89c 100644 --- a/apps/studio/tests/pages/projects/reports/api-report.test.tsx +++ b/apps/studio/tests/pages/projects/reports/api-report.test.tsx @@ -1,4 +1,5 @@ -import { prettyDOM, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' +import { test } from 'vitest' import { ApiReport } from 'pages/project/[ref]/reports/api-overview' import { render } from '../../../helpers' diff --git a/apps/studio/tests/pages/projects/reports/storage-report.test.tsx b/apps/studio/tests/pages/projects/reports/storage-report.test.tsx index 5fc6d3cd877..792d8d4dc81 100644 --- a/apps/studio/tests/pages/projects/reports/storage-report.test.tsx +++ b/apps/studio/tests/pages/projects/reports/storage-report.test.tsx @@ -1,7 +1,8 @@ import { screen } from '@testing-library/react' +import { test } from 'vitest' -import { render } from '../../../helpers' import { StorageReport } from 'pages/project/[ref]/reports/storage' +import { render } from '../../../helpers' // [Joshen] Mock data for ApiReport is in __mocks__/hooks/useStorageReport // I don't think this is an ideal set up as the mock data is not clear in this file itself diff --git a/apps/studio/tests/unit/get-return-to-path.test.ts b/apps/studio/tests/unit/get-return-to-path.test.ts index f7951444c17..54041463f90 100644 --- a/apps/studio/tests/unit/get-return-to-path.test.ts +++ b/apps/studio/tests/unit/get-return-to-path.test.ts @@ -1,4 +1,5 @@ import { getReturnToPath } from 'lib/gotrue' +import { describe, it, expect } from 'vitest' describe(`getReturnToPath`, () => { it(`returns to /projects when no fallback is provided`, () => { diff --git a/apps/studio/tests/vitestSetup.ts b/apps/studio/tests/vitestSetup.ts index d24e884ec27..704b2e8701a 100644 --- a/apps/studio/tests/vitestSetup.ts +++ b/apps/studio/tests/vitestSetup.ts @@ -1,8 +1,8 @@ -import { beforeAll, vi } from 'vitest' import { setupServer } from 'msw/node' +import { createDynamicRouteParser } from 'next-router-mock/dist/dynamic-routes' +import { afterAll, afterEach, beforeAll, vi } from 'vitest' import { APIMock } from './mocks/api' import { routerMock } from './mocks/router' -import { createDynamicRouteParser } from 'next-router-mock/dist/dynamic-routes' export const mswServer = setupServer(...APIMock) diff --git a/apps/www/lib/mdx/mdxComponents.tsx b/apps/www/lib/mdx/mdxComponents.tsx index 4938bc3064d..2f9c46f5340 100644 --- a/apps/www/lib/mdx/mdxComponents.tsx +++ b/apps/www/lib/mdx/mdxComponents.tsx @@ -3,7 +3,6 @@ import { PropsWithChildren } from 'react' import { CH } from '@code-hike/mdx/components' import { ArrowUpRight, Triangle } from 'lucide-react' import { - Admonition, Badge, cn, Collapsible_Shadcn_, @@ -12,6 +11,7 @@ import { Heading, Image, } from 'ui' +import { Admonition } from 'ui-patterns/admonition' import { type ImageProps } from 'ui/src/components/Image/Image' import Avatar from '~/components/Avatar' import Chart from '~/components/Charts/PGCharts' diff --git a/apps/www/package.json b/apps/www/package.json index d2d0f931a12..6fbfcbd3cd5 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -25,15 +25,19 @@ "@octokit/graphql": "^7.0.2", "@octokit/plugin-paginate-graphql": "^4.0.0", "@octokit/rest": "^20.0.2", + "@radix-ui/react-dialog": "^1.0.5", "@react-three/fiber": "^7.0.29", "@supabase/auth-helpers-react": "^0.4.2", "@supabase/auth-ui-react": "^0.4.5", "@supabase/supabase-js": "^2.44.3", "@vercel/og": "^0.6.2", + "ai-commands": "*", "animejs": "^3.2.2", "classnames": "^2.3.1", + "clsx": "^1.2.1", "cobe": "^0.6.2", "common": "*", + "common-tags": "^1.8.2", "config": "*", "contentlayer2": "0.4.6", "dat.gui": "^0.7.9", @@ -43,6 +47,7 @@ "globby": "^13.2.2", "gray-matter": "^4.0.3", "icons": "*", + "lodash": "^4.0.0", "lucide-react": "*", "markdown-toc": "^1.2.0", "next": "^14.2.10", @@ -50,6 +55,7 @@ "next-mdx-remote": "^4.4.1", "next-seo": "^6.5.0", "next-themes": "^0.3.0", + "openai": "^4.20.1", "parse-numeric-range": "^1.3.0", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", @@ -69,7 +75,7 @@ "swiper": "^11.0.7", "three": "^0.136.0", "typed.js": "^2.0.16", - "typescript": "^5.5.0", + "typescript": "~5.5.0", "ui": "*", "ui-patterns": "*", "use-debounce": "^7.0.1", @@ -79,9 +85,11 @@ "devDependencies": { "@types/animejs": "^3.1.12", "@types/classnames": "^2.3.1", + "@types/common-tags": "^1.8.4", "@types/dat.gui": "^0.7.10", "@types/mdx-js__react": "^1.5.6", "@types/node": "^20.11.16", + "@types/lodash": "^4.0.0", "@types/parse-numeric-range": "^0.0.1", "@types/react": "^18.2.24", "@types/react-copy-to-clipboard": "^5.0.4", diff --git a/apps/www/pages/partners/integrations/[slug].tsx b/apps/www/pages/partners/integrations/[slug].tsx index d4927e5bfe7..a65f36eda86 100644 --- a/apps/www/pages/partners/integrations/[slug].tsx +++ b/apps/www/pages/partners/integrations/[slug].tsx @@ -2,19 +2,20 @@ import { type CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx' import { CH } from '@code-hike/mdx/components' import { ChevronLeft, ExternalLink } from 'lucide-react' import { type GetStaticPaths, type GetStaticProps } from 'next' -import Image from 'next/image' -import Link from 'next/link' import { MDXRemote, type MDXRemoteSerializeResult } from 'next-mdx-remote' import { serialize } from 'next-mdx-remote/serialize' import { NextSeo } from 'next-seo' +import Image from 'next/image' +import Link from 'next/link' import { type Dispatch, type SetStateAction, useState } from 'react' import remarkGfm from 'remark-gfm' -import { Swiper, SwiperSlide } from 'swiper/react' import 'swiper/css' +import { Swiper, SwiperSlide } from 'swiper/react' import { useBreakpoint } from 'common' import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' } -import { Admonition, Button } from 'ui' +import { Button } from 'ui' +import { Admonition } from 'ui-patterns/admonition' import { ExpandableVideo } from 'ui-patterns/ExpandableVideo' import ImageModal from '~/components/ImageModal' diff --git a/package-lock.json b/package-lock.json index a86e3dbf85a..31024fc9581 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "apps/design-system": { "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^3.1.1", "@radix-ui/react-alert-dialog": "^1.0.5", "contentlayer2": "0.4.6", "eslint-config-supabase": "*", @@ -41,20 +42,26 @@ "markdown-wasm": "^1.2.0", "next": "^14.2.10", "next-contentlayer2": "0.4.6", + "next-themes": "^0.3.0", "react": "^18.2.0", "react-docgen": "^7.0.3", "react-dom": "^18.2.0", + "react-hook-form": "^7.45.0", "react-inlinesvg": "^4.0.4", "react-wrap-balancer": "^1.1.0", + "recharts": "^2.12.7", "rehype-autolink-headings": "^7.1.0", "rehype-pretty-code": "^0.6.0", "rehype-slug": "^6.0.0", "remark": "^14.0.3", "remark-code-import": "^1.2.0", "remark-gfm": "^4.0.0", + "sonner": "^1.5.0", "ts-morph": "^22.0.0", "ui": "*", - "unist-util-visit": "^5.0.0" + "ui-patterns": "*", + "unist-util-visit": "^5.0.0", + "zod": "^3.22.4" }, "devDependencies": { "@shikijs/compat": "^1.1.7", @@ -70,7 +77,7 @@ "shiki": "^1.1.7", "tailwindcss": "^3.3.0", "tsconfig": "*", - "typescript": "^5.5.0", + "typescript": "~5.5.0", "unist-builder": "3.0.0" } }, @@ -860,12 +867,14 @@ "@octokit/graphql": "^7.0.2", "@octokit/plugin-paginate-graphql": "^4.0.0", "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-collapsible": "^1.0.3", "@supabase/auth-helpers-react": "^0.4.2", "@supabase/supabase-js": "^2.44.3", "@tailwindcss/container-queries": "^0.1.1", "@tanstack/react-query": "^5.13.4", "acorn": "^8.11.3", "acorn-typescript": "^1.4.13", + "ai-commands": "*", "common": "*", "common-tags": "^1.8.2", "config": "*", @@ -874,8 +883,10 @@ "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "hast-util-has-property": "^2.0.1", + "hast-util-to-html": "^9.0.0", "icons": "*", "isbot": "^5.1.2", + "js-yaml": "^3.14.1", "jsrsasign": "^11.0.0", "katex": "^0.16.10", "lodash": "^4.17.21", @@ -885,6 +896,8 @@ "mdast-util-from-markdown": "^1.2.0", "mdast-util-gfm": "^2.0.2", "mdast-util-mdx": "^2.0.0", + "mdast-util-mdx-jsx": "^2.0.0", + "mdast-util-to-hast": "^12.3.0", "mdast-util-to-markdown": "^1.5.0", "mdast-util-to-string": "^3.1.1", "micromark-extension-gfm": "^2.0.3", @@ -897,10 +910,12 @@ "openai": "^4.20.1", "openapi-fetch": "^0.8.2", "react": "^18.2.0", + "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.12", "react-intersection-observer": "^9.5.3", "react-markdown": "^8.0.3", + "react-use": "^17.5.0", "rehype-katex": "^7.0.0", "rehype-slug": "^5.1.0", "remark": "^14.0.2", @@ -915,16 +930,21 @@ "unist-builder": "^3.0.1", "unist-util-filter": "^4.0.1", "unist-util-visit": "^4.1.2", + "unist-util-visit-parents": "^5.0.0", "uuid": "^9.0.1", "valtio": "^1.12.0", - "yaml": "^2.4.5" + "yaml": "^2.4.5", + "zod": "^3.22.4" }, "devDependencies": { "@aws-sdk/client-secrets-manager": "^3.410.0", + "@types/estree": "1.0.5", "@types/hast": "^2.3.4", "@types/jsrsasign": "^10.5.12", + "@types/mdast": "^3.0.0", "@types/node": "^20.11.16", "@types/react": "^18.2.24", + "@types/react-copy-to-clipboard": "^5.0.4", "@types/unist": "^2.0.6", "api-types": "*", "cheerio": "^1.0.0-rc.12", @@ -939,7 +959,7 @@ "slugify": "^1.6.6", "tsconfig": "*", "tsx": "^4.6.2", - "typescript": "^5.5.0", + "typescript": "~5.5.0", "vite-tsconfig-paths": "^4.3.2", "vitest": "^2.0.2" } @@ -1793,6 +1813,7 @@ "@hcaptcha/react-hcaptcha": "^1.8.1", "@headlessui/react": "^1.7.17", "@heroicons/react": "^2.1.3", + "@hookform/resolvers": "^3.1.1", "@monaco-editor/react": "^4.6.0", "@next/bundle-analyzer": "^14.2.3", "@radix-ui/react-popover": "^1.0.7", @@ -1801,7 +1822,9 @@ "@stripe/react-stripe-js": "^2.5.0", "@stripe/stripe-js": "^3.0.5", "@supabase/auth-helpers-react": "^0.4.2", + "@supabase/auth-js": "2.64.2", "@supabase/pg-meta": "*", + "@supabase/realtime-js": "2.10.2", "@supabase/shared-types": "0.1.70", "@supabase/supabase-js": "^2.44.3", "@tanstack/react-query": "4.35.7", @@ -1818,6 +1841,7 @@ "config": "*", "configcat-js": "^7.0.0", "dayjs": "^1.11.10", + "dnd-core": "^16.0.1", "file-saver": "^2.0.5", "framer-motion": "^11.0.3", "generate-password-browser": "^1.1.0", @@ -1829,6 +1853,7 @@ "lodash": "^4.17.21", "lucide-react": "^0.436.0", "markdown-table": "^3.0.3", + "memoize-one": "^5.0.1", "mime-db": "^1.53.0", "mobx": "^6.10.2", "mobx-react-lite": "^4.0.5", @@ -1846,6 +1871,7 @@ "react": "^18.2.0", "react-beautiful-dnd": "^13.1.0", "react-contexify": "^5.0.0", + "react-copy-to-clipboard": "^5.1.0", "react-csv": "^2.0.3", "react-data-grid": "7.0.0-beta.41", "react-datepicker": "^4.18.0", @@ -1854,18 +1880,22 @@ "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", "react-grid-layout": "^1.4.2", + "react-hook-form": "^7.45.0", "react-inlinesvg": "^4.0.4", "react-intersection-observer": "^9.5.3", "react-markdown": "^8.0.3", "react-simple-maps": "4.0.0-beta.6", "react-tracked": "^1.7.11", + "react-use": "^17.5.0", "react-virtualized-auto-sizer": "^1.0.20", "react-window": "^1.8.6", "react-window-infinite-loader": "^1.0.7", "reactflow": "^11.10.1", "recharts": "^2.8.0", + "remark-gfm": "^3.0.1", "semver": "^6.3.1", "shared-data": "*", + "sonner": "^1.5.0", "sql-formatter": "^13.1.0", "sqlstring": "^2.3.2", "tus-js-client": "^4.1.0", @@ -1877,6 +1907,7 @@ "vitest": "^1.6.0", "yup": "^1.4.0", "yup-password": "^0.3.0", + "zod": "^3.22.4", "zxcvbn": "^4.4.2" }, "devDependencies": { @@ -1899,6 +1930,7 @@ "@types/randombytes": "^2.0.3", "@types/react": "^18.2.24", "@types/react-beautiful-dnd": "^13.1.2", + "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-csv": "^1.1.2", "@types/react-datepicker": "^4.3.4", "@types/react-dom": "^18.2.8", @@ -1925,7 +1957,7 @@ "prettier": "^4.0.0-alpha.8", "storybook-dark-mode": "^3.0.1", "tailwindcss": "^3.4.1", - "typescript": "^5.5.0" + "typescript": "~5.5.0" } }, "apps/studio/node_modules/@heroicons/react": { @@ -2971,15 +3003,19 @@ "@octokit/graphql": "^7.0.2", "@octokit/plugin-paginate-graphql": "^4.0.0", "@octokit/rest": "^20.0.2", + "@radix-ui/react-dialog": "^1.0.5", "@react-three/fiber": "^7.0.29", "@supabase/auth-helpers-react": "^0.4.2", "@supabase/auth-ui-react": "^0.4.5", "@supabase/supabase-js": "^2.44.3", "@vercel/og": "^0.6.2", + "ai-commands": "*", "animejs": "^3.2.2", "classnames": "^2.3.1", + "clsx": "^1.2.1", "cobe": "^0.6.2", "common": "*", + "common-tags": "^1.8.2", "config": "*", "contentlayer2": "0.4.6", "dat.gui": "^0.7.9", @@ -2989,6 +3025,7 @@ "globby": "^13.2.2", "gray-matter": "^4.0.3", "icons": "*", + "lodash": "^4.0.0", "lucide-react": "*", "markdown-toc": "^1.2.0", "next": "^14.2.10", @@ -2996,6 +3033,7 @@ "next-mdx-remote": "^4.4.1", "next-seo": "^6.5.0", "next-themes": "^0.3.0", + "openai": "^4.20.1", "parse-numeric-range": "^1.3.0", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", @@ -3015,7 +3053,7 @@ "swiper": "^11.0.7", "three": "^0.136.0", "typed.js": "^2.0.16", - "typescript": "^5.5.0", + "typescript": "~5.5.0", "ui": "*", "ui-patterns": "*", "use-debounce": "^7.0.1", @@ -3025,7 +3063,9 @@ "devDependencies": { "@types/animejs": "^3.1.12", "@types/classnames": "^2.3.1", + "@types/common-tags": "^1.8.4", "@types/dat.gui": "^0.7.10", + "@types/lodash": "^4.0.0", "@types/mdx-js__react": "^1.5.6", "@types/node": "^20.11.16", "@types/parse-numeric-range": "^0.0.1", @@ -43303,7 +43343,7 @@ "sql-formatter": "^15.0.2", "ts-jest": "^29.1.1", "tsconfig": "*", - "typescript": "^5.5.0" + "typescript": "~5.5.0" } }, "packages/ai-commands/node_modules/argparse": { @@ -43408,10 +43448,11 @@ "react-use": "^17.4.0" }, "devDependencies": { + "@types/lodash": "4.17.5", "@types/react": "^18.2.24", "@types/react-dom": "^18.2.8", "tsconfig": "*", - "typescript": "^5.5.0" + "typescript": "~5.5.0" }, "peerDependencies": { "@supabase/auth-js": "^2.63.0", @@ -43464,7 +43505,13 @@ "packages/icons": { "dependencies": { "@supabase/build-icons": "*", - "typescript": "^5.5.0" + "typescript": "~5.5.0" + }, + "devDependencies": { + "@types/react": "^18.2.24" + }, + "peerDependencies": { + "react": "*" } }, "packages/pg-meta": { @@ -43475,7 +43522,7 @@ "zod": "^3.22.4" }, "devDependencies": { - "typescript": "^5.5.0" + "typescript": "~5.5.0" } }, "packages/shared-data": { @@ -43498,6 +43545,7 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-context": "^1.0.1", "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -43577,6 +43625,7 @@ "@testing-library/react": "^14.0.0", "@tokens-studio/sd-transforms": "^0.8.5", "@types/common-tags": "^1.8.1", + "@types/lodash": "4.17.5", "@types/react": "^18.2.24", "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-dom": "^18.2.8", @@ -43592,7 +43641,8 @@ "ts-jest": "^29.1.1", "tsconfig": "*", "tsconfig-paths-webpack-plugin": "^4.0.1", - "typescript": "^5.5.0" + "typescript": "~5.5.0", + "vitest": "^1.6.0" }, "peerDependencies": { "next": "*", @@ -43603,8 +43653,11 @@ "packages/ui-patterns": { "license": "MIT", "dependencies": { + "@hookform/resolvers": "^3.1.1", "@monaco-editor/react": "^4.6.0", "@supabase/sql-to-rest": "^0.1.6", + "class-variance-authority": "^0.6.0", + "clsx": "^1.2.1", "cmdk": "^1.0.0", "common": "*", "common-tags": "^1.8.2", @@ -43614,18 +43667,24 @@ "lucide-react": "*", "monaco-editor": "*", "next-themes": "*", + "openai": "^4.20.1", "prism-react-renderer": "^2.3.1", "react-error-boundary": "^4.0.12", + "react-hook-form": "^7.45.0", + "react-intersection-observer": "^9.8.2", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "react-tooltip": "*", "react-use": "^17.5.0", "reactflow": "*", "remark-gfm": "^4.0.0", + "sonner": "^1.5.0", "sql-formatter": "^15.3.1", + "sse.js": "^2.2.0", "tsconfig": "*", "ui": "*", - "valtio": "*" + "valtio": "*", + "zod": "^3.22.4" }, "devDependencies": { "@supabase/auth-helpers-react": "^0.4.2", @@ -43639,6 +43698,8 @@ "@types/react-dom": "^18.3.0", "api-types": "*", "next-router-mock": "^0.9.13", + "typescript": "~5.5.0", + "vite": "^5.2.14", "vitest": "^2.0.3" }, "peerDependencies": { @@ -44755,7 +44816,7 @@ "jest-environment-jsdom": "^29.7.0", "postgres": "^3.0.5", "ts-jest": "^29.1.1", - "typescript": "^5.5.0" + "typescript": "~5.5.0" } }, "tests/node_modules/@testdeck/core": { diff --git a/packages/ai-commands/package.json b/packages/ai-commands/package.json index 67127512fc3..d60f94f559d 100644 --- a/packages/ai-commands/package.json +++ b/packages/ai-commands/package.json @@ -34,6 +34,6 @@ "sql-formatter": "^15.0.2", "ts-jest": "^29.1.1", "tsconfig": "*", - "typescript": "^5.5.0" + "typescript": "~5.5.0" } } diff --git a/packages/common/package.json b/packages/common/package.json index dc691830695..07194c309d7 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -17,10 +17,11 @@ "react-use": "^17.4.0" }, "devDependencies": { + "@types/lodash": "4.17.5", "@types/react": "^18.2.24", "@types/react-dom": "^18.2.8", "tsconfig": "*", - "typescript": "^5.5.0" + "typescript": "~5.5.0" }, "peerDependencies": { "@supabase/supabase-js": "*", diff --git a/packages/icons/package.json b/packages/icons/package.json index 5bbbe5cf4e5..a33333c9b24 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -5,7 +5,13 @@ "build:icons": "build-icons --templateSrc=./scripts/exportTemplate.mjs --renderUniqueKey --iconFileExtension=.ts --exportFileName=index.ts" }, "dependencies": { - "typescript": "^5.5.0", + "typescript": "~5.5.0", "@supabase/build-icons": "*" + }, + "devDependencies": { + "@types/react": "^18.2.24" + }, + "peerDependencies": { + "react": "*" } } diff --git a/packages/pg-meta/package.json b/packages/pg-meta/package.json index a3a3c3e887b..5704c5d6090 100644 --- a/packages/pg-meta/package.json +++ b/packages/pg-meta/package.json @@ -9,6 +9,6 @@ "zod": "^3.22.4" }, "devDependencies": { - "typescript": "^5.5.0" + "typescript": "~5.5.0" } } diff --git a/packages/ui-patterns/TweetCard/index.tsx b/packages/ui-patterns/TweetCard/index.tsx index 1c737a2c1bf..1fbd7f36ab1 100644 --- a/packages/ui-patterns/TweetCard/index.tsx +++ b/packages/ui-patterns/TweetCard/index.tsx @@ -1,7 +1,4 @@ -import { hover } from '@testing-library/user-event/dist/hover' -import { drop } from 'lodash' import Image from 'next/image' -import xl from 'react-syntax-highlighter/dist/esm/languages/hljs/xl' import { cn } from 'ui/src/lib/utils' interface TweetCard { diff --git a/packages/ui-patterns/admonition.tsx b/packages/ui-patterns/admonition.tsx index ff3e12b46ef..0754ec46867 100644 --- a/packages/ui-patterns/admonition.tsx +++ b/packages/ui-patterns/admonition.tsx @@ -1,7 +1,6 @@ import { cva } from 'class-variance-authority' -import React, { forwardRef } from 'react' -import { cn } from 'ui/src/lib/utils/cn' -import { Alert, AlertDescription, AlertTitle } from 'ui/src/components/shadcn/ui/alert' +import { forwardRef } from 'react' +import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, cn } from 'ui' export interface AdmonitionProps { type: @@ -18,8 +17,8 @@ export interface AdmonitionProps { description?: string | React.ReactNode showIcon?: boolean childProps?: { - title?: React.ComponentProps - description?: React.ComponentProps + title?: React.ComponentProps + description?: React.ComponentProps } } @@ -88,8 +87,8 @@ const admonitionBase = cva('', { }) export const Admonition = forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & AdmonitionProps + React.ElementRef, + React.ComponentPropsWithoutRef & AdmonitionProps >( ( { type = 'note', variant, showIcon = true, label, title, description, children, ...props }, @@ -98,7 +97,7 @@ export const Admonition = forwardRef< const typeMapped = variant ? admonitionToAlertMapping[variant] : admonitionToAlertMapping[type] return ( - - {label || title} - + {description && ( - + {description} - + )} {/* // children is to handle Docs and MDX issues with children and

elements */} {children && ( - {children} - + )} ) : ( @@ -149,7 +148,7 @@ export const Admonition = forwardRef< {children} )} - + ) } ) diff --git a/packages/ui-patterns/form/Layout/FormLayout.tsx b/packages/ui-patterns/form/Layout/FormLayout.tsx index 8c57754db4c..a6a720eb94d 100644 --- a/packages/ui-patterns/form/Layout/FormLayout.tsx +++ b/packages/ui-patterns/form/Layout/FormLayout.tsx @@ -1,4 +1,5 @@ import { cva } from 'class-variance-authority' +import { motion } from 'framer-motion' import React from 'react' import { FormDescription_Shadcn_, @@ -8,7 +9,6 @@ import { cn, } from 'ui' import { SIZE } from 'ui/src/lib/constants' -import { AnimatePresence, motion } from 'framer-motion' type Props = { align?: 'left' | 'right' diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index a981daab814..f1b3bc5d1e3 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -9,8 +9,11 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@hookform/resolvers": "^3.1.1", "@monaco-editor/react": "^4.6.0", "@supabase/sql-to-rest": "^0.1.6", + "class-variance-authority": "^0.6.0", + "clsx": "^1.2.1", "cmdk": "^1.0.0", "common": "*", "common-tags": "^1.8.2", @@ -21,17 +24,23 @@ "monaco-editor": "*", "next-themes": "*", "prism-react-renderer": "^2.3.1", + "openai": "^4.20.1", "react-error-boundary": "^4.0.12", + "react-hook-form": "^7.45.0", + "react-intersection-observer": "^9.8.2", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "react-tooltip": "*", "react-use": "^17.5.0", "reactflow": "*", "remark-gfm": "^4.0.0", + "sonner": "^1.5.0", "sql-formatter": "^15.3.1", + "sse.js": "^2.2.0", "tsconfig": "*", "ui": "*", - "valtio": "*" + "valtio": "*", + "zod": "^3.22.4" }, "devDependencies": { "@supabase/auth-helpers-react": "^0.4.2", @@ -45,6 +54,8 @@ "@types/react-dom": "^18.3.0", "api-types": "*", "next-router-mock": "^0.9.13", + "typescript": "~5.5.0", + "vite": "^5.2.14", "vitest": "^2.0.3" }, "peerDependencies": { diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 2e93ef25595..80b9df5388b 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -4,7 +4,6 @@ export * from './src/lib/Markdown' // GENERAL -export * from 'ui-patterns/admonition' export * from './src/components/Button' export * from './src/components/Icon' export * from './src/components/Icon/IconContext' diff --git a/packages/ui/jest.config.ts b/packages/ui/jest.config.ts deleted file mode 100644 index e3b4f29d916..00000000000 --- a/packages/ui/jest.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Config } from '@jest/types' - -const config: Config.InitialOptions = { - preset: 'ts-jest', - setupFilesAfterEnv: ['/jest.setup.ts'], - testEnvironment: 'jsdom', - // Restricting test file pattern to just tsx files since the js files need a babel preset to run - // and most likely won't pass now anyway - the tests were ported from the UI repo and are out of date - testRegex: '(.*\\.test.tsx$)', - moduleNameMapper: { - '^@ui/(.*)$': '/src/$1', - }, -} - -export default config diff --git a/packages/ui/jest.setup.ts b/packages/ui/jest.setup.ts deleted file mode 100644 index c44951a680d..00000000000 --- a/packages/ui/jest.setup.ts +++ /dev/null @@ -1 +0,0 @@ -import '@testing-library/jest-dom' diff --git a/packages/ui/package.json b/packages/ui/package.json index 89cff926fff..6488a4043f0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -12,7 +12,7 @@ "transform-tokens": "node ./transformTokens.js", "extract-design-tokens": "node internals/tokens/extract-design-tokens.js", "generate-styles": "npm run extract-design-tokens && npm run transform-tokens && npm run cleanse-css-for-tailwind && npm run generate-demo-tailwind-classes", - "test": "jest" + "test": "vitest" }, "dependencies": { "@headlessui/react": "^1.7.17", @@ -24,6 +24,7 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-context": "^1.0.1", "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -98,11 +99,11 @@ "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.4", "@ctrl/tinycolor": "^3.4.0", - "@jest/types": "^29.6.0", "@testing-library/jest-dom": "^6.1.3", "@testing-library/react": "^14.0.0", "@tokens-studio/sd-transforms": "^0.8.5", "@types/common-tags": "^1.8.1", + "@types/lodash": "4.17.5", "@types/react": "^18.2.24", "@types/react-copy-to-clipboard": "^5.0.4", "@types/react-dom": "^18.2.8", @@ -111,14 +112,12 @@ "config": "*", "copyfiles": "^2.4.1", "glob": "^8.1.0", - "jest-environment-jsdom": "^29.7.0", - "next": "*", "sql-formatter": "^12.2.0", "style-dictionary": "^3.7.1", - "ts-jest": "^29.1.1", "tsconfig": "*", "tsconfig-paths-webpack-plugin": "^4.0.1", - "typescript": "^5.5.0" + "typescript": "~5.5.0", + "vitest": "^1.6.0" }, "peerDependencies": { "next": "*", diff --git a/packages/ui/src/components/Alert/Alert.test.tsx b/packages/ui/src/components/Alert/Alert.test.tsx index 0c0402e89fa..9def28fde71 100644 --- a/packages/ui/src/components/Alert/Alert.test.tsx +++ b/packages/ui/src/components/Alert/Alert.test.tsx @@ -1,4 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + import defaultTheme from '../../lib/theme/defaultTheme' import { Alert, AlertVariant } from './Alert' diff --git a/packages/ui/src/components/Breadcrumb/Breadcrumb.test.js b/packages/ui/src/components/Breadcrumb/Breadcrumb.test.jsx similarity index 74% rename from packages/ui/src/components/Breadcrumb/Breadcrumb.test.js rename to packages/ui/src/components/Breadcrumb/Breadcrumb.test.jsx index 2f5646af7e2..c560eb50a02 100644 --- a/packages/ui/src/components/Breadcrumb/Breadcrumb.test.js +++ b/packages/ui/src/components/Breadcrumb/Breadcrumb.test.jsx @@ -1,9 +1,11 @@ -import React from from 'react'; import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, it } from 'vitest'; + import Breadcrumb from './Breadcrumb'; describe('#Breadcrumb', () => { - it('should render breadcrumb correctly', async () => { + it.skip('should render breadcrumb correctly', async () => { render(

Item 1

@@ -13,7 +15,7 @@ describe('#Breadcrumb', () => { expect(screen.queryByTestId('breadcrumb')).toBeInTheDocument() }) - it('should have "breadcrumb--item" class', () => { + it.skip('should have "breadcrumb--item" class', () => { render(

Item 1

diff --git a/packages/ui/src/components/Button/Button.test.tsx b/packages/ui/src/components/Button/Button.test.tsx index 324f733011f..0defd7a47ec 100644 --- a/packages/ui/src/components/Button/Button.test.tsx +++ b/packages/ui/src/components/Button/Button.test.tsx @@ -1,6 +1,8 @@ import { fireEvent, render, screen } from '@testing-library/react' import Link from 'next/link' import React from 'react' +import { describe, expect, it } from 'vitest' + import { Button } from './Button' const SIZES = ['tiny', 'small', 'medium', 'large', 'xlarge'] as const diff --git a/packages/ui/src/components/Checkbox/Checkbox.test.js b/packages/ui/src/components/Checkbox/Checkbox.test.jsx similarity index 91% rename from packages/ui/src/components/Checkbox/Checkbox.test.js rename to packages/ui/src/components/Checkbox/Checkbox.test.jsx index 41eda3a2002..699296c95c3 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.test.js +++ b/packages/ui/src/components/Checkbox/Checkbox.test.jsx @@ -1,5 +1,7 @@ -import React from 'react' import { render, screen } from '@testing-library/react' +import React from 'react' +import { describe, expect, it } from 'vitest' + import Checkbox from './Checkbox' describe('#Checkbox', () => { diff --git a/packages/ui/src/components/CustomHTMLElements/CustomHTMLElements.utils.test.tsx b/packages/ui/src/components/CustomHTMLElements/CustomHTMLElements.utils.test.tsx index 2d2d1285229..83c0cb998c4 100644 --- a/packages/ui/src/components/CustomHTMLElements/CustomHTMLElements.utils.test.tsx +++ b/packages/ui/src/components/CustomHTMLElements/CustomHTMLElements.utils.test.tsx @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest' + import { getAnchor, removeAnchor } from './CustomHTMLElements.utils' describe('CustomHTMLElementsUtils', () => { diff --git a/packages/ui/src/components/Icon/Icon.test.js b/packages/ui/src/components/Icon/Icon.test.js deleted file mode 100644 index 865c70baa4b..00000000000 --- a/packages/ui/src/components/Icon/Icon.test.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react' -import { render, screen } from '@testing-library/react' -import Icon from './Icon' - -describe('#Icon', () => { - it('should render Icon correctly', () => { - render() - const $elIcon = screen.queryByTestId('icon') - expect($elIcon).toBeInTheDocument() - }) - - it('should change svg size', () => { - render() - - const $elIcon = screen.queryByTestId('icon') - expect($elIcon.getAttribute('width')).toEqual('40') - expect($elIcon.getAttribute('height')).toEqual('40') - }) - - it('should change strokeWidth value', () => { - render() - - const $elIcon = screen.queryByTestId('icon') - expect($elIcon.getAttribute('stroke-width')).toEqual('1') - }) -}) diff --git a/packages/ui/src/components/InputOld/Input.test.js b/packages/ui/src/components/InputOld/Input.test.js deleted file mode 100644 index 98fb06abcf9..00000000000 --- a/packages/ui/src/components/InputOld/Input.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react' -import { render, screen } from '@testing-library/react' -import Input, { SIZES } from './Input' - -describe('#Input', () => { - it('should render input correctly', async () => { - render() - expect(screen.queryByTestId('form-input')).toBeInTheDocument() - }) - - it('should have "form-input--error" class', () => { - render() - expect(screen.queryByTestId('form-input')).toHaveClass('form-input border-solid form-input--error') - }) - - it.each(SIZES)('should have "form-input--%s" class', (size) => { - render() - expect(screen.queryByTestId('form-input')).toHaveClass(`form-input border-solid form-input--${size}`) - }) -}) \ No newline at end of file diff --git a/packages/ui/src/components/InputOld/Input.test.jsx b/packages/ui/src/components/InputOld/Input.test.jsx new file mode 100644 index 00000000000..fb3ec4ec06e --- /dev/null +++ b/packages/ui/src/components/InputOld/Input.test.jsx @@ -0,0 +1,19 @@ +import { describe, it } from 'vitest' + + +describe('#Input', () => { + it.skip('should render input correctly', async () => { + // render() + // expect(screen.queryByTestId('form-input')).toBeInTheDocument() + }) + + it.skip('should have "form-input--error" class', () => { + // render() + // expect(screen.queryByTestId('form-input')).toHaveClass('form-input border-solid form-input--error') + }) + + // it.each(SIZES)('should have "form-input--%s" class', (size) => { + // render() + // expect(screen.queryByTestId('form-input')).toHaveClass(`form-input border-solid form-input--${size}`) + // }) +}) \ No newline at end of file diff --git a/packages/ui/src/components/Listbox/SelectStyled.test.js b/packages/ui/src/components/Listbox/SelectStyled.test.jsx similarity index 74% rename from packages/ui/src/components/Listbox/SelectStyled.test.js rename to packages/ui/src/components/Listbox/SelectStyled.test.jsx index ca4eca6aa86..cdd90f083c6 100644 --- a/packages/ui/src/components/Listbox/SelectStyled.test.js +++ b/packages/ui/src/components/Listbox/SelectStyled.test.jsx @@ -1,9 +1,11 @@ -import React from 'react' import { render, screen } from '@testing-library/react' -import Select from './Listbox' +import React from 'react' +import { describe, expect, it } from 'vitest' + +import Select from './index' describe('#Select', () => { - it('should render select correctly', async () => { + it.skip('should render select correctly', async () => { render( diff --git a/packages/ui/src/components/NavMenu/nav-menu.test.tsx b/packages/ui/src/components/NavMenu/nav-menu.test.tsx index dac394b5909..6c45ae91595 100644 --- a/packages/ui/src/components/NavMenu/nav-menu.test.tsx +++ b/packages/ui/src/components/NavMenu/nav-menu.test.tsx @@ -1,8 +1,9 @@ -import React from 'react' import { render, screen } from '@testing-library/react' -import '@testing-library/jest-dom' -import { NavMenu, NavMenuItem } from '.' import Link from 'next/link' +import React from 'react' +import { describe, expect, it } from 'vitest' + +import { NavMenu, NavMenuItem } from './index' describe('NavMenu Component', () => { it('renders without crashing', () => { diff --git a/packages/ui/src/components/RadioOld/Radio.test.js b/packages/ui/src/components/RadioOld/Radio.test.js deleted file mode 100644 index 7eaa33451b4..00000000000 --- a/packages/ui/src/components/RadioOld/Radio.test.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import { render, screen } from '@testing-library/react' -import Radio from './Radio' - -describe('#Radio', () => { - it('should render radio correctly', () => { - render( - - ) - expect(screen.queryByTestId('form-radio')).toBeInTheDocument() - }) - - it('should show label', () => { - render( - - ) - expect(screen.queryByText('JavaScript')).toBeInTheDocument() - }) -}) \ No newline at end of file diff --git a/packages/ui/src/components/RadioOld/Radio.test.jsx b/packages/ui/src/components/RadioOld/Radio.test.jsx new file mode 100644 index 00000000000..e9fb5d9f0ae --- /dev/null +++ b/packages/ui/src/components/RadioOld/Radio.test.jsx @@ -0,0 +1,19 @@ +import { describe, it } from 'vitest' + + +describe('#Radio', () => { + + it('should render radio correctly', () => { + // render( + // // + // ) + // expect(screen.queryByTestId('form-radio')).toBeInTheDocument() + }) + + it('should show label', () => { + // render( + // + // ) + // expect(screen.queryByText('JavaScript')).toBeInTheDocument() + }) +}) \ No newline at end of file diff --git a/packages/ui/src/components/Select/Select.test.js b/packages/ui/src/components/Select/Select.test.jsx similarity index 58% rename from packages/ui/src/components/Select/Select.test.js rename to packages/ui/src/components/Select/Select.test.jsx index 2615c4dd687..4e425003738 100644 --- a/packages/ui/src/components/Select/Select.test.js +++ b/packages/ui/src/components/Select/Select.test.jsx @@ -1,5 +1,7 @@ -import React from 'react' import { render, screen } from '@testing-library/react' +import React from 'react' +import { describe, expect, it } from 'vitest' + import Select from './Select' describe('#Select', () => { @@ -21,7 +23,7 @@ describe('#Select', () => { ) expect(screen.queryByTestId('form-select')).toHaveClass( - 'sbui-select sbui-select--error sbui-select--medium' + 'block box-border w-full rounded-md shadow-sm transition-all text-foreground border focus-visible:shadow-md outline-none focus:ring-current focus:ring-2 focus-visible:border-foreground-muted focus-visible:ring-background-control placeholder-foreground-muted appearance-none bg-none bg-destructive-200 border border-destructive-500 focus:ring-destructive-400 placeholder:text-destructive-400 text-sm px-4 py-2' ) }) }) diff --git a/packages/ui/src/components/SimpleCodeBlock/SimpleCodeBlock.tsx b/packages/ui/src/components/SimpleCodeBlock/SimpleCodeBlock.tsx index 7e5eb415e7d..7ffcbd5ca56 100644 --- a/packages/ui/src/components/SimpleCodeBlock/SimpleCodeBlock.tsx +++ b/packages/ui/src/components/SimpleCodeBlock/SimpleCodeBlock.tsx @@ -6,12 +6,12 @@ * LICENSE file in the root directory of this source tree. */ -import { Prism, themes, Highlight, Language } from 'prism-react-renderer' -import { PropsWithChildren, useEffect, useRef, useState } from 'react' import { useTheme } from 'next-themes' -import { dart } from './prism' -import { copyToClipboard } from '../../lib/utils/clipboard' +import { Highlight, Language, Prism, themes } from 'prism-react-renderer' +import { PropsWithChildren, useEffect, useRef, useState } from 'react' +import { copyToClipboard } from '../../lib/utils' import { Button } from '../Button' +import { dart } from './prism' dart(Prism) diff --git a/packages/ui/src/components/StatusIcon.tsx b/packages/ui/src/components/StatusIcon.tsx index d7dc7b9c656..1f4dea1d4e4 100644 --- a/packages/ui/src/components/StatusIcon.tsx +++ b/packages/ui/src/components/StatusIcon.tsx @@ -1,5 +1,5 @@ import { SVGProps, forwardRef } from 'react' -import { cn } from 'ui' +import { cn } from '../lib/utils' export interface StatusIconProps { hideBackground?: boolean @@ -101,4 +101,4 @@ const WarningIcon: React.FC & StatusIconProps> = ({ ) } -export { WarningIcon, CriticalIcon, InfoIcon } +export { CriticalIcon, InfoIcon, WarningIcon } diff --git a/packages/ui/src/components/shadcn/ui/dialog.tsx b/packages/ui/src/components/shadcn/ui/dialog.tsx index d65eeb88d32..c87b4f5ebf7 100644 --- a/packages/ui/src/components/shadcn/ui/dialog.tsx +++ b/packages/ui/src/components/shadcn/ui/dialog.tsx @@ -1,11 +1,12 @@ 'use client' import * as DialogPrimitive from '@radix-ui/react-dialog' +import { DialogProps } from '@radix-ui/react-dialog' import { X } from 'lucide-react' import * as React from 'react' -import { cn } from '../../../lib/utils/cn' import { VariantProps, cva } from 'class-variance-authority' +import { cn } from '../../../lib/utils/cn' export const DIALOG_PADDING_Y_SMALL = 'py-4' export const DIALOG_PADDING_X_SMALL = 'px-5' @@ -208,13 +209,14 @@ DialogSectionSeparator.displayName = 'DialogSectionSeparator' export { Dialog, + DialogClose, DialogContent, DialogDescription, DialogFooter, - DialogClose, DialogHeader, - DialogTitle, - DialogTrigger, DialogSection, DialogSectionSeparator, + DialogTitle, + DialogTrigger, + type DialogProps, } diff --git a/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx b/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx index 9f34c377704..6c9ce6e363a 100644 --- a/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx +++ b/packages/ui/src/components/shadcn/ui/dropdown-menu.tsx @@ -1,6 +1,10 @@ 'use client' import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { + DropdownMenuCheckboxItemProps, + DropdownMenuTriggerProps, +} from '@radix-ui/react-dropdown-menu' import { Check, ChevronRight, Circle } from 'lucide-react' import * as React from 'react' @@ -184,4 +188,6 @@ export { DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, + type DropdownMenuCheckboxItemProps, + type DropdownMenuTriggerProps, } diff --git a/packages/ui/src/components/shadcn/ui/input-otp.tsx b/packages/ui/src/components/shadcn/ui/input-otp.tsx index d3c75c21ac6..0b5e19bc777 100644 --- a/packages/ui/src/components/shadcn/ui/input-otp.tsx +++ b/packages/ui/src/components/shadcn/ui/input-otp.tsx @@ -1,8 +1,8 @@ 'use client' -import * as React from 'react' -import { OTPInput, OTPInputContext } from 'input-otp' +import { OTPInput, OTPInputContext, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp' import { Dot } from 'lucide-react' +import * as React from 'react' import { cn } from '../../../lib/utils/cn' @@ -68,4 +68,4 @@ const InputOTPSeparator = React.forwardRef< )) InputOTPSeparator.displayName = 'InputOTPSeparator' -export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } +export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, REGEXP_ONLY_DIGITS_AND_CHARS } diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 4f7d61f4e15..aebbf48962e 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig/react-library.json", + "extends": "tsconfig/react-library.json", "compilerOptions": { "resolveJsonModule": true }, diff --git a/packages/ui/vitest.config.ts b/packages/ui/vitest.config.ts new file mode 100644 index 00000000000..8623a6c59bf --- /dev/null +++ b/packages/ui/vitest.config.ts @@ -0,0 +1,10 @@ +/// + +import { defineConfig } from 'vite' + +export default defineConfig({ + test: { + environment: 'jsdom', + setupFiles: ['./vitest.setup.ts'], + }, +}) diff --git a/packages/ui/vitest.setup.ts b/packages/ui/vitest.setup.ts new file mode 100644 index 00000000000..a4f40c8d8d0 --- /dev/null +++ b/packages/ui/vitest.setup.ts @@ -0,0 +1,27 @@ +/// + +import * as matchers from '@testing-library/jest-dom/matchers' +import { cleanup } from '@testing-library/react' +import { afterEach, expect, vi } from 'vitest' + +expect.extend(matchers) + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), // deprecated + removeListener: vi.fn(), // deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}) + +vi.mock('next/navigation', () => require('next-router-mock')) + +afterEach(() => { + cleanup() +}) diff --git a/tests/package.json b/tests/package.json index 610c16c8411..79e649014de 100644 --- a/tests/package.json +++ b/tests/package.json @@ -31,6 +31,6 @@ "jest-allure2-adapter": "^0.3.12", "jest-environment-jsdom": "^29.7.0", "postgres": "^3.0.5", - "typescript": "^5.5.0" + "typescript": "~5.5.0" } } From 2e7745883a220de61d95ca0de6544bbfef085343 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Tue, 1 Oct 2024 18:58:22 +0800 Subject: [PATCH 03/13] Fix search user (#29615) * Fix search user * Tiny --- apps/studio/components/interfaces/Auth/Users/UsersV2.tsx | 2 +- apps/studio/data/auth/users-infinite-query.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx index 94ac53a8dea..1cb9d0a34c6 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx @@ -123,7 +123,7 @@ export const UsersV2 = () => { }) // eslint-disable-next-line react-hooks/exhaustive-deps - const totalUsers = useMemo(() => countData?.result[0].count, [countData?.result[0].count]) + const totalUsers = useMemo(() => countData?.result[0].count ?? 0, [countData?.result[0].count]) const users = useMemo(() => data?.pages.flatMap((page) => page.result), [data?.pages]) const handleScroll = (event: UIEvent) => { diff --git a/apps/studio/data/auth/users-infinite-query.ts b/apps/studio/data/auth/users-infinite-query.ts index 79728a35c6d..95e98727aaa 100644 --- a/apps/studio/data/auth/users-infinite-query.ts +++ b/apps/studio/data/auth/users-infinite-query.ts @@ -43,7 +43,7 @@ const getUsersSQL = ({ if (hasValidKeywords) { conditions.push( - `id::text ilike '%${keywords}%' or email ilike '%${keywords}%' or phone ilike '%${keywords}%` + `id::text ilike '%${keywords}%' or email ilike '%${keywords}%' or phone ilike '%${keywords}%'` ) } From eba13666e218978ee4eaddfc5d139b8ee7650366 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:19:06 +0200 Subject: [PATCH 04/13] hide create collection button on sidebar if flag isnt enabled (#29619) --- .../layouts/LogsLayout/LogsSidebarMenuV2.tsx | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx index a9f0b881c9f..8b01cebced6 100644 --- a/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx +++ b/apps/studio/components/layouts/LogsLayout/LogsSidebarMenuV2.tsx @@ -192,25 +192,27 @@ export function LogsSidebarMenuV2() { Create query - - - - - - - {!canCreateCollection && ( - - You need additional permissions to create a collection - - )} - + {warehouseEnabled && ( + + + + + + + {!canCreateCollection && ( + + You need additional permissions to create a collection + + )} + + )} ))} - {whCollectionsLoading ? ( + {whCollectionsLoading && warehouseEnabled ? (
From f8cb0821507a084d043154e8c3ebf57de4feff40 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Tue, 1 Oct 2024 17:28:32 +0200 Subject: [PATCH 05/13] feat: careers ATS switch [DON'T MERGE] (#29283) * switch careers ATS * clean up careers page --- apps/www/lib/careers.ts | 24 ++ apps/www/pages/careers.tsx | 767 ++++++++++++++++++------------------- 2 files changed, 407 insertions(+), 384 deletions(-) create mode 100644 apps/www/lib/careers.ts diff --git a/apps/www/lib/careers.ts b/apps/www/lib/careers.ts new file mode 100644 index 00000000000..51b022ea1de --- /dev/null +++ b/apps/www/lib/careers.ts @@ -0,0 +1,24 @@ +export interface JobItemProps { + id: string + title: string + location: any + team: string + employment: string + descriptionHtml: string + jobUrl: string +} + +export const PLACEHOLDER_JOB_ID = '64d76968-1fe1-458c-8c6d-8859168c3fb7' +export const filterGenericJob = (job: JobItemProps) => job.id === PLACEHOLDER_JOB_ID +export const groupJobsByTeam = (jobs: JobItemProps[]) => { + return jobs.reduce( + (acc, job) => { + if (!acc[job.team]) { + acc[job.team] = [] + } + acc[job.team].push(job) + return acc + }, + {} as Record + ) +} diff --git a/apps/www/pages/careers.tsx b/apps/www/pages/careers.tsx index 70e68fcb283..05085155582 100644 --- a/apps/www/pages/careers.tsx +++ b/apps/www/pages/careers.tsx @@ -1,22 +1,26 @@ -import { GlobeAltIcon } from '@heroicons/react/outline' -import { Check } from 'lucide-react' import { NextPage } from 'next' -import { NextSeo } from 'next-seo' import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/router' -import ReactMarkdown from 'react-markdown' +import { NextSeo } from 'next-seo' +import { GlobeAltIcon } from '@heroicons/react/outline' +import { Check } from 'lucide-react' import { Badge, Button, Separator, buttonVariants, cn } from 'ui' +import ReactMarkdown from 'react-markdown' +import Styles from '~/styles/career.module.css' + import Globe from '~/components/Globe' import DefaultLayout from '~/components/Layouts/Default' import SectionContainer from '~/components/Layouts/SectionContainer' + +import { groupJobsByTeam, filterGenericJob, JobItemProps, PLACEHOLDER_JOB_ID } from '~/lib/careers' import career from '~/data/career.json' -import Styles from '~/styles/career.module.css' export async function getStaticProps() { - const job_res = await fetch('https://boards-api.greenhouse.io/v1/boards/supabase/jobs') - + const job_res = await fetch('https://api.ashbyhq.com/posting-api/job-board/supabase') const job_data = await job_res.json() + const jobs = groupJobsByTeam(job_data.jobs.filter((job: JobItemProps) => !filterGenericJob(job))) + const placeholderJob = job_data.jobs.find(filterGenericJob) const contributor_res = await fetch( 'https://api.github.com/repos/supabase/supabase/contributors?per_page=100' @@ -70,13 +74,20 @@ export async function getStaticProps() { return { props: { - jobs: job_data.jobs, + jobs, + placeholderJob, contributors: contributors, }, } } -const CareerPage: NextPage = ({ jobs, contributors }: any) => { +interface CareersPageProps { + jobs: Record + placeholderJob: JobItemProps + contributors: { login: string; avatar_url: string; html_url: string }[] +} + +const CareerPage: NextPage = ({ jobs, placeholderJob, contributors }) => { const { basePath } = useRouter() const meta_title = 'Careers | Supabase' @@ -99,412 +110,400 @@ const CareerPage: NextPage = ({ jobs, contributors }: any) => { }} /> -
-
-

- Supabase Careers -

-

- We're on a mission to build the best developer platform -

-

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

- -
+
+

+ Supabase Careers +

+

+ We're on a mission to build the best developer platform +

+

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

+ +
- -
- {career.company.map((company: { number: string; text: string }, i: number) => { - return ( -
-
-

- {company.number} -

- - {company.text} - -
- ) - })} -
-
- -
-
- + +
+ {career.company.map((company: { number: string; text: string }, i: number) => { + return (
+
+

{company.number}

+ + {company.text} + +
+ ) + })} +
+
+ +
+
+ +
- -
-
-

- We work together, wherever we are -

-

- Working in a globally distributed team is rewarding but has its challenges. We - are across many different timezones, so we use tools like Notion, Slack, and - Discord to stay connected to our team, and our community. -

-
-
-

- We deeply believe in the efficacy of collaborative open source -

-
-
-
- - -
-
-
-

- What is Supabase -

-

- Supabase is an open source Firebase alternative, built by developers for - developers. Supabase adds auth, realtime, storage, restful APIs, and edge - functions to Postgres without a single line of code. Supabase was - born-remote. Having a globally distributed, open source company is our - secret weapon to hiring top-tier talent. -

-
-
-
- team photo -
-
-
-
-
- team photo -
-
-
-
-
-
-
- team photo -
-
-
-
-
- team photo -
-
-
-
- team photo -
-
-
-
-
- team photo -
-
-
-
-
- - -

- Human powered + > + +

+
+

+ We work together, wherever we are

-

- As a completely remote and asynchronous team, we focus on these five traits to - keep our team effective: +

+ Working in a globally distributed team is rewarding but has its challenges. We are + across many different timezones, so we use tools like Notion, Slack, and Discord + to stay connected to our team, and our community.

-
- {career.humanPowered.map( - (human: { icon: string; title: string; text: string }, i: number) => { - return ( -
-
-

{human.title}

-

{human.text}

-
-
- ) - } - )} -
- - - -
-

- 1,000 + Contributors building Supabase +
+
+

+ We deeply believe in the efficacy of collaborative open source

-

- We're building a community of communities, bringing together developers from - many different backgrounds, as well as new developers looking to get involved - with open source. We love celebrating everyone who contributes their time to the - Supabase mission. -

-
- {contributors.map((contributor: any, i: number) => { - return ( -
- -
- {`${contributor.login} -
- -
- ) - })} -
-
-
-
-
-
-
-
-
-
-
-
-
- -

-
- - -
-
-

- Great people deserve great benefits -

-
- {career.benefits.map( - (benefits: { icon: string; title: string; text: string }, i: number) => { - return ( -
-
-

{benefits.title}

- - {benefits.text} - -
-
- ) - } - )} -
-
-
+ - -
-

How we hire

-

- The entire process is fully remote and all communication happens over email or via - video chat in Google Meet. The calls are all 1:1 and usually take between 20-45 - minutes. We know you are interviewing us too, so please ask questions. We are happy - to answer. -

-
-
- {career.hiring.map((hiring: { title: string; text: string }, i: number) => { - return ( -
-
-

- {i + 1} -

-
-
-
-

- {hiring.title} -

-

- {hiring.text} -

+ +
+
+
+

+ What is Supabase +

+

+ Supabase is an open source Firebase alternative, built by developers for + developers. Supabase adds auth, realtime, storage, restful APIs, and edge + functions to Postgres without a single line of code. Supabase was born-remote. + Having a globally distributed, open source company is our secret weapon to + hiring top-tier talent. +

+
+
+
+ team photo
- ) - })} -

- -

-
- +
+
+
+ team photo +
+
+
+
+
+
+
+ team photo +
+
+
+
+
+ team photo +
+
+
+
+ team photo +
+
+
+
+
+ team photo +
+
+
+
+ - - -
-

- Open positions -

-

- Want to build the best developer platform? -
We’d love to talk to you. +

Human powered

+

+ As a completely remote and asynchronous team, we focus on these five traits to keep + our team effective:

-
- {jobs.map( - ( - job: { - title: string - location: any - employment: string - description: string - absolute_url: string - }, - i: number - ) => { +
+ {career.humanPowered.map( + (human: { icon: string; title: string; text: string }, i: number) => { return ( - -

- {job.title} -

-
-
- - - - {job.location.name} - - {job.employment} -
-

{job.description}

-
- Apply for position -
+
+
+

{human.title}

+

{human.text}

- +
) } )}
+ + +
+

+ 1,000 + Contributors building Supabase +

+

+ We're building a community of communities, bringing together developers from many + different backgrounds, as well as new developers looking to get involved with open + source. We love celebrating everyone who contributes their time to the Supabase + mission. +

+
+
+ {contributors.map((contributor: any, i: number) => { + return ( +
+ +
+ {`${contributor.login} +
+ +
+ ) + })} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+

+ Great people deserve great benefits +

+
+
+ {career.benefits.map( + (benefits: { icon: string; title: string; text: string }, i: number) => { + return ( +
+
+

{benefits.title}

+ + {benefits.text} + +
+
+ ) + } + )} +
+
+
+ + +
+

How we hire

+

+ The entire process is fully remote and all communication happens over email or via + video chat in Google Meet. The calls are all 1:1 and usually take between 20-45 + minutes. We know you are interviewing us too, so please ask questions. We are happy to + answer. +

+
+
+ {career.hiring.map((hiring: { title: string; text: string }, i: number) => { + return ( +
+
+

+ {i + 1} +

+
+
+
+

+ {hiring.title} +

+

+ {hiring.text} +

+
+
+ ) + })} +

+ +

+
+
+ + + +
+ +

+ Open positions +

+

+ Want to build the best developer platform? +
We’d love to talk to you. +

+
+ {Object.entries(jobs).map(([team, teamJobs]) => ( +
+

{team}

+
+ {(teamJobs as JobItemProps[]) + .filter((job: any) => !filterGenericJob(job)) + .map((job: JobItemProps) => ( + + ))} +
+
+ ))} +
+
+ {placeholderJob && } +
+
+
) } +const JobItem: React.FC<{ job: JobItemProps }> = ({ job }) => { + const isPlaceholderJob = job.id === PLACEHOLDER_JOB_ID + + return ( + +

{job.title}

+
+
+ + + {job.location} + + {job.employment} +
+
+ {isPlaceholderJob ? 'Submit resume' : 'Apply for position'} +
+
+ + ) +} + export default CareerPage From 6176d1b130cca647023c9f1131eeef834d915dfe Mon Sep 17 00:00:00 2001 From: Rodrigo Mansueli Date: Tue, 1 Oct 2024 12:38:31 -0300 Subject: [PATCH 06/13] chore: Update NavigationMenu.constants.ts (#29620) Update NavigationMenu.constants.ts --- .../Navigation/NavigationMenu/NavigationMenu.constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 7692f366e14..e0bdc633be9 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1188,7 +1188,7 @@ export const functions: NavMenuConstant = { }, { name: 'Integrating with Log Drains', - url: '/guides/functions/log-drains', + url: '/guides/platform/log-drains', }, ], }, From e36a92b429f5e5c604c2c19736494f00439b5e0a Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Tue, 1 Oct 2024 19:03:59 +0200 Subject: [PATCH 07/13] update careers page stats (#29621) --- apps/www/data/career.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/www/data/career.json b/apps/www/data/career.json index 4814deb907b..1c1809c2098 100644 --- a/apps/www/data/career.json +++ b/apps/www/data/career.json @@ -1,7 +1,7 @@ { "company": [ { - "number": "85+", + "number": "90+", "text": "team members\n\nin 25+ countries" }, { @@ -9,7 +9,7 @@ "text": "languages spoken" }, { - "number": "$116M", + "number": "$196M", "text": "in funding" }, { @@ -17,7 +17,7 @@ "text": "community members" }, { - "number": "5000+", + "number": "5,000+", "text": "memes posted\n\n(and counting)" } ], From 109dd1dc2ed4a13e959afa9dc1c1f2ee162c9e69 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 1 Oct 2024 19:35:38 +0000 Subject: [PATCH 08/13] Chore/add 24hr notice (#29626) Add 24h notice to report --- apps/studio/pages/project/[ref]/reports/database.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/studio/pages/project/[ref]/reports/database.tsx b/apps/studio/pages/project/[ref]/reports/database.tsx index e5453c1dd69..9ce9b4dc911 100644 --- a/apps/studio/pages/project/[ref]/reports/database.tsx +++ b/apps/studio/pages/project/[ref]/reports/database.tsx @@ -214,7 +214,10 @@ const DatabaseUsage = () => { renderer={(props) => { return (
-
+

+ The data refreshes every 24 hours. +

+
Space used
{formatBytes(databaseSizeBytes, 2, 'GB')} From e550f18d36a279dfdc4efaba16d5d1afc770a601 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 1 Oct 2024 21:05:35 +0000 Subject: [PATCH 09/13] Chore/remove menu notice (#29627) Remove temp notice --- .../layouts/ReportsLayout/ReportsMenu.tsx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx b/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx index 0fe62e0accf..a4e768de272 100644 --- a/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx +++ b/apps/studio/components/layouts/ReportsLayout/ReportsMenu.tsx @@ -194,21 +194,6 @@ const ReportsMenu = () => {
))} - {/* [Joshen] Temp notice while we shift some pages on 040424 - can probably remove in 3 months perhaps */} - - - Query Performance and Project Linter reports have been shifted - - -

They can now be found in the menu under the database section.

- -
-
- setSelectedReportToUpdate(undefined)} selectedReport={selectedReportToUpdate} From ec70baf0a374fe7f37949f7050dff7c323b74ef9 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:29:24 -0400 Subject: [PATCH 10/13] Clarify how to use the service role key with the rest API (#29629) I struggled with this for a while. v0 was able to figure this out from the docs, but I wasn't, so I thought an explicit example which makes it easy to go from values you see in the dashboard to what-do-i-need-to-do would help. --- apps/docs/content/guides/auth/jwts.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/docs/content/guides/auth/jwts.mdx b/apps/docs/content/guides/auth/jwts.mdx index 5392067c05e..1ae41b0170a 100644 --- a/apps/docs/content/guides/auth/jwts.mdx +++ b/apps/docs/content/guides/auth/jwts.mdx @@ -162,6 +162,14 @@ You'll notice that this token is quite a bit longer, since it contains informati } ``` +If using the service role key, you'll need to pass it into both the `apikey` and `authorization` headers (again, only do this from a secure environment such as your own server): + +```bash +curl "$YOUR_PROJECT_URL/rest/v1/colors?select=name" \ + -H "apikey: $YOUR_SERVICE_ROLE_KEY" \ + -H "authorization: Bearer $YOUR_SERVICE_ROLE_KEY" +``` + Now that you understand what JWTs are and where they're used in Supabase, you can explore how to use them in combination with Row Level Security to start restricting access to certain tables, rows, and columns in your Postgres database. ## Resources From 0381676f91c5520a33d979b037033b91161a90cb Mon Sep 17 00:00:00 2001 From: Jan Tennert Date: Wed, 2 Oct 2024 01:57:05 +0200 Subject: [PATCH 11/13] Add Kotlin v3 docs (#29531) * Update docs * add v3 docs * add note about kotlin targets * Apply suggestions from code review Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/spec/supabase_kt_v3.yml Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> --------- Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> --- README.md | 2 +- .../NavigationMenu.constants.ts | 13 +- .../NavigationMenu/NavigationMenu.tsx | 6 + .../getting-started/tutorials/with-kotlin.mdx | 2 +- .../storage/serving/image-transformations.mdx | 40 +- .../storage/uploads/standard-uploads.mdx | 30 +- apps/docs/content/navigation.references.ts | 6 +- apps/docs/docs/ref/kotlin/installing.mdx | 13 +- apps/docs/docs/ref/kotlin/introduction.mdx | 2 +- apps/docs/docs/ref/kotlin/v2/installing.mdx | 394 ++ apps/docs/docs/ref/kotlin/v2/introduction.mdx | 13 + apps/docs/layouts/MainSkeleton.tsx | 4 + .../spec/common-client-libs-sections.json | 9 +- apps/docs/spec/supabase_kt_v3.yml | 5535 +++++++++++++++++ packages/common/constants/client-libraries.ts | 4 +- 15 files changed, 6036 insertions(+), 37 deletions(-) create mode 100644 apps/docs/docs/ref/kotlin/v2/installing.mdx create mode 100644 apps/docs/docs/ref/kotlin/v2/introduction.mdx create mode 100644 apps/docs/spec/supabase_kt_v3.yml diff --git a/README.md b/README.md index 8cc82c6f339..437878278b6 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i Kotlin supabase-kt postgrest-kt - gotrue-kt + auth-kt realtime-kt storage-kt functions-kt diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index e0bdc633be9..f68bda1f992 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -2260,6 +2260,17 @@ export const reference_kotlin_v2 = { }, } +export const reference_kotlin_v3 = { + icon: 'reference-kotlin', + title: 'kotlin', + url: 'guides/reference/kotlin', + parent: '/reference', + pkg: { + name: '@supabase-community/supabase-kt', + repo: 'https://github.com/supabase-community/supabase-kt', + }, +} + export const reference_cli = { icon: 'reference-cli', title: 'Supabase CLI', @@ -2352,7 +2363,7 @@ export const references = [ }, { label: 'supabase-kt', - versions: ['v2', 'v1'], + versions: ['v3', 'v2', 'v1'], description: 'something about the reference', icon: '/docs/img/icons/kotlin-icon.svg', url: '/reference/kotlin/start', diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx index 272443a3498..75f1e55df12 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx @@ -30,6 +30,7 @@ enum MenuId { RefSwiftV2 = 'reference_swift_v2', RefKotlinV1 = 'reference_kotlin_v1', RefKotlinV2 = 'reference_kotlin_v2', + RefKotlinV3 = 'reference_kotlin_v3', RefCli = 'reference_cli', RefApi = 'reference_api', SelfHostingAuth = 'reference_self_hosting_auth', @@ -166,6 +167,11 @@ const menus: Menu[] = [ { id: MenuId.RefKotlinV2, type: 'reference', + path: '/reference/kotlin/v2', + }, + { + id: MenuId.RefKotlinV3, + type: 'reference', path: '/reference/kotlin', }, { diff --git a/apps/docs/content/guides/getting-started/tutorials/with-kotlin.mdx b/apps/docs/content/guides/getting-started/tutorials/with-kotlin.mdx index 8a659320906..513a651444d 100644 --- a/apps/docs/content/guides/getting-started/tutorials/with-kotlin.mdx +++ b/apps/docs/content/guides/getting-started/tutorials/with-kotlin.mdx @@ -80,7 +80,7 @@ In the `build.gradle` (app) file, add these dependencies then press "Sync now." ```kotlin implementation "io.github.jan-tennert.supabase:postgrest-kt:$supabase_version" implementation "io.github.jan-tennert.supabase:storage-kt:$supabase_version" -implementation "io.github.jan-tennert.supabase:gotrue-kt:$supabase_version" +implementation "io.github.jan-tennert.supabase:auth-kt:$supabase_version" implementation "io.ktor:ktor-client-android:$ktor_version" implementation "io.ktor:ktor-client-core:$ktor_version" implementation "io.ktor:ktor-utils:$ktor_version" diff --git a/apps/docs/content/guides/storage/serving/image-transformations.mdx b/apps/docs/content/guides/storage/serving/image-transformations.mdx index c35546b4bf9..b09ef5639ad 100644 --- a/apps/docs/content/guides/storage/serving/image-transformations.mdx +++ b/apps/docs/content/guides/storage/serving/image-transformations.mdx @@ -213,13 +213,17 @@ let data = try await supabase.storage.from("bucket") ```kotlin val data = supabase.storage.from("bucket").downloadAuthenticated("image.jpg") { - size(800, 300) + transform { + size(800, 300) + } } //Or on JVM stream directly to a file val file = File("image.jpg") supabase.storage.from("bucket").downloadAuthenticatedTo("image.jpg", file) { - size(800, 300) + transform { + size(800, 300) + } } ``` @@ -308,15 +312,19 @@ let data = try await supabase.storage.from("bucket") ```kotlin val data = supabase.storage.from("bucket").downloadAuthenticated("image.jpg") { - size(200, 200) - format = "origin" + transform { + size(200, 200) + format = ImageTransformation.Format.ORIGIN + } } //Or on JVM stream directly to a file val file = File("image.jpg") supabase.storage.from("bucket").downloadAuthenticatedTo("image.jpg", file) { - size(200, 200) - format = "origin" + transform { + size(200, 200) + format = ImageTransformation.Format.ORIGIN + } } ``` @@ -431,13 +439,17 @@ let data = try await supabase.storage.from("bucket") ```kotlin val data = supabase.storage["bucket"].downloadAuthenticated("image.jpg") { - quality = 50 + transform { + quality = 50 + } } //Or on JVM stream directly to a file val file = File("image.jpg") supabase.storage["bucket"].downloadAuthenticatedTo("image.jpg", file) { - quality = 50 + transform { + quality = 50 + } } ``` @@ -529,15 +541,19 @@ let data = try await supabase.storage.from("bucket") ```kotlin val data = supabase.storage.from("bucket").downloadAuthenticated("image.jpg") { - size(800, 300) - resize = ImageTransformation.Resize.CONTAIN + transform { + size(800, 300) + resize = ImageTransformation.Resize.CONTAIN + } } //Or on JVM stream directly to a file val file = File("image.jpg") supabase.storage.from("bucket").downloadAuthenticatedTo("image.jpg", file) { - size(800, 300) - resize = ImageTransformation.Resize.CONTAIN + transform { + size(800, 300) + resize = ImageTransformation.Resize.CONTAIN + } } ``` diff --git a/apps/docs/content/guides/storage/uploads/standard-uploads.mdx b/apps/docs/content/guides/storage/uploads/standard-uploads.mdx index 9d383cb1e9f..255e01155a6 100644 --- a/apps/docs/content/guides/storage/uploads/standard-uploads.mdx +++ b/apps/docs/content/guides/storage/uploads/standard-uploads.mdx @@ -70,18 +70,10 @@ try await supabase.storage.from("bucket_name").upload(path: "file_path", file: f ```kotlin -val supabase = createSupabaseClient(supabaseUrl, supabaseKey) { - install(Storage) -} - -suspend fun uploadData(bytes: ByteArray) { - supabase.storage.from("bucket_name").upload("file_path", bytes) -} +supabase.storage.from("bucket_name").upload("file_path", bytes) //Or on JVM/Android: (This will stream the data from the file to supabase) -suspend fun uploadFile(file: File) { - supabase.storage.from("bucket_name").upload("file_path", file) -} +supabase.storage.from("bucket_name").upload("file_path", file) ``` @@ -147,6 +139,15 @@ try await supabase.storage.from("bucket_name") ) ``` + + + +```kotlin +supabase.storage.from("bucket_name").upload("file_path", bytes) { + upsert = true +} +``` + @@ -214,6 +215,15 @@ try await supabase.storage.from("bucket_name") ) ``` + + + +```kotlin +supabase.storage.from("bucket_name").upload("file_path", bytes) { + contentType = ContentType.Image.JPEG +} +``` + diff --git a/apps/docs/content/navigation.references.ts b/apps/docs/content/navigation.references.ts index 49d144b20f2..e3b02e54ab0 100644 --- a/apps/docs/content/navigation.references.ts +++ b/apps/docs/content/navigation.references.ts @@ -77,9 +77,13 @@ export const REFERENCES = { name: 'Kotlin', library: 'supabase-kt', libPath: 'kotlin', - versions: ['v2', 'v1'], + versions: ['v3', 'v2', 'v1'], icon: 'reference-kotlin', meta: { + v3: { + libId: 'reference_kotlin_v3', + specFile: 'supabase_kt_v3', + }, v2: { libId: 'reference_kotlin_v2', specFile: 'supabase_kt_v2', diff --git a/apps/docs/docs/ref/kotlin/installing.mdx b/apps/docs/docs/ref/kotlin/installing.mdx index 80d450bbba5..c3eb71b2374 100644 --- a/apps/docs/docs/ref/kotlin/installing.mdx +++ b/apps/docs/docs/ref/kotlin/installing.mdx @@ -21,13 +21,15 @@ custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supab Add dependency to your build file using the BOM. The available modules are: - - [**gotrue-kt**](https://github.com/supabase-community/supabase-kt/tree/master/GoTrue) + - [**auth-kt**](https://github.com/supabase-community/supabase-kt/tree/master/Auth) - [**realtime-kt**](https://github.com/supabase-community/supabase-kt/tree/master/Realtime) - [**storage-kt**](https://github.com/supabase-community/supabase-kt/tree/master/Storage) - [**functions-kt**](https://github.com/supabase-community/supabase-kt/tree/master/Functions) - [**postgrest-kt**](https://github.com/supabase-community/supabase-kt/tree/master/Postgrest) - Other plugins also available [here](https://github.com/supabase-community/supabase-kt/tree/master/plugins) + Checkout the different READMEs for information about supported Kotlin targets. + io.github.jan-tennert.supabase - gotrue-kt + auth-kt io.github.jan-tennert.supabase @@ -92,7 +94,8 @@ custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supab You can find a list of engines [here](https://ktor.io/docs/http-client-engines.html) - Note that not all Ktor engines support Websockets. So if you plan to use the Realtime module, make sure to use an engine that supports Websockets. Checkout the [engine limitations](https://ktor.io/docs/client-engines.html#limitations) for more information. + - Note that not all Ktor engines support Websockets. So if you plan to use the Realtime module, make sure to use an engine that supports Websockets. Checkout the [engine limitations](https://ktor.io/docs/client-engines.html#limitations) for more information. + - If using `supabase-kt` 3.0.0 and above, you need to use Ktor version 3.0.0-rc-1 or later. diff --git a/apps/docs/docs/ref/kotlin/introduction.mdx b/apps/docs/docs/ref/kotlin/introduction.mdx index 602ed0284ee..64839eb07ad 100644 --- a/apps/docs/docs/ref/kotlin/introduction.mdx +++ b/apps/docs/docs/ref/kotlin/introduction.mdx @@ -2,7 +2,7 @@ This reference documents every object and method available in Supabase's Kotlin To see supported Kotlin targets, check the corresponding module README on [GitHub](https://github.com/supabase-community/supabase-kt). -To migrate from version 1.4.X to 2.0.0, see the [migration guide](https://github.com/supabase-community/supabase-kt/blob/master/MIGRATION.md) +To migrate from version 2.X to 3.0.0, see the [migration guide](https://github.com/supabase-community/supabase-kt/blob/master/MIGRATION.md) diff --git a/apps/docs/docs/ref/kotlin/v2/installing.mdx b/apps/docs/docs/ref/kotlin/v2/installing.mdx new file mode 100644 index 00000000000..80d450bbba5 --- /dev/null +++ b/apps/docs/docs/ref/kotlin/v2/installing.mdx @@ -0,0 +1,394 @@ +--- +id: installing +title: 'Installing & Initialization' +slug: installing +custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supabase.yml +--- + +### Add one or more modules to your project + + + + + + Add dependency to your build file using the BOM. + + The available modules are: + - [**gotrue-kt**](https://github.com/supabase-community/supabase-kt/tree/master/GoTrue) + - [**realtime-kt**](https://github.com/supabase-community/supabase-kt/tree/master/Realtime) + - [**storage-kt**](https://github.com/supabase-community/supabase-kt/tree/master/Storage) + - [**functions-kt**](https://github.com/supabase-community/supabase-kt/tree/master/Functions) + - [**postgrest-kt**](https://github.com/supabase-community/supabase-kt/tree/master/Postgrest) + - Other plugins also available [here](https://github.com/supabase-community/supabase-kt/tree/master/plugins) + + + + + + + ```kotlin + implementation(platform("io.github.jan-tennert.supabase:bom:VERSION")) + implementation("io.github.jan-tennert.supabase:postgrest-kt") + implementation("io.github.jan-tennert.supabase:gotrue-kt") + implementation("io.github.jan-tennert.supabase:realtime-kt") + ``` + + + + + ```groovy + implementation platform("io.github.jan-tennert.supabase:bom:VERSION") + implementation 'io.github.jan-tennert.supabase:postgrest-kt' + implementation 'io.github.jan-tennert.supabase:gotrue-kt' + implementation 'io.github.jan-tennert.supabase:realtime-kt' + ``` + + + + + ```xml + + io.github.jan-tennert.supabase + bom + VERSION + pom + import + + + io.github.jan-tennert.supabase + postgrest-kt + + + io.github.jan-tennert.supabase + gotrue-kt + + + io.github.jan-tennert.supabase + realtime-kt + + ``` + + + + + + + +### Add Ktor Client Engine to each of your Kotlin targets (required) + + + + + You can find a list of engines [here](https://ktor.io/docs/http-client-engines.html) + Note that not all Ktor engines support Websockets. So if you plan to use the Realtime module, make sure to use an engine that supports Websockets. Checkout the [engine limitations](https://ktor.io/docs/client-engines.html#limitations) for more information. + + + + + + + ```kotlin + implementation("io.ktor:ktor-client-[engine]:KTOR_VERSION") + ``` + + + + + ```groovy + implementation 'io.ktor:ktor-client-[engine]:KTOR_VERSION' + ``` + + + + + ```xml + + io.ktor + ktor-client-[engine] + KTOR_VERSION + + ``` + + + + + + + + Multiplatform example: + + + + + + + + ```kotlin + val commonMain by getting { + dependencies { + //supabase modules + } + } + val jvmMain by getting { + dependencies { + implementation("io.ktor:ktor-client-cio:KTOR_VERSION") + } + } + val androidMain by getting { + dependsOn(jvmMain) + } + val jsMain by getting { + dependencies { + implementation("io.ktor:ktor-client-js:KTOR_VERSION") + } + } + val iosMain by getting { + dependencies { + implementation("io.ktor:ktor-client-darwin:KTOR_VERSION") + } + } + ``` + + + + + + + + +### Serialization + +supabase-kt provides several different ways to encode and decode your custom objects. +By default, [KotlinX Serialization](https://github.com/Kotlin/kotlinx.serialization) is used. + + + + + Use [KotlinX Serialization](https://github.com/Kotlin/kotlinx.serialization). + + + + + + + + ```kotlin + plugins { + kotlin("plugin.serialization") version "KOTLIN_VERSION" + } + ``` + + + + + ```groovy + plugins { + id 'org.jetbrains.kotlin.plugin.serialization' version 'KOTLIN_VERSION' + } + ``` + + + + + ```xml + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + + + kotlinx-serialization + + + + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} + + + + + + ``` + + + + ```kotlin + val supabase = createSupabaseClient(supabaseUrl, supabaseKey) { + //Already the default serializer, but you can provide a custom Json instance (optional): + defaultSerializer = KotlinXSerializer(Json { + //apply your custom config + }) + } + ``` + + + + + + + Use [Moshi](https://github.com/square/moshi). + + + + + + + + ```kotlin + implementation("io.github.jan-tennert.supabase:serializer-moshi:VERSION") + ``` + + + + + ```groovy + implementation 'io.github.jan-tennert.supabase:serializer-moshi:VERSION' + ``` + + + + + ```xml + + io.github.jan-tennert.supabase + serializer-moshi + VERSION + + ``` + + + + ```kotlin + val supabase = createSupabaseClient(supabaseUrl, supabaseKey) { + defaultSerializer = MoshiSerializer() + } + ``` + + + + + + + + Use [Jackson](https://github.com/FasterXML/jackson-module-kotlin). + + + + + + + + ```kotlin + implementation("io.github.jan-tennert.supabase:serializer-jackson:VERSION") + ``` + + + + + ```groovy + implementation 'io.github.jan-tennert.supabase:serializer-jackson:VERSION' + ``` + + + + + ```xml + + io.github.jan-tennert.supabase + serializer-jackson + VERSION + + ``` + + + + ```kotlin + val supabase = createSupabaseClient(supabaseUrl, supabaseKey) { + defaultSerializer = JacksonSerializer() + } + ``` + + + + + + + + Use custom serializer. + + + + + ```kotlin + class CustomSerializer: SupabaseSerializer { + + override fun encode(type: KType, value: T): String { + //encode value to string + } + + override fun decode(type: KType, value: String): T { + //decode value + } + + } + ``` + + ```kotlin + val supabase = createSupabaseClient(supabaseUrl, supabaseKey) { + defaultSerializer = CustomSerializer() + } + ``` + + + + diff --git a/apps/docs/docs/ref/kotlin/v2/introduction.mdx b/apps/docs/docs/ref/kotlin/v2/introduction.mdx new file mode 100644 index 00000000000..602ed0284ee --- /dev/null +++ b/apps/docs/docs/ref/kotlin/v2/introduction.mdx @@ -0,0 +1,13 @@ +This reference documents every object and method available in Supabase's Kotlin Multiplatform library, [supabase-kt](https://github.com/supabase-community/supabase-kt). You can use supabase-kt to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files. + +To see supported Kotlin targets, check the corresponding module README on [GitHub](https://github.com/supabase-community/supabase-kt). + +To migrate from version 1.4.X to 2.0.0, see the [migration guide](https://github.com/supabase-community/supabase-kt/blob/master/MIGRATION.md) + + + +The Kotlin client library is created and maintained by the Supabase community, and is not an official library. Please be tolerant of areas where the library is still being developed, and — as with all the libraries — feel free to contribute wherever you find issues. + +Huge thanks to official maintainer, [jan-tennert](https://github.com/jan-tennert). + + diff --git a/apps/docs/layouts/MainSkeleton.tsx b/apps/docs/layouts/MainSkeleton.tsx index e902413138e..257805a5aec 100644 --- a/apps/docs/layouts/MainSkeleton.tsx +++ b/apps/docs/layouts/MainSkeleton.tsx @@ -123,6 +123,10 @@ const levelsData = { icon: 'reference-kotlin', name: 'Kotlin Reference v2.0', }, + reference_kotlin_v3: { + icon: 'reference-kotlin', + name: 'Kotlin Reference v3.0', + }, reference_cli: { icon: 'reference-cli', name: 'CLI Reference', diff --git a/apps/docs/spec/common-client-libs-sections.json b/apps/docs/spec/common-client-libs-sections.json index ac715b2049f..970e6dec100 100644 --- a/apps/docs/spec/common-client-libs-sections.json +++ b/apps/docs/spec/common-client-libs-sections.json @@ -33,7 +33,8 @@ "reference_swift_v1", "reference_swift_v2", "reference_kotlin_v1", - "reference_kotlin_v2" + "reference_kotlin_v2", + "reference_kotlin_v3" ] }, { @@ -50,7 +51,8 @@ "reference_swift_v1", "reference_swift_v2", "reference_kotlin_v1", - "reference_kotlin_v2" + "reference_kotlin_v2", + "reference_kotlin_v3" ] }, { @@ -989,7 +991,8 @@ "reference_kotlin_v2", "reference_python_v2", "reference_swift_v1", - "reference_swift_v2" + "reference_swift_v2", + "reference_kotlin_v3" ], "items": [ { diff --git a/apps/docs/spec/supabase_kt_v3.yml b/apps/docs/spec/supabase_kt_v3.yml new file mode 100644 index 00000000000..25bafd81cc5 --- /dev/null +++ b/apps/docs/spec/supabase_kt_v3.yml @@ -0,0 +1,5535 @@ +openref: 0.1 + +info: + id: reference/supabase-kotlin + title: Supabase Kotlin Client + description: | + + Supabase Kotlin. + + specUrl: https://github.com/supabase/supabase/edit/master/apps/docs/spec/supabase_kt_v3.yml + slugPrefix: '/' + libraries: + - name: 'Kotlin' + id: 'kt' + version: '0.0.1' + +functions: + - id: initializing + title: 'Initializing' + description: | + ### Create Supabase Client + + Independently of which Supabase module you are using, you will need to initialize the main client first and install the module. + + To create a new client, you can use the `createSupabaseClient` function. + + When installing a module, you can pass a block to configure it. + + ### OAuth and OTP link verification + + [supabase-kt](https://github.com/supabase-community/supabase-kt) provides several platform implementations for OAuth and OTP link verification. + + **On Desktop platforms (JVM, MacOS\*, Linux)**, it uses a HTTP Callback Server to receive the session data from a successful OAuth login. The success page can be customized via `AuthConfig#httpCallbackConfig` \ + \* If no deeplinks are being used. + + *Note: OTP link verification such as sign ups are not supported on JVM. You may have to send a verification token rather than a url in your email. To send the token, rather than a redirect url, change `{{ .ConfirmationURL }}` in your sign up email to `{{ .Token }}`* + + **On Android, iOS & MacOS**, OAuth and OTP verification use deeplinks. Refer to the guide below on how to setup deeplinks. Alternatively you can use [Native Google Auth](/docs/guides/auth/social-login/auth-google?platform=android). \ + **On JS**, it uses the website origin as the callback url. Session importing gets handled automatically. \ + **Windows, tvOS, watchOS & Linux** currently have no default implementation. Feel free to create a PR. + + You always make your own implementation and use `auth.parseSessionFromFragment(fragment)` or `auth.parseSessionFromUrl(url)` to let [supabase-kt](https://github.com/supabase-community/supabase-kt) handle the parsing after receiving a callback. + Then you can simply use `auth.importSession(session)`. + + ### Configure deeplink callbacks for Authentication + + Deeplinks are supported on Android, iOS and MacOS. + 1. **Set up a deeplink** \ + On Android, set up a [deeplink](https://developer.android.com/training/app-links/deep-linking) in your Android manifest. \ + On iOS and MacOS, set up a [url scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). + 2. **Add your deeplink to the [redirect URLs](https://supabase.com/dashboard/project/_/auth/url-configuration)** \ + **Pattern**: scheme://host + 3. **Configure the Auth plugin** + Set the `host` and `scheme` in the Auth config: + ```kotlin + install(Auth) { + host = "deeplink host" // this can be anything, eg. your package name or app/company url (not your Supabase url) + scheme = "deeplink scheme" + + // On Android only, you can set OAuth and SSO logins to open in a custom tab, rather than an external browser: + defaultExternalAuthAction = ExternalAuthAction.CustomTabs() //defaults to ExternalAuthAction.ExternalBrowser + } + ``` + 4. **Call platform specific function on startup** \ + On Android: `supabase.handleDeeplinks(intent)` \ + If you don't want a separate activity, just call this function at the top of your `onCreate` function in your MainActivity. \ + On iOS/MacOS: `supabase.handleDeeplinks(url)` + + Then you can log in using OAuth: + ```kotlin + supabase.auth.signInWith(Google) + ``` + Or open OTP links directly in your app. + + ### PKCE Authentication flow + supabase-kt supports the [PKCE authentication flow](https://supabase.com/blog/supabase-auth-sso-pkce). + To use it, change the `flowType` in the Auth configuration: + ```kotlin + install(Auth) { + flowType = FlowType.PKCE + } + ``` + That's it! If you already implemented deeplinks to handle OTPs and OAuth you don't have to change anything! + params: + - name: supabaseUrl + isOptional: false + type: String + description: The unique Supabase URL which is supplied when you create a new project in your project dashboard. + - name: supabaseKey + isOptional: false + type: String + description: The unique Supabase Key which is supplied when you create a new project in your project dashboard. + - name: builder + isOptional: true + type: SupabaseClientBuilder.() -> Unit + description: Apply additional configuration and install plugins. + subContent: + - name: useHTTPS + isOptional: true + type: Boolean + description: Whether to use HTTPS for network requests. Will be set automatically depending on your Supabase url, but can be changed manually. + - name: httpEngine + isOptional: true + type: HttpClientEngine? + description: Custom Ktor Client engine. Shouldn't be set manually as Ktor uses an engine from your dependencies, but can be set to a MockEngine for tests. + - name: ignoreModulesInUrl + isOptional: true + type: Boolean + description: Whether to ignore if [supabaseUrl] contains modules like 'realtime' or 'auth'. If false, an exception will be thrown. Defaults to false. + - name: requestTimeout + isOptional: true + type: Duration + description: Timeout after network requests throw a `HttpRequestTimeoutException`. Defaults to 10 seconds. + - name: defaultLogLevel + isOptional: true + type: LogLevel + description: The default log level used for plugins. Can be overridden per plugin. Defaults to `LogLevel.INFO`. + - name: defaultSerializer + isOptional: true + type: SupabaseSerializer + description: The default serializer used to serialize and deserialize custom data types. Defaults to `KotlinXSerializer`. + + examples: + - id: initialize-client + name: Initialize Client + code: | + ```kotlin + val supabase = createSupabaseClient( + supabaseUrl = "https://xyzcompany.supabase.co", + supabaseKey = "public-anon-key" + ) { + install(Auth) + install(Postgrest) + //install other modules + } + ``` + - id: configure-auth + name: Configure Auth module + code: | + ```kotlin + val supabase = createSupabaseClient( + supabaseUrl = "https://xyzcompany.supabase.co", + supabaseKey = "public-anon-key" + ) { + install(Auth) { + alwaysAutoRefresh = false // default: true + autoLoadFromStorage = false // default: true + //and more... + } + } + ``` + description: | + **Common:** + + `alwaysAutoRefresh` - whether the Auth plugin should always auto refresh expired sessions automatically. Default: `true` + + `autoLoadFromStorage` - whether the Auth plugin should automatically load the session from the session manager. Default: `true` + + `autoSaveToStorage` - whether the Auth plugin should automatically save the session to the session manager. Default: `true` + + `flowType` - Which authentication flow to use. Currently available: FlowType.PKCE and FlowType.IMPLICIT. Default: `FlowType.IMPLICIT` + + `codeVerifierCache` - Interface for saving and loading codes for the PKCE authentication flow. Default: `SettingsCodeVerifierCache` + + `customUrl` - Custom url for the Auth API. Can be safely ignored when using Supabase. Default: `null` + + `jwtToken` - Plugin specific JWT Token. Can be ignored when using the Auth plugin. Default: `null` + + `retryDelay` - Duration after which the Auth plugin should retry a failed session refresh. Default: `10.seconds` + + `sessionManager` - Interface for saving and loading the user session. Default: `SettingsSessionManager` + + **Android & iOS:** + + `scheme` - The scheme for the redirect url, when using deep linking. Default: `supabase` + + `host` - The host for the redirect url, when using deep linking. Default: `login` + + **Android:** + + `enableLifecycleCallbacks` - Whether to stop auto-refresh on focus loss, and resume it on focus again. Default: `true` + + **Desktop:** + + `httpPort`: The port the web server is running on, when logging in with OAuth. Default: `0` (random port). + + `timeout`: The timeout for the web server, when logging in with OAuth. Default: `1.minutes`. + + `htmlTitle`: The title of the redirect page, when logging in with OAuth. Default: `"Supabase Auth"`. + + `htmlText`: The text of the redirect page, when logging in with OAuth. Default: `"Logged in. You may continue in your app."`. + + `htmlIconUrl`: The icon of the redirect page, when logging in with OAuth. Default: `"https://supabase.com/brand-assets/supabase-logo-icon.png"`. + - id: configure-postgrest + name: Configure PostgREST module + code: | + ```kotlin + val supabase = createSupabaseClient( + supabaseUrl = "https://xyzcompany.supabase.co", + supabaseKey = "public-anon-key" + ) { + install(Postgrest) { + defaultSchema = "schema" // default: "public" + propertyConversionMethod = PropertyConversionMethod.SERIAL_NAME // default: PropertyConversionMethod.CAMEL_CASE_TO_SNAKE_CASE + } + } + ``` + description: | + `propertyConversionMethod` - The method to use to convert the property names to the column names when applying filters and using the update method. Default: `PropertyConversionMethod.CAMEL_CASE_TO_SNAKE_CASE` + + `defaultSchema` - The default schema to use for database requests. Default: `public` + + `customUrl` - Custom url for the PostgREST API. Can be safely ignored when using Supabase. Default: `null` + + `jwtToken` - Plugin specific JWT Token. Can be ignored when using the Auth plugin. Default: `null` + - id: configure-storage + name: Configure Storage module + code: | + ```kotlin + val supabase = createSupabaseClient( + supabaseUrl = "https://xyzcompany.supabase.co", + supabaseKey = "public-anon-key" + ) { + install(Storage) { + transferTimeout = 90.seconds // Default: 120 seconds + } + } + ``` + description: | + `transferTimeout` - the timeout for uploading and downloading files. Default: `120.seconds` + + `resumable.cache` - Interface for storing resumable upload urls. Default: `SettingsResumableCache` + + `resumable.defaultChunkSize` - The default chunk size for resumable uploads. Supabase currently only supports a chunk size of 6MB, so be careful when changing this value. Default: `6MB` + + `resumable.retryTimeout` - the timeout for retrying resumable uploads when uploading a chunk fails. Default: `5.seconds` + + `resumable.onlyUpdateStateAfterChunk` - whether the upload state should only be updated after a chunk was uploaded successfully or also when the chunk is currently being uploaded. Default: `false` + + `customUrl` - Custom url for the Storage API. Can be safely ignored when using Supabase. Default: `null` + + `jwtToken` - Plugin specific JWT Token. Can be ignored when using the Auth plugin. Default: `null` + - id: configure-realtime + name: Configure Realtime module + code: | + ```kotlin + val supabase = createSupabaseClient( + supabaseUrl = "https://xyzcompany.supabase.co", + supabaseKey = "public-anon-key" + ) { + install(Realtime) { + reconnectDelay = 5.seconds // Default: 7 seconds + } + } + ``` + description: | + `reconnectDelay` - The delay between reconnect attempts. Default: `7.seconds` + + `heartbeatInterval` - The interval between heartbeat messages. Default: `15.seconds` + + `disconnectOnSessionLoss` - Whether to disconnect from the websocket when the session is lost. Default: `true` + + `secure` - Whether to use wss or ws. Defaults to [SupabaseClient.useHTTPS] when null + + `websocketConfig` - Custom Ktor websocket config + + `customUrl` - Custom url for the Realtime websocket. Can be safely ignored when using Supabase. Default: `null` + + `jwtToken` - Plugin specific JWT Token. Can be ignored when using the Auth plugin. Default: `null` + - id: configure-functions + name: Configure Functions plugin + code: | + ```kotlin + val supabase = createSupabaseClient( + supabaseUrl = "https://xyzcompany.supabase.co", + supabaseKey = "public-anon-key" + ) { + install(Functions) { + //no custom settings + } + } + ``` + description: | + `customUrl` - Custom url for the Functions API. Can be safely ignored when using Supabase. Default: `null` + + `jwtToken` - Plugin specific JWT Token. Can be ignored when using the Auth plugin. Default: `null` + - id: configure-graphql + name: Configure GraphQL plugin + code: | + ```kotlin + val supabase = createSupabaseClient( + supabaseUrl = "https://xyzcompany.supabase.co", + supabaseKey = "public-anon-key" + ) { + install(GraphQL) { + apolloConfiguration { + //custom configuration + } + } + } + ``` + description: | + `apolloConfiguration` - Custom configuration for the ApolloClient + + `customUrl` - Custom url for the GraphQL API. Can be safely ignored when using Supabase. Default: `null` + + `jwtToken` - Plugin specific JWT Token. Can be ignored when using the Auth plugin. Default: `null` + + **You can access the created ApolloClient via `supabase.graphql.apolloClient`, which automatically adds the required headers depending on your session.** + + - id: select + title: 'Fetch data: select()' + notes: | + Perform a SELECT query on the table or view. + - When calling a `decode` method, you have to provide a [serializable class](/docs/reference/kotlin/installing#serialization) as the type parameter. + - You can provide a `Columns` object to select specific columns. + - You can provide a [filter](/docs/reference/kotlin/using-filters) block to filter the results + params: + - name: columns + isOptional: true + type: Columns + description: The columns to retrieve, defaults to `Columns.ALL`. You can also use `Columns.list`, `Columns.type` or `Columns.raw` to specify the columns. + - name: head + isOptional: true + type: Boolean + description: If true, select will delete the selected data. + - name: request + isOptional: true + type: PostgrestRequestBuilder.() -> Unit + description: Additional configuration & filtering for the request. + examples: + - id: getting-your-data + name: Getting your data + isSpotlight: true + code: | + ```kotlin + val city = supabase.from("cities").select().decodeSingle() + ``` + - id: selecting-specific-columns + name: Selecting specific columns + description: You can select specific fields from your tables. + code: | + ```kotlin + val city = supabase.from("cities").select(columns = Columns.list("id, name")).decodeSingle() + ``` + - id: query-foreign-tables + name: Query foreign tables + description: If your database has foreign key relationships, you can query related tables too. + code: | + ```kotlin + val columns = Columns.raw(""" + id, + name, + cities ( + id, + name + ) + """.trimIndent()) + val country = supabase.from("countries") + .select( + columns = columns + ) + .decodeSingle() + ``` + note: | + What about join tables + If your tables are **NOT** directly related, but instead are joined by a _join table_, + you can still use the `select()` method to query the related data. The PostgREST engine detects the relationship automatically. + For more details, [follow the link](https://postgrest.org/en/latest/api.html#embedding-through-join-tables). + - id: query-the-same-foreign-table-multiple-times + name: Query the same foreign table multiple times + description: | + Sometimes you will need to query the same foreign table twice. + In this case, you can use the name of the joined column to identify + which join you intend to use. For convenience, you can also give an + alias for each column. + code: | + ```kotlin + val columns = Columns.raw(""" + content, + from: sender_id(name), + to: receiver_id(name) + """.trimIndent()) + val message = supabase.from("messages") + .select(columns = columns) + .decodeSingle() + ``` + - id: querying-with-count-option + name: Querying with count option + description: | + You can get the number of rows by using the count option. + Allowed values for count option are [Count.EXACT](https://postgrest.org/en/stable/api.html#exact-count), [Count.PLANNED](https://postgrest.org/en/stable/api.html#planned-count) and [Count.ESTIMATED](https://postgrest.org/en/stable/api.html#estimated-count). + code: | + ```kotlin + val count = supabase.from("countries") + .select { + count(Count.EXACT) + } + .count()!! + ``` + - id: querying-json-data + name: Querying JSON data + description: | + If you have data inside of a JSONB column, you can apply select + and query filters to the data values. Postgres offers a + [number of operators](https://www.postgresql.org/docs/current/functions-json.html) + for querying JSON data. Also see + [PostgREST docs](http://postgrest.org/en/v7.0.0/api.html#json-columns) for more details. + code: | + ```kotlin + val columns = Columns.raw(""" + id, name + address->city + """.trimIndent()) + val user = supabase.from("users") + .select(columns = columns) + .decodeSingle() + ``` + + - id: insert + title: 'Create data: insert()' + $ref: '@supabase/postgrest-js."lib/PostgrestQueryBuilder".PostgrestQueryBuilder.insert' + notes: | + Perform an INSERT into the table or view. + - When calling an `insert` method, you have to provide a [serializable value](/docs/reference/kotlin/installing#serialization). + - By default, `insert` will not return the inserted data. If you want to return the inserted data, you can use the `select()` method inside the request. + params: + - name: value # This function has two signatures: "value: T" and "values: List" + isOptional: false + type: T or List + description: The value(s) you want to insert. `T` can be any serializable type. + - name: request + isOptional: true + type: PostgrestRequestBuilder.() -> Unit + description: Additional configuration & filtering for the request. + + examples: + - id: create-a-record + name: Create a record + isSpotlight: true + code: | + ```kotlin + val city = City(name = "The Shire", countryId = 554) + supabase.from("cities").insert(city) + ``` + - id: create-a-record-and-return + name: Create a record and return it + isSpotlight: true + code: | + ```kotlin + val city = City(name = "The Shire", countryId = 554) + val result = supabase.from("cities").insert(city) { + select() + }.decodeSingle() + ``` + - id: bulk-create + name: Bulk create + description: | + When running a bulk create, the operation is handled in a single transaction. If any of the inserts fail, all other operations are + rolled back. + code: | + ```kotlin + val theShire = City(name = "The Shire", countryId = 554) + val rohan = City(name = "Rohan", countryId = 554) + supabase.from("cities").insert(listOf(theShire, rohan)) + ``` + + - id: update + title: 'Modify data: update()' + notes: | + Perform an UPDATE on the table or view. + - `update()` should always be combined with a [filter](/docs/reference/kotlin/using-filters) block to avoid updating all records. + - When calling `insert` or `update`, you have to provide a [serializable value](/docs/reference/kotlin/installing#serialization) in the function parameter. + - By default, `update` will not return the inserted data. If you want to return the inserted data, you can use the `select()` method inside the request. + params: + - name: value + type: T or PostgrestUpdate.() -> Unit = {} + isOptional: false + description: The new value, can be either a serializable value or PostgrestUpdate DSL where you can set new values per column. + - name: request + isOptional: true + type: PostgrestRequestBuilder.() -> Unit + description: Additional configuration & filtering for the request. + examples: + - id: updating-your-data + name: Updating your data + isSpotlight: true + code: | + ```kotlin + supabase.from("countries").update( + { + Country::name setTo "Australia" + //or + set("name", "Australia") + } + ) { + filter { + Country::id eq 1 + //or + eq("id", 1) + } + } + ``` + - id: update-a-record-and-return-it + name: Update a record and return it + code: | + ```kotlin + val newCountry = supabase.from("countries").update( + { + Country::name setTo "Australia" + //or + set("name", "Australia") + } + ) { + select() + filter { + Country::id eq 1 + //or + eq("id", 1) + } + }.decodeSingle() + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Australia'); + ``` + response: | + ```json + { + "data": [ + { + "id": 1, + "name": "Australia" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + hideCodeBlock: true + - id: updating-json-data + name: Updating JSON data + description: | + Postgres offers a + [number of operators](https://www.postgresql.org/docs/current/functions-json.html) + for working with JSON data. Right now it is only possible to update an entire JSON document, + but we are [working on ideas](https://github.com/PostgREST/postgrest/issues/465) for updating individual keys. + code: | + ```kotlin + val address = Address(street = "Melrose Place", postcode = 90210) + supabase.from("users").update( + { + User::address setTo address + } + ) { + filter { + eq("address->postcode", 90210) + } + } + ``` + + - id: upsert + title: 'Upsert data: upsert()' + $ref: '@supabase/postgrest-js."lib/PostgrestQueryBuilder".PostgrestQueryBuilder.upsert' + notes: | + Perform an UPSERT on the table or view. Depending on the column(s) passed to `onConflict`, `.upsert()` allows you to perform the equivalent of `.insert()` if a row with the corresponding `onConflict` columns doesn't exist, or if it does exist, perform an alternative action depending on `ignoreDuplicates`. + - Primary keys should be included in the data payload in order for an update to work correctly. + - Primary keys must be natural, not surrogate. There are however, [workarounds](https://github.com/PostgREST/postgrest/issues/1118) for surrogate primary keys. + - If you need to insert new data and update existing data at the same time, use [Postgres triggers](https://github.com/supabase/postgrest-js/issues/173#issuecomment-825124550). + - When calling `insert` or `update`, you have to provide a [serializable value](/docs/reference/kotlin/installing#serialization) in the function parameter. + - By default, `upsert` will not return the inserted data. If you want to return the inserted data, you can use the `select()` method inside the request. + params: + - name: value # This function has two signatures: "value: T" and "values: List" + isOptional: false + type: T or List + description: The value(s) you want to insert. `T` can be any serializable type. + - name: request + isOptional: true + type: PostgrestRequestBuilder.() -> Unit + description: Additional configuration & filtering for the request. + subParams: + - name: onConflict + isOptional: true + type: String? + description: Comma-separated UNIQUE column(s) to specify how duplicate rows are determined. Two rows are duplicates if all the `onConflict` columns are equal. + - name: defaultToNull + isOptional: true + type: Boolean + description: Make missing fields default to `null`. Otherwise, use the default value for the column. This only applies when inserting new rows, not when merging with existing rows under + - name: ignoreDuplicates + isOptional: true + type: Boolean + description: If `true`, duplicate rows are ignored. If `false`, duplicate rows are merged with existing rows. + examples: + - id: upsert-your-data + name: Upsert your data + isSpotlight: true + code: | + ```kotlin + val toUpsert = Message(id = 3, message = "foo", username = "supabot") + supabase.from("messages").upsert(toUpsert) + ``` + - id: upsert-your-data-and-return + name: Upsert your data and return it + isSpotlight: true + code: | + ```kotlin + val toUpsert = Message(id = 3, message = "foo", username = "supabot") + val message = supabase.from("messages").upsert(toUpsert) { + select() + }.decodeSingle() + ``` + - id: upserting-into-tables-with-constraints + name: Upserting into tables with constraints + description: | + Running the following will cause Supabase to upsert data into the `users` table. + If the username 'supabot' already exists, the `onConflict` argument tells Supabase to overwrite that row + based on the column passed into `onConflict`. + isSpotlight: true + code: | + ```kotlin + val toUpsert = User(username = "supabot") + supabase.from("users").upsert(toUpsert) { + onConflict = "username" + } + ``` + - id: upsert-return-row-count + name: Return the exact number of rows + isSpotlight: true + code: | + ```kotlin + val toUpsert = User(username = "supabot") + val count = supabase.from("users").upsert(toUpsert) { + count(Count.EXACT) + onConflict = "username" + }.count() + ``` + + - id: delete + title: 'Delete data: delete()' + $ref: '@supabase/postgrest-js."lib/PostgrestQueryBuilder".PostgrestQueryBuilder.delete' + notes: | + Perform a DELETE on the table or view. + - `delete()` should always be combined with a [filter](/docs/reference/kotlin/using-filters) block to target the item(s) you wish to delete. + - If you use `delete()` with filters and you have + [RLS](/docs/learn/auth-deep-dive/auth-row-level-security) enabled, only + rows visible through `SELECT` policies are deleted. Note that by default + no rows are visible, so you need at least one `SELECT`/`ALL` policy that + makes the rows visible. + - By default, `delete` will not return the deleted data. If you want to return the deleted data, you can use the `select()` method inside the request. + params: + - name: request + isOptional: true + type: PostgrestRequestBuilder.() -> Unit + description: Additional configuration & filtering for the request. + examples: + - id: delete-records + name: Delete records + isSpotlight: true + code: | + ```kotlin + supabase.from("cities").delete { + filter { + City::id eq 666 + //or + eq("id", 666) + } + } + ``` + - id: fetch-delete-records + name: Fetch deleted records + code: | + ```kotlin + val deletedCity = supabase.from("cities").delete { + select() + filter { + City::id eq 666 + //or + eq("id", 666) + } + }.decodeSingle() + ``` + - id: rpc + title: 'Stored Procedures: rpc()' + description: | + You can call stored procedures as a "Remote Procedure Call". + + That's a fancy way of saying that you can put some logic into your database then call it from anywhere. + It's especially useful when the logic rarely changes - like password resets and updates. + + - When calling `rpc` with parameters, you have to provide a [serializable value](/docs/reference/kotlin/installing#serialization) in the function parameter. + params: + - name: function + isOptional: false + type: String + description: The name of the function + - name: parameters + isOptional: true # Not directly `optional`, as there are two overloads one with and one without `parameters`. + type: T + description: Parameters to pass to the function. T can be any serializable type. + - name: request + isOptional: true + type: PostgrestRequestBuilder.() -> Unit + description: Additional configuration & filtering for the request. + subParams: + - name: method + isOptional: true + type: RpcMethod + description: The HTTP method to use. Defaults to `RpcMethod.POST` + - name: schema + isOptional: true + type: String + description: The schema to use for the function. Defaults to `public` + examples: + - id: call-a-stored-procedure + name: Call a stored procedure + isSpotlight: true + description: This is an example invoking a stored procedure. + code: | + ```kotlin + supabase.postgrest.rpc("hello_world") + ``` + - id: with-parameters + name: With Parameters + code: | + ```kotlin + val rpcParams = City(name = "The Shire") + supabase.postgrest.rpc("echo_city", rpcParams) + ``` + - id: using-filters + title: Using Filters + description: | + Filters allow you to only return rows that match certain conditions. + + Filters can be used on `select()`, `update()`, and `delete()` queries. + + You can use two different types for applying filters: + ```kotlin + eq("country_id", 1) + ``` + And using a class property: + ```kotlin + City::countryId eq 1 + ``` + + As you can see on the property syntax: + the name of the `countryId` gets converted to `country_id`. + + By default, this is done by converting camel case to snake case, but you can customize this by changing the `propertyConversionMethod` in the Postgrest Config + + If a database function returns a table response, you can also apply filters. + examples: + - id: applying-filters + name: Applying a filter block + description: | + Filters can be applied on any of these functions: `select()`, `update()`, `upsert()`, + `delete()`, and `rpc()` + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name", "country_id")) { + filter { + City::name eq "The Shire" + //or + eq("name", "The Shire") + } + } + ``` + - id: multiple-filters + name: Multiple filters on one column + description: | + Filters can be applied on any of these functions: `select()`, `update()`, `upsert()`, + `delete()`, and `rpc()` + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name, country_id")) { + filter { + and { //when both are true + City::population gt 40000 + City::population lt 700000 + } + or { //when either one of the filters are true + City::name eq "London" + City::name eq "Berlin" + } + } + } + ``` + - id: filter-by-value-within-json-column + name: Filter by values within a JSON column + description: | + Filters can be built up one step at a time and then executed. For example: + data: + sql: | + ```sql + create table + users ( + id int8 primary key, + name text, + address jsonb + ); + + insert into + users (id, name, address) + values + (1, 'Michael', '{ "postcode": 90210 }'), + (2, 'Jane', null); + ``` + response: | + ```json + { + "data": [ + { + "id": 1, + "name": "Michael", + "address": { + "postcode": 90210 + } + } + ], + "status": 200, + "statusText": "OK" + } + ``` + code: | + ```kotlin + supabase.from("users").select { + filter { + eq("address->postcode", 90210) + } + } + ``` + - id: filter-foreign-tables + name: Filter Foreign Tables + code: | + ```kotlin + val columns = Columns.raw(""" + name, + cities!inner ( + name + ) + """.trimIndent()) + supabase.from("countries").select( + columns = columns + ) { + filter { + eq("cities.name", "Bali") + } + } + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 2, 'Bali'), + (2, 1, 'Munich'); + ``` + response: | + ```json + { + "data": [ + { + "name": "Indonesia", + "cities": [ + { + "name": "Bali" + } + ] + } + ], + "status": 200, + "statusText": "OK" + } + ``` + - id: or + title: or() + description: | + Finds all rows satisfying at least one of the filters. + params: + - name: negate + isOptional: true + type: Boolean + description: If true, negate the entire block. + - name: block + isOptional: false + type: PostgrestFilterBuilder.() -> Unit + description: The block to apply the `or` filter to. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("countries").select(columns = Columns.list("name")) { + filter { + or { + Country::id eq 2 + Country::name eq "Algeria" + //or + eq("id", 2) + eq("name", "Algeria") + } + } + } + ``` + - id: use-or-with-and + name: Use `or` with `and` + code: | + ```kotlin + supabase.from("countries").select(columns = Columns.list("name")) { + filter { + or { + Country::id gt 3 + and { + Country::id eq 1 + Country::name eq "Afghanistan" + } + } + } + } + ``` + + - id: not + title: filterNot() + description: | + Finds all rows that don't satisfy the filter. + notes: | + - `.filterNot()` expects you to use the raw [PostgREST syntax](https://postgrest.org/en/stable/api.html#horizontal-filtering-rows) for the filter names and values. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: operator + isOptional: false + type: FilterOperator + description: The operator to use for the filter. + - name: value + isOptional: false + type: Any + description: The value to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("countries").select { + filter { + filterNot("name", FilterOperation.IS, "") + } + } + ``` + + - id: eq + title: eq() + description: | + Finds all rows whose value on the stated `column` exactly matches the specified `value`. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: value + isOptional: false + type: Any + description: The value to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name", "country_id")) { + filter { + City::name eq "The Shire" + //or + eq("name", "The Shire") + } + } + ``` + - id: with-update + name: With `update()` + code: | + ```kotlin + val toUpdate = City(name = "Mordor") + supabase.from("cities").update(toUpdate) { + filter { + City::name eq "San Francisco" + //or + eq("name", "San Francisco") + } + } + ``` + - id: with-delete + name: With `delete()` + code: | + ```kotlin + supabase.from("cities").delete { + filter { + City::name eq "Mordor" + //or + eq("name", "Mordor") + } + } + ``` + - id: with-rpc + name: With `rpc()` + code: | + ```kotlin + supabase.postgrest.rpc("function") { + filter { + City::name eq "Mordor" + //or + eq("name", "Mordor") + } + } + ``` + + - id: neq + title: neq() + description: | + Finds all rows whose value on the stated `column` doesn't match the specified `value`. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: value + isOptional: false + type: Any + description: The value to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name", "country_id")) { + filter { + City::name neq "The Shire" + //or + neq("name", "The Shire") + } + } + ``` + - id: with-update + name: With `update()` + code: | + ```kotlin + val toUpdate = City(name = "Mordor") + supabase.from("cities").update(toUpdate) { + filter { + City::name neq "The Shire" + //or + neq("name", "The Shire") + } + } + ``` + - id: with-delete + name: With `delete()` + code: | + ```kotlin + supabase.from("cities").delete { + filter { + City::name neq "The Shire" + //or + neq("name", "The Shire") + } + } + ``` + - id: with-rpc + name: With `rpc()` + code: | + ```kotlin + supabase.rpc("echo_all_cities") { + filter { + neq("address->postcode", 90210) + } + } + ``` + + - id: gt + title: gt() + description: | + Finds all rows whose value on the stated `column` is greater than the specified `value`. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: value + isOptional: false + type: Any + description: The value to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name")) { + filter { + City::countryId gt 300 + //or + gt("country_id", 300) + } + } + ``` + - id: with-update + name: With `update()` + code: | + ```kotlin + val toUpdate = City(name = "Mordor") + supabase.from("cities").update(toUpdate) { + filter { + City::countryId gt 300 + //or + gt("country_id", 300) + } + } + ``` + - id: with-delete + name: With `delete()` + code: | + ```kotlin + supabase.from("cities").delete { + filter { + City::countryId gt 300 + //or + gt("country_id", 300) + } + } + ``` + - id: with-rpc + name: With `rpc()` + code: | + ```kotlin + supabase.postgrest.rpc("echo_all_cities") { + filter { + City::countryId gt 300 + //or + gt("country_id", 300) + } + } + ``` + + - id: gte + title: gte() + description: | + Finds all rows whose value on the stated `column` is greater than or equal to the specified `value`. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: value + isOptional: false + type: Any + description: The value to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name")) { + filter { + City::countryId gte 300 + //or + gte("country_id", 300) + } + } + ``` + - id: with-update + name: With `update()` + code: | + ```kotlin + val toUpdate = City(name = "Mordor") + supabase.from("cities").update(toUpdate) { + filter { + City::countryId gte 300 + //or + gte("country_id", 300) + } + } + ``` + - id: with-delete + name: With `delete()` + code: | + ```kotlin + supabase.from("cities").delete { + filter { + City::countryId gte 300 + //or + gte("country_id", 300) + } + } + ``` + - id: with-rpc + name: With `rpc()` + code: | + ```kotlin + supabase.postgrest.rpc("echo_all_cities") { + filter { + City::countryId gte 300 + //or + gte("country_id", 300) + } + } + ``` + + - id: lt + title: lt() + description: | + Finds all rows whose value on the stated `column` is less than the specified `value`. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: value + isOptional: false + type: Any + description: The value to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name")) { + filter { + City::countryId lt 300 + //or + lt("country_id", 300) + } + } + ``` + - id: with-update + name: With `update()` + code: | + ```kotlin + val toUpdate = City(name = "Mordor") + supabase.from("cities").update(toUpdate) { + filter { + City::countryId lt 300 + //or + lt("country_id", 300) + } + } + ``` + - id: with-delete + name: With `delete()` + code: | + ```kotlin + supabase.from("cities").delete { + filter { + City::countryId lt 300 + //or + lt("country_id", 300) + } + } + ``` + - id: with-rpc + name: With `rpc()` + code: | + ```kotlin + supabase.postgrest.rpc("echo_all_cities") { + filter { + City::countryId lt 300 + //or + lt("country_id", 300) + } + } + ``` + + - id: lte + title: lte() + description: | + Finds all rows whose value on the stated `column` is less than or equal to the specified `value`. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: value + isOptional: false + type: Any + description: The value to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name")) { + filter { + City::countryId lte 300 + //or + lte("country_id", 300) + } + } + ``` + - id: with-update + name: With `update()` + code: | + ```kotlin + val toUpdate = City(name = "Mordor") + supabase.from("cities").update(toUpdate) { + filter { + City::countryId lte 300 + //or + lte("country_id", 300) + } + } + ``` + - id: with-delete + name: With `delete()` + code: | + ```kotlin + supabase.from("cities").delete { + filter { + City::countryId lte 300 + //or + lte("country_id", 300) + } + } + ``` + - id: with-rpc + name: With `rpc()` + code: | + ```kotlin + supabase.postgrest.rpc("echo_all_cities") { + filter { + City::countryId lte 300 + //or + lte("country_id", 300) + } + } + ``` + + - id: like + title: like() + description: | + Finds all rows whose value in the stated `column` matches the supplied `pattern` (case sensitive). + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: pattern + isOptional: false + type: String + description: The pattern to match with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name")) { + filter { + City::name like "%la%" + //or + like("name", "%la%") + } + } + ``` + - id: with-update + name: With `update()` + code: | + ```kotlin + val toUpdate = City(name = "Mordor") + supabase.from("cities").update(toUpdate) { + filter { + City::name like "%la%" + //or + like("name", "%la%") + } + } + ``` + - id: with-delete + name: With `delete()` + code: | + ```kotlin + supabase.from("cities").delete { + filter { + City::name like "%la%" + //or + like("name", "%la%") + } + } + ``` + - id: with-rpc + name: With `rpc()` + code: | + ```kotlin + supabase.postgrest.rpc("echo_all_cities") { + filter { + City::name like "%la%" + //or + like("name", "%la%") + } + } + ``` + + - id: ilike + title: ilike() + description: | + Finds all rows whose value in the stated `column` matches the supplied `pattern` (case insensitive). + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: pattern + isOptional: false + type: String + description: The pattern to match with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name")) { + filter { + City::name ilike "%la%" + //or + ilike("name", "%la%") + } + } + ``` + - id: with-update + name: With `update()` + code: | + ```kotlin + val toUpdate = City(name = "Mordor") + supabase.from("cities").update(toUpdate) { + filter { + City::name ilike "%la%" + //or + ilike("name", "%la%") + } + } + ``` + - id: with-delete + name: With `delete()` + code: | + ```kotlin + supabase.from("cities").delete { + filter { + City::name ilike "%la%" + //or + ilike("name", "%la%") + } + } + ``` + - id: with-rpc + name: With `rpc()` + code: | + ```kotlin + supabase.postgrest.rpc("echo_all_cities") { + filter { + City::name ilike "%la%" + //or + ilike("name", "%la%") + } + } + ``` + + - id: is + title: is_() + description: | + A check for exact equality (null, true, false), finds all rows whose value on the stated `column` exactly match the specified `value`. + + `is_` and `in_` filter methods are suffixed with `_` to avoid collisions with reserved keywords. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: value + isOptional: false + type: Boolean? + description: The value to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name")) { + filter { + City::name isExact null + //or + exact("name", null) + } + } + ``` + - id: with-update + name: With `update()` + code: | + ```kotlin + val toUpdate = City(name = "Mordor") + supabase.from("cities").update(toUpdate) { + filter { + City::name isExact null + //or + exact("name", null) + } + } + ``` + - id: with-delete + name: With `delete()` + code: | + ```kotlin + supabase.from("cities").delete { + filter { + City::name isExact null + //or + exact("name", null) + } + } + ``` + - id: with-rpc + name: With `rpc()` + code: | + ```kotlin + supabase.postgrest.rpc("echo_all_cities") { + filter { + City::name isExact null + //or + exact("name", null) + } + } + ``` + + - id: in + title: in_() + description: | + Finds all rows whose value on the stated `column` is found on the specified `values`. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: values + isOptional: false + type: List + description: The values to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name")) { + filter { + City::name isIn listOf("Rio de Janeiro", "San Francisco") + //or + isIn("name", listOf("Rio de Janeiro", "San Francisco")) + } + } + ``` + - id: with-update + name: With `update()` + code: | + ```kotlin + val toUpdate = City(name = "Mordor") + supabase.from("cities").update(toUpdate) { + filter { + City::name isIn listOf("Rio de Janeiro", "San Francisco") + //or + isIn("name", listOf("Rio de Janeiro", "San Francisco")) + } + } + ``` + - id: with-delete + name: With `delete()` + code: | + ```kotlin + supabase.from("cities").delete { + filter { + City::name isIn listOf("Rio de Janeiro", "San Francisco") + //or + isIn("name", listOf("Rio de Janeiro", "San Francisco")) + } + } + ``` + - id: with-rpc + name: With `rpc()` + code: | + ```kotlin + supabase.postgrest.rpc("echo_all_cities") { + filter { + City::name isIn listOf("Rio de Janeiro", "San Francisco") + //or + isIn("name", listOf("Rio de Janeiro", "San Francisco")) + } + } + ``` + + - id: contains + title: contains() + description: | + Only relevant for jsonb, array, and range columns. Match only rows where `column` contains every element appearing in `value`. + params: + - name: column + isOptional: false + type: String + description: The jsonb, array, or range column to filter on + - name: value + isOptional: false + type: List + description: The jsonb, array, or range value to filter with + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("cities").select(columns = Columns.list("name")) { + filter { + City::mainExports contains listOf("oil") + //or + contains("main_exports", listOf("oil")) + } + } + ``` + - id: with-update + name: With `update()` + code: | + ```kotlin + val toUpdate = City(name = "Mordor") + supabase.from("cities").update(toUpdate) { + filter { + City::mainExports contains listOf("oil") + //or + contains("main_exports", listOf("oil")) + } + } + ``` + - id: with-delete + name: With `delete()` + code: | + ```kotlin + supabase.from("cities").delete { + filter { + City::mainExports contains listOf("oil") + //or + contains("main_exports", listOf("oil")) + } + } + ``` + - id: with-rpc + name: With `rpc()` + code: | + ```kotlin + supabase.postgrest.rpc("echo_all_cities") { + filter { + City::mainExports contains listOf("oil") + //or + contains("main_exports", listOf("oil")) + } + } + ``` + + - id: range-lt + title: rangeLt() + description: | + Only relevant for range columns. Match only rows where every element in column is less than any element in range. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: values + isOptional: false + type: Pair + description: The values to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("reservations").select { + filter { + Reservation::during rangeLt ("2000-01-02 08:30" to "2000-01-02 09:30") + //or + rangeLt("during", "2000-01-02 08:30" to "2000-01-02 09:30") + } + } + ``` + data: + sql: | + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + response: | + ```json + { + "data": [ + { + "id": 1, + "room_name": "Emerald", + "during": "[\"2000-01-01 13:00:00\",\"2000-01-01 15:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + hideCodeBlock: true + + - id: range-gt + title: rangeGt() + description: | + Only relevant for range columns. Match only rows where every element in column is greater than any element in range. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: values + isOptional: false + type: Pair + description: The values to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("reservations").select { + filter { + Reservation::during rangeGt ("2000-01-02 08:30" to "2000-01-02 09:30") + //or + rangeGt("during", "2000-01-02 08:30" to "2000-01-02 09:30") + } + } + ``` + data: + sql: | + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + response: | + ```json + { + "data": [ + { + "id": 2, + "room_name": "Topaz", + "during": "[\"2000-01-02 09:00:00\",\"2000-01-02 10:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + hideCodeBlock: true + + - id: range-gte + title: rangeGte() + description: | + Only relevant for range columns. Match only rows where every element in column is either contained in range or greater than any element in range. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: values + isOptional: false + type: Pair + description: The values to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("reservations").select { + filter { + Reservation::during rangeGte ("2000-01-02 08:30" to "2000-01-02 09:30") + //or + rangeGte("during", "2000-01-02 08:30" to "2000-01-02 09:30") + } + } + ``` + data: + sql: | + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + response: | + ```json + { + "data": [ + { + "id": 2, + "room_name": "Topaz", + "during": "[\"2000-01-02 09:00:00\",\"2000-01-02 10:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + hideCodeBlock: true + + - id: range-lte + title: rangeLte() + description: | + Only relevant for range columns. Match only rows where every element in column is either contained in range or less than any element in range. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: values + isOptional: false + type: Pair + description: The values to filter with. + $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.rangeLte' + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("reservations").select { + filter { + Reservation::during rangeLte ("2000-01-02 08:30" to "2000-01-02 09:30") + //or + rangeLte("during", "2000-01-02 08:30" to "2000-01-02 09:30") + } + } + ``` + data: + sql: | + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + response: | + ```json + { + "data": [ + { + "id": 1, + "room_name": "Emerald", + "during": "[\"2000-01-01 13:00:00\",\"2000-01-01 15:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + hideCodeBlock: true + + - id: range-adjacent + title: rangeAdjacent() + description: | + Only relevant for range columns. Match only rows where column is mutually exclusive to range and there can be no element between the two ranges. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: values + isOptional: false + type: Pair + description: The values to filter with. + examples: + - id: with-select + name: With `select()` + isSpotlight: true + code: | + ```kotlin + supabase.from("reservations").select { + filter { + Reservation::during adjacent ("2000-01-02 08:30" to "2000-01-02 09:30") + //or + adjacent("during", "2000-01-02 08:30" to "2000-01-02 09:30") + } + } + ``` + data: + sql: | + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + response: | + ```json + { + "data": [ + { + "id": 1, + "room_name": "Emerald", + "during": "[\"2000-01-01 13:00:00\",\"2000-01-01 15:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + hideCodeBlock: true + + - id: overlaps + title: overlaps() + $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.overlaps' + description: | + Only relevant for array and range columns. Match only rows where column and value have an element in common. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: values + isOptional: false + type: List + description: The values to filter with. + examples: + - id: on-array-columns + name: On array columns + code: | + ```kotlin + supabase.from("issues").select(columns = Columns.list("title")) { + filter { + Issue::tags overlaps listOf("is:closed", "severity:high") + //or + overlaps("tags", listOf("is:closed", "severity:high")) + } + } + ``` + data: + sql: | + ```sql + create table + issues ( + id int8 primary key, + title text, + tags text[] + ); + + insert into + issues (id, title, tags) + values + (1, 'Cache invalidation is not working', array['is:open', 'severity:high', 'priority:low']), + (2, 'Use better names', array['is:open', 'severity:low', 'priority:medium']); + ``` + response: | + ```json + { + "data": [ + { + "title": "Cache invalidation is not working" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + hideCodeBlock: true + isSpotlight: true + - id: on-range-columns + name: On range columns + code: | + ```kotlin + supabase.from("issues").select(columns = Columns.list("title")) { + filter { + Issue::during overlaps listOf("2000-01-01 12:45", "2000-01-01 13:15") + //or + overlaps("during", listOf("2000-01-01 12:45", "2000-01-01 13:15")) + } + } + ``` + data: + sql: | + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + response: | + ```json + { + "data": [ + { + "id": 1, + "room_name": "Emerald", + "during": "[\"2000-01-01 13:00:00\",\"2000-01-01 15:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + hideCodeBlock: true + + - id: text-search + title: textSearch() + description: | + Only relevant for text and tsvector columns. Match only rows where `column` matches the query string in `query`. + + For more information, see [Postgres full text search](https://supabase.com/docs/guides/database/full-text-search). + params: + - name: column + isOptional: false + type: String + description: The text or tsvector column to filter on + - name: query + isOptional: false + type: String + description: The query text to match with + - name: textSearchType + isOptional: true + type: TextSearchType + description: The type of text search to use. Defaults to `TextSearchType.NONE`. + - name: config + isOptional: true + type: String + description: The text search configuration to use. + examples: + - id: text-search + name: Text search + code: | + ```kotlin + supabase.from("quotes").select(columns = Columns.list("catchphrase")) { + filter { + textSearch(column = "catchphrase", query = "'fat' & 'cat'", config = "english", type = TextSearchType.YOUR_TYPE) + } + } + ``` + - id: search-multiple-columns + name: Search multiple columns + code: | + ```kotlin + supabase.from("quotes").select(columns = Columns.list("catchphrase")) { + filter { + or { + textSearch(column = "catchphrase", query = "'fat' & 'cat'", config = "english", type = TextSearchType.YOUR_TYPE) + textSearch(column = "author", query = "'fat' & 'cat'", config = "english", type = TextSearchType.YOUR_TYPE) + } + } + } + ``` + - id: basic-normalization + name: Basic normalization + description: Uses PostgreSQL's `plainto_tsquery` function. + code: | + ```kotlin + supabase.from("quotes").select(columns = Columns.list("catchphrase")) { + filter { + textSearch(column = "catchphrase", query = "'fat' & 'cat'", config = "english", type = TextSearchType.PLAINTO) + } + } + ``` + - id: full-normalization + name: Full normalization + description: Uses PostgreSQL's `phraseto_tsquery` function. + code: | + ```kotlin + supabase.from("quotes").select(columns = Columns.list("catchphrase")) { + filter { + textSearch(column = "catchphrase", query = "'fat' & 'cat'", config = "english", type = TextSearchType.PHRASETO) + } + } + ``` + - id: web-search + name: Websearch + description: | + Uses PostgreSQL's `websearch_to_tsquery` function. + This function will never raise syntax errors, which makes it possible to use raw user-supplied input for search, and can be used + with advanced operators. + + - `unquoted text`: text not inside quote marks will be converted to terms separated by & operators, as if processed by plainto_tsquery. + - `"quoted text"`: text inside quote marks will be converted to terms separated by `<->` operators, as if processed by phraseto_tsquery. + - `OR`: the word “or” will be converted to the | operator. + - `-`: a dash will be converted to the ! operator. + + code: | + ```kotlin + supabase.from("quotes").select(columns = Columns.list("catchphrase")) { + filter { + textSearch(column = "catchphrase", query = "'fat' & 'cat'", config = "english", type = TextSearchType.WEBSEARCH) + } + } + ``` + + - id: filter + title: filter() + $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.filter' + notes: | + filter() expects you to use the raw PostgREST syntax for the filter values. + params: + - name: column + isOptional: false + type: String + description: The column to filter on. + - name: operator + isOptional: false + type: FilterOperator + description: The operator to use for the filter. + - name: value + isOptional: false + type: Any + description: The value to filter with. + examples: + - id: with-select + name: With `select()` + code: | + ```kotlin + supabase.from("countries").select { + filter { + filter(column = "name", operator = FilterOperator.IN, value = "('Algeria', 'Japan')") + } + } + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + response: | + ```json + { + "data": [ + { + "id": 3, + "name": "Algeria" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + hideCodeBlock: true + isSpotlight: true + - id: on-a-foreign-table + name: On a foreign table + code: | + ```kotlin + val columns = Columns.raw(""" + name, + cities!inner ( + name + ) + """.trimIndent()) + supabase.from("countries").select( + columns = columns + ) { + filter { + filter(column = "cities.name", operator = FilterOperator.EQ, value = "Bali") + } + } + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 2, 'Bali'), + (2, 1, 'Munich'); + ``` + response: | + ```json + { + "data": [ + { + "name": "Indonesia", + "cities": [ + { + "name": "Bali" + } + ] + } + ], + "status": 200, + "statusText": "OK" + } + ``` + hideCodeBlock: true + - id: using-modifiers + title: Using Modifiers + description: | + Filters work on the row level—they allow you to return rows that + only match certain conditions without changing the shape of the rows. + Modifiers are everything that don't fit that definition—allowing you to + change the format of the response (e.g., returning a CSV string). + + Modifiers are be specified next to the filter block. Some modifiers only apply for + queries that return rows (e.g., `select()` or `rpc()` on a function that + returns a table response). + - id: db-modifiers-select + title: select() + $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.select' + params: + - name: columns + isOptional: true + type: Columns + description: The columns to select. + examples: + - id: with-upsert + name: With `upsert()` + code: | + ```kotlin + val toUpsert = Country(id = 2, name = "Algeria") + val count = supabase.from("countries").upsert(toUpsert) { + select() + }.decodeSingle() + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'); + ``` + response: | + ```json + { + "data": [ + { + "id": 2, + "name": "Algeria" + } + ], + "status": 201, + "statusText": "Created" + } + ``` + hideCodeBlock: true + isSpotlight: true + - id: order + title: order() + description: | + Order the query result by column. + $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.order' + params: + - name: column + isOptional: false + type: String + description: The column to order by. + - name: order + isOptional: false + type: Order + description: The order to use. + - name: nullsFirst + isOptional: true + type: Boolean + description: Whether to order nulls first. + - name: referencedTable + isOptional: true + type: String + description: The foreign table to order by. + examples: + - id: with-select + name: With `select()` + code: | + ```kotlin + supabase.from("countries").select(columns = Columns.list("id", "name")) { + order(column = "id", order = Order.ASCENDING) + } + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + response: | + ```json + { + "data": [ + { + "id": 3, + "name": "Algeria" + }, + { + "id": 2, + "name": "Albania" + }, + { + "id": 1, + "name": "Afghanistan" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + hideCodeBlock: true + isSpotlight: true + - id: on-a-foreign-table + name: On a foreign table + code: | + ```kotlin + val columns = Columns.raw(""" + name, + cities ( + name + ) + """.trimIndent()) + supabase.from("countries").select( + columns = columns + ) { + order(column = "id", order = Order.ASCENDING, referencedTable = "cities") + } + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'United States'), + (2, 'Vanuatu'); + insert into + cities (id, country_id, name) + values + (1, 1, 'Atlanta'), + (2, 1, 'New York City'); + ``` + response: | + ```json + { + "data": [ + { + "name": "United States", + "cities": [ + { + "name": "New York City" + }, + { + "name": "Atlanta" + } + ] + }, + { + "name": "Vanuatu", + "cities": [] + } + ], + "status": 200, + "statusText": "OK" + } + ``` + description: | + Ordering on foreign tables doesn't affect the ordering of + the parent table. + hideCodeBlock: true + + - id: limit + title: limit() + description: | + Limit the query result by count. + $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.limit' + params: + - name: count + isOptional: false + type: Long + description: The number of rows to limit the result to. + - name: referencedTable + isOptional: true + type: String + description: The foreign table to limit by. + examples: + - id: with-select + name: With `select()` + code: | + ```kotlin + supabase.from("countries").select { + limit(count = 1) + } + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + response: | + ```json + { + "data": [ + { + "name": "Afghanistan" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + hideCodeBlock: true + isSpotlight: true + - id: on-a-foreign-table + name: On a foreign table + code: | + ```kotlin + val columns = Columns.raw(""" + name, + cities ( + name + ) + """) + supabase.from("countries").select( + columns = columns + ) { + limit(count = 1, referencedTable = "cities") + } + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'United States'); + insert into + cities (id, country_id, name) + values + (1, 1, 'Atlanta'), + (2, 1, 'New York City'); + ``` + response: | + ```json + { + "data": [ + { + "name": "United States", + "cities": [ + { + "name": "Atlanta" + } + ] + } + ], + "status": 200, + "statusText": "OK" + } + ``` + hideCodeBlock: true + - id: range + title: range() + description: | + Limit the query result by from and to inclusively. + params: + - name: from + isOptional: false + type: Long + description: The start of the range. + - name: to + isOptional: false + type: Long + description: The end of the range. + - name: referencedTable + isOptional: true + type: String + description: The foreign table to limit by. + examples: + - id: with-select + name: With `select()` + code: | + ```kotlin + supabase.from("countries").select { + range(1L..5L) + } + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + response: | + ```json + { + "data": [ + { + "name": "Afghanistan" + }, + { + "name": "Albania" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + hideCodeBlock: true + isSpotlight: true + - id: single + title: single() + $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.single' + examples: + - id: with-select + name: With `select()` + code: | + ```kotlin + val result = supabase.from("countries").select(Columns.list("name")) { + limit(1) + single() + } + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + response: | + ```json + { + "data": { + "name": "Afghanistan" + }, + "status": 200, + "statusText": "OK" + } + ``` + hideCodeBlock: true + isSpotlight: true + + - id: csv + $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.csv' + title: csv() + examples: + - id: return-data-as-csv + name: Return data as CSV + code: | + ```kotlin + val (csvData, _) = supabase.from("countries").select { + csv() + } + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + response: | + ```json + { + "data": "id,name\n1,Afghanistan\n2,Albania\n3,Algeria", + "status": 200, + "statusText": "OK" + } + ``` + description: | + By default, the data is returned in JSON format, but can also be returned as Comma Separated Values. + hideCodeBlock: true + isSpotlight: true + + - id: explain + $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.explain' + title: Using Explain + description: | + For debugging slow queries, you can get the [Postgres `EXPLAIN` execution plan](https://www.postgresql.org/docs/current/sql-explain.html) of a query + using the `explain()` method. This works on any query, even for `rpc()` or writes. + + Explain is not enabled by default as it can reveal sensitive information about your database. + It's best to only enable this for testing environments but if you wish to enable it for production you can provide additional protection by using a `pre-request` function. + + Follow the [Performance Debugging Guide](/docs/guides/api/rest/debugging-performance) to enable the functionality on your project. + params: + - name: analyze + isOptional: true + type: Boolean + description: If `true`, the query will be executed and the actual run time will be returned + - name: verbose + isOptional: true + type: Boolean + description: If `true`, the query identifier will be returned and `data` will include the output columns of the query + - name: settings + isOptional: true + type: Boolean + description: If `true`, include information on configuration parameters that affect query planning + - name: buffers + isOptional: true + type: Boolean + description: If `true`, include information on buffer usage + - name: wal + isOptional: true + type: Boolean + description: If `true`, include information on WAL record generation + - name: format + isOptional: true + type: String + description: The format of the output, can be `"text"` (default) or `"json"` + examples: + - id: get-execution-plan + name: Get the execution plan + code: | + ```kotlin + val result = supabase.from("countries").select { + explain() + } + ``` + data: + sql: | + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + response: | + ``` + Aggregate (cost=33.34..33.36 rows=1 width=112) + -> Limit (cost=0.00..18.33 rows=1000 width=40) + -> Seq Scan on countries (cost=0.00..22.00 rows=1200 width=40) + ``` + description: | + By default, the data is returned in TEXT format, but can also be returned as JSON by using the `format` parameter. + hideCodeBlock: true + isSpotlight: true + - id: auth-api + title: 'Overview' + notes: | + - The auth methods can be accessed via the Supabase Auth client. + examples: + - id: create-auth-client + name: Create auth client + isSpotlight: true + code: | + ```kotlin + val supabase = createSupabaseClient(supabaseURL = "https://xyzcompany.supabase.co'", supabaseKey = "public-anon-key") { ... } + val auth = supabase.auth + ``` + - id: sign-up + title: 'signUp()' + $ref: '@supabase/gotrue-js.GoTrueClient.signUp' + notes: | + Creates a new user. + - By default, the user needs to verify their email address before logging in. To turn this off, disable **Confirm email** in [your project](https://supabase.com/dashboard/project/_/auth/providers). + - **Confirm email** determines if users need to confirm their email address after signing up. + - If **Confirm email** is enabled, the return value is the user and you won't be logged in automatically. + - If **Confirm email** is disabled, the return value is null and you will be logged in instead. + - When the user confirms their email address, they are redirected to the [`SITE_URL`](https://supabase.com/docs/guides/auth/redirect-urls) by default. You can modify your `SITE_URL` or add additional redirect URLs in [your project](https://supabase.com/dashboard/project/_/auth/url-configuration). + - To learn how to handle OTP links & OAuth refer to [initializing](/docs/reference/kotlin/initializing) + - If signUpWith() is called for an existing confirmed user: + - When both **Confirm email** and **Confirm phone** (even when phone provider is disabled) are enabled in [your project](/dashboard/project/_/auth/providers), an obfuscated/fake user object is returned. + - When either **Confirm email** or **Confirm phone** (even when phone provider is disabled) is disabled, the error message, `User already registered` is returned. + params: + - name: provider + isOptional: false + type: Email or Phone + description: The provider to use for the user's authentication. In this case `Email` or `Phone`. + - name: redirectUrl + isOptional: true + type: String? + description: The redirect url to use. If you don't specify this, the platform specific will be used, like deeplinks on android. + - name: config + isOptional: true + type: Email.Config.() -> Unit or Phone.Config.() -> Unit + description: The configuration for signing in with `Email` or `Phone`. + subContent: + - name: password + isOptional: false + type: String + description: The user's password + - name: email/phone + isOptional: false + type: String + description: The user's email or phone. + - name: captchaToken + isOptional: true + type: String? + description: The captcha token when having captcha enabled. + - name: data + isOptional: true + type: JsonObject? + description: Extra user data to pass in. + examples: + - id: sign-up-email + name: Sign up with email + isSpotlight: true + code: | + ```kotlin + val user = supabase.auth.signUpWith(Email) { + email = "example@email.com" + password = "example-password" + } + ``` + - id: sign-up-phone-whatsapp + name: Sign up with a phone number and password (whatsapp) + isSpotlight: true + code: | + ```kotlin + val user = supabase.auth.signUpWith(Phone) { + phone = "+4912345679" + password = "example-password" + channel = Phone.Channel.WHATSAPP + } + ``` + - id: sign-up-phone-sms + name: Sign up with a phone number and password (sms) + isSpotlight: true + code: | + ```kotlin + val user = supabase.auth.signUpWith(Phone) { + phone = "+4912345679" + password = "example-password" + } + ``` + - id: sign-up-with-additional-user-metadata + name: Sign up with additional user metadata + isSpotlight: false + code: | + ```kotlin + val user = supabase.auth.signUpWith(Email) { + email = "example@email.com" + password = "example-password" + data = buildJsonObject { + put("first_name", "John") + put("age", 24) + } + } + ``` + - id: sign-up-with-redirect + name: Sign up with a redirect URL + description: | + - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + code: | + ```kotlin + val user = supabase.auth.signUpWith(Email, redirectUrl = "https://example.com") { + email = "example@email.com" + password = "example-password" + } + ``` + - id: sign-in-anonymously + title: 'signInAnonymously()' + notes: | + - Creates an anonymous user. + - The user can be retrieved by calling `supabase.auth.currentUserOrNull()`. + - It is recommended to set up captcha for anonymous sign-ins to prevent abuse. You can pass in the captcha token in the `options` param. + params: + - name: captchaToken + isOptional: true + type: String? + description: The captcha token when having captcha enabled. + - name: data + isOptional: true + type: JsonObject? or T + description: Extra user data to pass in. + examples: + - id: sign-in-anonymously + name: Create an anonymous user + isSpotlight: true + code: | + ```kotlin + supabase.auth.signInAnonymously(captchaToken = "token") + ``` + - id: sign-in-anonymously-with-user-metadata + name: Create an anonymous user with custom user metadata + isSpotlight: false + code: | + ```kotlin + supabase.auth.signInAnonymously(data = customUserData) + ``` + + - id: sign-in-with-password + title: 'signInWith()' + $ref: '@supabase/gotrue-js.GoTrueClient.signInWithPassword' + notes: | + Logs in an existing user. + - Requires either an email and password or a phone number and password. + params: + - name: provider + isOptional: false + type: Email or Phone + description: The provider to use for the user's authentication, in this case `Email` or `Phone`. + - name: redirectUrl + isOptional: true + type: String? + description: The redirect url to use. If you don't specify this, the platform specific will be used, like deeplinks on android. + - name: config + isOptional: true + type: Email.Config.() -> Unit or Phone.Config.() -> Unit + description: The configuration for signing in with `Email` or `Phone`. + subContent: + - name: password + isOptional: false + type: String + description: The user's password + - name: email/phone + isOptional: false + type: String + description: The user's email or phone. + - name: captchaToken + isOptional: true + type: String? + description: The captcha token when having captcha enabled. + examples: + - id: sign-in-with-email-and-password + name: Sign in with email and password + isSpotlight: true + code: | + ```kotlin + supabase.auth.signInWith(Email) { + email = "example@email.com" + password = "example-password" + } + ``` + - id: sign-in-with-phone-and-password + name: Sign in with phone and password + isSpotlight: false + code: | + ```kotlin + supabase.auth.signInWith(Phone) { + phone = "+4912345679" + password = "example-password" + } + ``` + - id: sign-in-with-id-token + title: 'signInWithIdToken' + params: + - name: provider + isOptional: false + type: IDToken + description: The provider to use for the user's authentication. For this method it will be `IDToken`. + - name: redirectUrl + isOptional: true + type: String? + description: The redirect url to use. If you don't specify this, the platform specific will be used, like deeplinks on android. + - name: config + isOptional: true + type: IDToken.Config.() -> Unit + description: The configuration for signing in with an id token. + subContent: + - name: idToken + isOptional: false + type: String + description: OIDC ID token issued by the specified provider. The `iss` claim in the ID token must match the supplied provider. Some ID tokens contain an `at_hash` which require that you provide an `access_token` value to be accepted properly. If the token contains a `nonce` claim you must supply the nonce used to obtain the ID token. + - name: provider + isOptional: false + type: IDTokenProvider + description: The provider of the id token. Only `Apple`, `Google`, `Facebook` and `Azure` are supported. + - name: accessToken + isOptional: true + type: String? + description: If the ID token contains an `at_hash` claim, then the hash of this value is compared to the value in the ID token. + - name: nonce + isOptional: true + type: String? + description: If the ID token contains a `nonce` claim, then the hash of this value is compared to the value in the ID token. + - name: captchaToken + isOptional: true + type: String? + description: The captcha token when having captcha enabled. + + examples: + - id: sign-in-with-id-token + name: 'Sign In using ID Token' + code: | + ```kotlin + supabase.auth.signInWith(IDToken) { + idToken = "token" + provider = Google //Also supported: Apple, Azure and Facebook + //optional: + nonce = "nonce" + data = buildJsonObject { + //... + } + } + ``` + - id: sign-in-with-otp + title: 'signInWith(OTP)' + $ref: '@supabase/gotrue-js.GoTrueClient.signInWithOtp' + notes: | + Sends a OTP to the user's email or phone number. + - Requires either an email or phone number. + - This method is used for passwordless sign-ins where a OTP is sent to the user's email or phone number. + - If the user doesn't exist, `signInWith(OTP)` will signup the user instead. To restrict this behavior, you can set `createUser` to `false`. + - The method `signUpWith(OTP)` does the exact same thing as `signInWith(OTP)`, so it doesn't matter which one you use. + - If you're using an email, you can configure whether you want the user to receive a magiclink or a OTP. + - If you're using phone, you can configure whether you want the user to receive a OTP. + - The magic link's destination URL is determined by the [`SITE_URL`](/docs/guides/auth/redirect-urls). + - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - To learn how to handle OTP links & OAuth refer to [initializing](/docs/reference/kotlin/initializing) + - Magic links and OTPs share the same implementation. To send users a one-time code instead of a magic link, [modify the magic link email template](https://supabase.com/dashboard/project/_/auth/templates) to include `{{ .Token }}` instead of `{{ .ConfirmationURL }}`. + params: + - name: provider + isOptional: false + type: OTP + description: The provider to use for the user's authentication, in this case `OTP`. + - name: redirectUrl + isOptional: true + type: String? + description: The redirect url to use. If you don't specify this, the platform specific will be used, like deeplinks on android. + - name: config + isOptional: true + type: OTP.Config.() -> Unit + description: The configuration for signing in with `OTP`. + subContent: + - name: email/phone + isOptional: false + type: String + description: The user's email or phone. + - name: captchaToken + isOptional: true + type: String? + description: The captcha token when having captcha enabled. + examples: + - id: sign-in-with-email + name: Sign in with email + isSpotlight: true + description: The user will be sent an email which contains either a magiclink or a OTP or both. By default, a given user can only request a OTP once every 60 seconds. + code: | + ```kotlin + supabase.auth.signInWith(OTP) { + email = "example@email.com" + } + ``` + - id: sign-in-with-sms-otp + name: Sign in with SMS OTP + isSpotlight: false + description: The user will be sent a SMS which contains a OTP. By default, a given user can only request a OTP once every 60 seconds. + code: | + ```kotlin + supabase.auth.signInWith(OTP) { + phone = "+4912345679" + } + ``` + - id: sign-in-with-oauth + title: 'signInWith(OAuthProvider)' + $ref: '@supabase/gotrue-js.GoTrueClient.signInWithOAuth' + notes: | + - This method is used for signing in using a third-party provider. + - Supabase supports many different [third-party providers](https://supabase.com/docs/guides/auth#providers). + - To learn how to handle OTP links & OAuth refer to [initializing](/docs/reference/kotlin/initializing) + params: + - name: provider + isOptional: false + type: OAuthProvider + description: The OAuth provider to use for the user's authentication, for example `Google` or `Github`. + - name: redirectUrl + isOptional: true + type: String? + description: The redirect url to use. If you don't specify this, the platform specific will be used, like deeplinks on android. + - name: config + isOptional: true + type: ExternalAuthConfig.() -> Unit + description: The configuration for signing in with an OAuth provider. + subContent: + - name: scopes + isOptional: true + type: MutableList + description: The scopes to request from the OAuth provider. + - name: queryParams + isOptional: true + type: MutableMap + description: Additional query parameters to use. + examples: + - id: sign-in-using-a-third-party-provider + name: Sign in using a third-party provider + isSpotlight: true + code: | + ```kotlin + supabase.auth.signInWith(Github) + ``` + - id: sign-in-using-a-third-party-provider with scopes + name: Sign in using a third-party provider with scopes + isSpotlight: true + code: | + ```kotlin + supabase.auth.signInWith(Github) { + scopes.add("email") + } + ``` + + - id: sign-in-using-a-third-party-provider-with-redirect + name: Create a custom url + isSpotlight: false + description: | + - When the third-party provider successfully authenticates the user, the provider redirects the user to the URL specified in the `redirectUrl` parameter. This parameter defaults to the [`SITE_URL`](/docs/guides/auth/redirect-urls). It does not redirect the user immediately after invoking this method. + - See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project. + - getOAuthUrl() provides the URL which needs to be opened in a browser. + - The redirectTo URL needs to be setup correctly in your project under Authentication -> URL Configuration -> Redirect URLs. + - To see how you can use a custom in-app browser on Android, check our [demo](https://github.com/supabase-community/supabase-kt/tree/development/demos/android-login) on GitHub. + code: | + ```kotlin + val url = supabase.auth.getOAuthUrl(Github, redirectUrl = "https://example.com") + ``` + - id: sign-in-with-scopes + name: Create a custom url with scopes + isSpotlight: false + description: | + If you need additional data from an OAuth provider, you can include a space-separated list of scopes in your request to get back an OAuth provider token. + You may also need to specify the scopes in the provider's OAuth app settings, depending on the provider. The list of scopes will be documented by the third-party provider you are using and specifying scopes will enable you to use the OAuth provider token to call additional APIs supported by the third-party provider to get more information. + code: | + ```kotlin + val url = supabase.auth.getOAuthUrl(Github, redirectUrl = "https://example.com") { + scopes.add("email") + } + ``` + - id: sign-in-with-sso + title: 'signInWithSSO()' + $ref: '@supabase/gotrue-js.GoTrueClient.signInWithSSO' + notes: | + - Before you can call this method you need to [establish a connection](/docs/guides/auth/sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this. + - If you've associated an email domain to the identity provider, you can change the `domain` property in the `signInWith(SSO)` method to start a sign-in flow. + - In case you need to use a different way to start the authentication flow with an identity provider, you can change the `providerId` property. For example: + - Mapping specific user email addresses with an identity provider. + - Using different hints to identity the identity provider to be used by the user, like a company-specific page, IP address or other tracking information. + - To learn how to handle OTP links & OAuth refer to [initializing](/docs/reference/kotlin/initializing) + params: + - name: provider + isOptional: false + type: SSO + description: The OAuth provider to use for the user's authentication, in this case `SSO`. + - name: redirectUrl + isOptional: true + type: String? + description: The redirect url to use. If you don't specify this, the platform specific will be used, like deeplinks on android. + - name: config + isOptional: true + type: ExternalAuthConfig.() -> Unit + description: The configuration for signing in with an OAuth provider. + subContent: + - name: providerId/domain + isOptional: false + type: String + description: The providerId or domain. + - name: captchaToken + isOptional: true + type: String? + description: The captcha token when having captcha enabled. + examples: + - id: sign-in-with-domain + name: Sign in with email domain + isSpotlight: true + code: | + ```kotlin + // You can extract the user's email domain and use it to trigger the + // authentication flow with the correct identity provider. + + supabase.auth.signInWith(SSO) { + domain = "company.com" + } + + //the url was opened automatically, if you don't want that, provide a custom redirect url + ``` + - id: sign-in-with-provider-uuid + name: Sign in with provider UUID + isSpotlight: true + code: | + ```kotlin + // Useful when you need to map a user's sign in request according + // to different rules that can't use email domains. + + supabase.auth.signInWith(SSO) { + providerId = "21648a9d-8d5a-4555-a9d1-d6375dc14e92" + } + + //the url was opened automatically, if you don't want that, provide a custom redirect url + ``` + - id: sign-out + title: 'signOut()' + $ref: '@supabase/gotrue-js.GoTrueClient.signOut' + notes: | + Logs out the current user. + - In order to use the `signOut()` method, the user needs to be signed in first. + params: + - name: scope + isOptional: true + type: SignOutScope + description: The scope of the sign-out. + examples: + - id: sign-out + name: Sign out + isSpotlight: true + code: | + ```kotlin + supabase.auth.signOut() + ``` + - id: sign-out-all-sessions + name: Sign out all sessions + isSpotlight: true + code: | + ```kotlin + supabase.auth.signOut(SignOutScope.GLOBAL) + ``` + - id: sign-out-others + name: Sign out all sessions except the current + isSpotlight: true + code: | + ```kotlin + supabase.auth.signOut(SignOutScope.OTHERS) + ``` + - id: verify-otp + title: 'Verify OTPs' + $ref: '@supabase/gotrue-js.GoTrueClient.verifyOtp' + notes: | + - Verifying an OTP is done through either `verifyPhoneOtp` or `verifyEmailOtp`. + - The verification type used should be determined based on the corresponding auth method called before using `verifyPhoneOtp`/`verifyEmailOtp` to sign up / sign-in a user. + params: + - name: type + isOptional: false + type: OtpType.Email or OtpType.Phone + description: The OTP type. Depending on the type, an email or phone has to be specified as parameter. + - name: email/phone + isOptional: false + type: String + description: The email or phone number, depending on which type you specified. + - name: token + isOptional: false + type: String + description: The token to verify. + - name: captchaToken + isOptional: true + type: String? + description: The captcha token when having captcha enabled. + examples: + - id: verify-email-otp(otp) + name: Verify an Email OTP + isSpotlight: true + code: | + ```kotlin + supabase.auth.verifyEmailOtp(type = OtpType.Email.EMAIL, email = "example@email.com", token = "token") + ``` + description: | + Available types are: + - `OtpType.Email.MAGIC_LINK` (deprecated) + - `OtpType.Email.SIGNUP` (deprecated) + - `OtpType.Email.INVITE` + - `OtpType.Email.RECOVERY` + - `OtpType.Email.EMAIL_CHANGE` + - `OtpType.Email.EMAIL` + - id: verify-phone-otp(otp) + name: Verify an Phone OTP + isSpotlight: false + code: | + ```kotlin + supabase.auth.verifyPhoneOtp(type = OtpType.Phone.SMS, phone = "+491234567", token = "token") + ``` + description: | + Available types are: + - `OtpType.Phone.SMS` + - `OtpType.Phone.PHONE_CHANGE` + - id: send-password-reauthentication + title: 'reauthenticate()' + $ref: '@supabase/gotrue-js.GoTrueClient.reauthenticate' + notes: | + - This method is used together with `updateUser()` when a user's password needs to be updated. + - This method will send a nonce to the user's email. If the user doesn't have a confirmed email address, the method will send the nonce to the user's confirmed phone number instead. + examples: + - id: send-reauthentication-nonce + name: Send reauthentication nonce + description: Sends a reauthentication nonce to the user's email or phone number. + isSpotlight: true + code: | + ```kotlin + supabase.auth.reauthenticate() + ``` + - id: resend-email-or-phone-otps + title: 'resend()' + $ref: '@supabase/gotrue-js.GoTrueClient.resend' + notes: | + - Resends a signup confirmation, email change or phone change email to the user. + - Passwordless sign-ins can be resent by calling the `signInWith(OTP)` method again. + - Password recovery emails can be resent by calling the `resetPasswordForEmail()` method again. + - This method will only resend an email or phone OTP to the user if there was an initial signup, email change or phone change request being made. + params: + - name: type + isOptional: false + type: OtpType.Email or OtpType.Phone + description: The OTP type. Depending on the type, an email or phone has to be specified as parameter. + - name: email/phone + isOptional: false + type: String + description: The email or phone number, depending on which type you specified. + - name: captchaToken + isOptional: true + type: String? + description: The captcha token when having captcha enabled. + examples: + - id: resend-email-signup-confirmation + name: Resend an email signup confirmation + description: Resends the email signup confirmation to the user + isSpotlight: true + code: | + ```kotlin + supabase.auth.resendEmail(OtpType.Email.SIGNUP, "example@email.com") + ``` + - id: resend-phone-signup-confirmation + name: Resend a phone signup confirmation + description: Resends the phone signup confirmation email to the user + code: | + ```kotlin + supabase.auth.resendPhone(OtpType.Phone.SMS, "1234567890") + ``` + - id: resend-email-change-email + name: Resend email change email + description: Resends the email change email to the user + code: | + ```kotlin + supabase.auth.resendEmail(OtpType.Email.EMAIL_CHANGE, "example@email.com") + ``` + - id: resend-phone-change + name: Resend phone change OTP + description: Resends the phone change OTP to the user + code: | + ```kotlin + supabase.auth.resendPhone(OtpType.Phone.PHONE_CHANGE, "1234567890") + ``` + - id: get-session + title: 'Get current session' + $ref: '@supabase/gotrue-js.GoTrueClient.getSession' + notes: | + Returns the current session, or `null` if there is none. + examples: + - id: get-the-session-data + name: Get the session data + isSpotlight: true + code: | + ```kotlin + val session = supabase.auth.currentSessionOrNull() + ``` + - id: get-user + title: 'getUser()' + $ref: '@supabase/gotrue-js.GoTrueClient.getUser' + description: | + - This method gets the user object from the current session. + - Fetches the user object from the database instead of local session. + - Should be used only when you require the most current user data. For faster results, `getCurrentSessionOrNull()?.user` is recommended. + params: + - name: jwt + isOptional: false + type: String + description: The JWT token. + examples: + - id: get-the-logged-in-user-with-the-current-existing-session + name: Get the logged in user with the current session + isSpotlight: true + code: | + ```kotlin + val user = supabase.auth.retrieveUserForCurrentSession(updateSession = true) + ``` + description: | + `updateSession` updates the local session with the new user + - id: get-different-user + name: Get a user based on their access token + isSpotlight: true + code: | + ```kotlin + val user = supabase.auth.retrieveUser("JWT") + ``` + - id: update-user + title: 'updateUser()' + $ref: '@supabase/gotrue-js.GoTrueClient.updateUser' + notes: | + Modifies the user data. + - In order to use the `updateUser()` method, the user needs to be signed in first. + - By default, email updates sends a confirmation link to both the user's current and new email. + To only send a confirmation link to the user's new email, disable **Secure email change** in your project's [email auth provider settings](https://supabase.com/dashboard/project/_/auth/providers). + params: + - name: updateCurrentUser + isOptional: true + type: Boolean + description: Whether to update the local session with the new user. Defaults to `true`. + - name: redirectUrl + isOptional: true + type: String? + description: The redirect url to use. If you don't specify this, the platform specific will be used, like deeplinks on android. + - name: config + isOptional: false + type: UserUpdateBuilder.() -> Unit + subContent: + - name: email + isOptional: true + type: String? + description: The new email. + - name: password + isOptional: true + type: String? + description: The new password. + - name: phone + isOptional: true + type: String? + description: The new phone number. + - name: nonce + isOptional: true + type: String? + description: The nonce sent for reauthentication if the user's password is to be updated. + - name: data + isOptional: true + type: JsonObject + description: The new user data. + examples: + - id: update-the-email-for-an-authenticated-user + name: Update the email for an authenticated user + description: Sends a "Confirm Email Change" email to the new email address. + isSpotlight: false + code: | + ```kotlin + val user = supabase.auth.updateUser { + email = "newEmail@email.com" + } + ``` + - id: update-the-password-for-an-authenticated-user + name: Update the password for an authenticated user + isSpotlight: false + code: | + ```kotlin + val user = supabase.auth.updateUser { + password = "secretPassword" + } + ``` + - id: update-the-users-metadata + name: Update the user's metadata + isSpotlight: true + code: | + ```kotlin + val user = supabase.auth.updateUser { + data { + put("name", "John") + } + } + ``` + - id: get-user-identities + title: 'currentIdentitiesOrNull()' + $ref: '@supabase/gotrue-js.GoTrueClient.getUserIdentities' + notes: | + - The user needs to be signed in to call `currentIdentitiesOrNull()`. + examples: + - id: get-user-identities + name: Returns a list of identities linked to the user + isSpotlight: true + code: | + ```kotlin + //get the identities from the current user + val identities = supabase.auth.currentIdentitiesOrNull() + //Or retrieve them + val identities = supabase.auth.retrieveUserForCurrentSession().identities + ``` + - id: link-identity + title: 'linkIdentity()' + $ref: '@supabase/gotrue-js.GoTrueClient.linkIdentity' + notes: | + - The **Enable Manual Linking** option must be enabled from your [project's authentication settings](/dashboard/project/_/settings/auth). + - The user needs to be signed in to call `linkIdentity()`. + - If the candidate identity is already linked to the existing user or another user, `linkIdentity()` will fail. + - This method works similarly to `signInWith()` using an OAuthProvider. To learn how to handle OTP links & OAuth refer to [initializing](/docs/reference/kotlin/initializing) + params: + - name: provider + isOptional: false + type: OAuthProvider + description: The OAuth provider you want to link the user with. + - name: redirectUrl + isOptional: true + type: String? + description: The redirect url to use. If you don't specify this, the platform specific will be used, like deeplinks on android. + - name: config + isOptional: true + type: ExternalAuthConfigDefaults.() -> Unit + description: Extra configuration. + subContent: + - name: scopes + isOptional: true + type: MutableList + description: The scopes to request from the OAuth provider. + - name: queryParams + isOptional: true + type: MutableMap + description: Additional query parameters to use. + examples: + - id: link-identity + name: Link an identity to a user + isSpotlight: true + code: | + ```kotlin + supabase.auth.linkIdentity(OAuthProvider) + + //Example: + supabase.auth.linkIdentity(Google) + ``` + - id: unlink-identity + title: 'unlinkIdentity()' + $ref: '@supabase/gotrue-js.GoTrueClient.unlinkIdentity' + notes: | + - The **Enable Manual Linking** option must be enabled from your [project's authentication settings](/dashboard/project/_/settings/auth). + - The user needs to be signed in to call `unlinkIdentity()`. + - The user must have at least 2 identities in order to unlink an identity. + - The identity to be unlinked must belong to the user. + params: + - name: identityId + isOptional: false + type: String + description: The id of the OAuth identity + - name: updateLocalUser + isOptional: true + type: Boolean + description: Whether to delete the identity from the local user or not. Defaults to `true`. + examples: + - id: unlink-identity + name: Unlink an identity + isSpotlight: true + code: | + ```kotlin + //get all identities linked to a user + val identities = supabase.auth.currentIdentitiesOrNull() ?: emptyList() + + //find the google identity linked to the user + val googleIdentity = identities.first { it.provider == "google" } + + //unlink the google identity from the user + supabase.auth.unlinkIdentity(googleIdentity.identityId!!) + ``` + - id: set-session + title: 'importSession()' + $ref: '@supabase/gotrue-js.GoTrueClient.setSession' + notes: | + Changes the local session. + - `importSession()` takes in a UserSession. + - [Refresh token rotation](/docs/reference/auth/config#refresh_token_rotation_enabled) is enabled by default on all projects to guard against replay attacks. + - You can configure the [`REFRESH_TOKEN_REUSE_INTERVAL`](https://supabase.com/docs/reference/auth/config#refresh_token_reuse_interval) which provides a short window in which the same refresh token can be used multiple times in the event of concurrency or offline issues. + params: + - name: session + isOptional: false + type: UserSession + description: The session to set. + examples: + - id: refresh-the-session + name: Set local session + description: Sets the local session from refresh_token and returns current session or an error if the refresh_token is invalid. + isSpotlight: true + code: | + ```kotlin + supabase.auth.importSession(UserSession(accessToken = "token", refreshToken = "refresh", expiresIn = 2000, tokenType = "Bearer", user = null)) + ``` + - id: refresh-session + title: 'refreshSession()' + $ref: '@supabase/gotrue-js.GoTrueClient.refreshSession' + notes: | + This method will refresh the session whether the current one is expired or not. + + - This is done automatically, but can be disabled in the Auth config. + params: + - name: refreshToken + isOptional: false + type: String + description: The refresh token to use. + examples: + - id: refresh-current-session + name: Refresh current session + isSpotlight: true + code: | + ```kotlin + val session = supabase.auth.refreshCurrentSession() + ``` + - id: refresh-session-using-the-current-session + name: Refresh session using the refresh token + isSpotlight: true + code: | + ```kotlin + val session = supabase.auth.refreshSession(refreshToken = "refreshToken") + ``` + - id: on-auth-state-change + title: 'sessionStatus' + $ref: '@supabase/gotrue-js.GoTrueClient.onAuthStateChange' + notes: | + Listen to session changes. + examples: + - id: listen-to-auth-changes + name: Listen to auth changes + isSpotlight: true + code: | + ```kotlin + supabase.auth.sessionStatus.collect { + when(it) { + is SessionStatus.Authenticated -> { + println("Received new authenticated session.") + when(it.source) { //Check the source of the session + SessionSource.External -> TODO() + is SessionSource.Refresh -> TODO() + is SessionSource.SignIn -> TODO() + is SessionSource.SignUp -> TODO() + SessionSource.Storage -> TODO() + SessionSource.Unknown -> TODO() + is SessionSource.UserChanged -> TODO() + is SessionSource.UserIdentitiesChanged -> TODO() + } + } + SessionStatus.Initializing -> println("Initializing") + is SessionStatus.RefreshFailure -> println("Refresh failure ${it.cause}") //Either a network error or a internal server error + is SessionStatus.NotAuthenticated -> { + if(it.isSignOut) { + println("User signed out") + } else { + println("User not signed in") + } + } + } + } + ``` + description: | + Types of statuses: + - `NotAuthenticated(isSignOut)`, + - `Initializing`, + - `RefreshFailure(cause)`, + - `Authenticated(session, source)` + - id: reset-password-for-email + title: 'Send a password reset request' + notes: | + Sends a password reset request to the given email address. + - The password reset flow consist of 2 broad steps: (i) Allow the user to login via the password reset link; (ii) Update the user's password. + - The `resetPasswordForEmail()` only sends a password reset link to the user's email. + To update the user's password, see [`updateUser()`](/docs/reference/kotlin/auth-updateuser). + - The user gets redirected back to your app, assuming you setup [OTP handling](/docs/reference/kotlin/initializing) + - After the user has been redirected successfully, prompt them for a new password and call `updateUser()`: + ```kotlin + supabase.auth.updateUser { + password = "1234567" + } + ``` + params: + - name: email + isOptional: false + type: String + description: The email to send the password reset email to. + - name: redirectUrl + isOptional: true + type: String? + description: The redirect url to use. If you don't specify this, the platform specific will be used, like deeplinks on android. + - name: captchaToken + isOptional: true + type: String? + description: The captcha token when having captcha enabled. + examples: + - id: send-password-reset-email + name: Send password reset email + isSpotlight: true + code: | + ```kotlin + supabase.auth.resetPasswordForEmail(email = "example@email.com") + ``` + - id: exchange-code-for-session + title: 'exchangeCodeForSession()' + $ref: '@supabase/gotrue-js.GoTrueClient.exchangeCodeForSession' + notes: | + - Used when `flowType` is set to `FlowType.PKCE` in the Auth configuration. + params: + - name: code + isOptional: false + type: String + description: The code to exchange. + - name: saveSession + isOptional: true + type: Boolean + description: Whether to save the session. Defaults to true. + examples: + - id: exchange-auth-code + name: Exchange Auth Code + isSpotlight: true + code: | + ```kotlin + supabase.auth.exchangeCodeForSession("34e770dd-9ff9-416c-87fa-43b31d7ef225") + ``` + - id: auth-mfa-api + title: 'Overview' + notes: | + This section contains methods commonly used for Multi-Factor Authentication (MFA) and are invoked behind the `supabase.auth.mfa` namespace. + + Currently, we only support time-based one-time password (TOTP) as the 2nd factor. We don't support recovery codes but we allow users to enroll more than 1 TOTP factor, with an upper limit of 10. + + Having a 2nd TOTP factor for recovery frees the user of the burden of having to store their recovery codes somewhere. It also reduces the attack surface since multiple recovery codes are usually generated compared to just having 1 backup TOTP factor. + - id: mfa-enroll + title: 'Enroll a factor' + $ref: '@supabase/gotrue-js.GoTrueMFAApi.enroll' + notes: | + Enrolls a new factor. + - Use `FactorType.TOTP` or `FactorType.Phone` as the factorType and use the returned id to create a challenge. + - To create a challenge, see [`mfa.createChallenge()`](/docs/reference/kotlin/auth-mfa-challenge). + - To verify a challenge, see [`mfa.verifyChallenge()`](/docs/reference/kotlin/auth-mfa-verify). + - To create and verify a challenge in a single step, see [`mfa.createChallengeAndVerify()`](/docs/reference/kotlin/auth-mfa-challengeandverify). + params: + - name: factorType + isOptional: false + type: FactorType + description: The type of MFA factor to enroll. Currently supports `FactorType.TOTP` and `FactorType.Phone`. + - name: issuer + isOptional: true + type: String? + description: Domain which the user is enrolling with. + - name: config + isOptional: true + type: Config.() -> Unit + description: Factor type specific configuration. + examples: + - id: enroll-totp-factor + name: Enroll a time-based, one-time password (TOTP) factor + isSpotlight: true + code: | + ```kotlin + val factor = supabase.auth.mfa.enroll(factorType = FactorType.TOTP, friendlyName = "Your friendly Name") { + // Optional + issuer = "example.com" + } + + // Use the id to create a challenge. + // The challenge can be verified by entering the code generated from the authenticator app. + // The code will be generated upon scanning the qr_code or entering the secret into the authenticator app. + val (id, type, qrCode) = factor.data //qrCode is a svg as a string + val (factorId, factorType, _) = factor + val challenge = supabase.auth.mfa.createChallenge(factor.id) + ``` + - id: enroll-phone-factor + name: Enroll a Phone Factor + isSpotlight: true + code: | + ```kotlin + val factor = supabase.auth.mfa.enroll(factorType = FactorType.Phone, friendlyName = "Your friendly Name") { + phone = "+123456789" + } + + // Use the id to create a challenge and send an SMS with a code to the user. + val (phone) = factor.data + val (factorId, factorType, _) = factor + val challenge = supabase.auth.mfa.createChallenge(factor.id) + ``` + - id: get-local-verified-factors + name: Check the local user for verified factors + isSpotlight: true + code: | + ```kotlin + val verifiedFactors = supabase.auth.mfa.verifiedFactors + ``` + - id: retrieve-verified-factors + name: Retrieve verified factors + isSpotlight: true + code: | + ```kotlin + val verifiedFactors = supabase.auth.mfa.retrieveFactorsForCurrentUser() + ``` + - id: mfa-challenge + title: 'mfa.challenge()' + $ref: '@supabase/gotrue-js.GoTrueMFAApi.challenge' + notes: | + Creates a challenge for a factor. + - An [enrolled factor](/docs/reference/kotlin/auth-mfa-enroll) is required before creating a challenge. + - To verify a challenge, see [`mfa.verifyChallenge()`](/docs/reference/kotlin/auth-mfa-verify). + - A phone factor sends a code to the user upon challenge. The channel defaults to `Phone.Channel.SMS` unless otherwise specified. + params: + - name: factorId + isOptional: false + type: String + description: The id of the MFA factor you want to create a challenge for. + - name: channel + isOptional: true + type: Phone.Channel? + description: The channel to send the challenge to. Defaults to `Phone.Channel.SMS`. + examples: + - id: create-mfa-challenge + name: Create a challenge for a factor + isSpotlight: true + code: | + ```kotlin + val challenge = supabase.auth.mfa.createChallenge(factorId = "34e770dd-9ff9-416c-87fa-43b31d7ef225") + ``` + - id: mfa-verify + title: 'mfa.verify()' + $ref: '@supabase/gotrue-js.GoTrueMFAApi.verify' + notes: | + Verifies a challenge for a factor. + - To verify a challenge, please [create a challenge](/docs/reference/kotlin/auth-mfa-challenge) first. + params: + - name: factorId + isOptional: false + type: String + description: The id of the MFA factor to verify. + - name: challengeId + isOptional: false + type: String + description: The id of the challenge to verify. + - name: code + isOptional: false + type: String + description: The code used to verify. + - name: saveSession + isOptional: true + type: Boolean + description: Whether to save the session. Defaults to true. + examples: + - id: verify-challenge + name: Verify a challenge for a factor + isSpotlight: true + code: | + ```kotlin + supabase.auth.mfa.verifyChallenge( + factorId = "34e770dd-9ff9-416c-87fa-43b31d7ef225", + challengeId = "4034ae6f-a8ce-4fb5-8ee5-69a5863a7c15", + code = "123456", + saveSession = true // this is set to true by default, but you can set it to false if you want to handle the session yourself + ) + ``` + - id: mfa-challenge-and-verify + title: 'mfa.challengeAndVerify()' + $ref: '@supabase/gotrue-js.GoTrueMFAApi.challengeAndVerify' + notes: | + Creates and verifies a challenge for a factor. + - Creating and verifying a challenge in a single step is not supported by the `Phone` factor type. + - An [enrolled factor](/docs/reference/kotlin/auth-mfa-enroll) is required before invoking `createChallengeAndVerify()`. + - Executes [`mfa.createChallenge()`](/docs/reference/kotlin/auth-mfa-challenge) and [`mfa.verifyChallenge()`](/docs/reference/kotlin/auth-mfa-verify) in a single step. + params: + - name: factorId + isOptional: false + type: String + description: The id of the MFA factor to verify. + - name: code + isOptional: false + type: String + description: The code used to verify. + - name: saveSession + isOptional: true + type: Boolean + description: Whether to save the session. Defaults to true. + examples: + - id: challenge-and-verify + name: Create and verify a challenge for a factor + isSpotlight: true + code: | + ```kotlin + supabase.auth.mfa.createChallengeAndVerify( + factorId = "34e770dd-9ff9-416c-87fa-43b31d7ef225", + code = "123456", + saveSession = true // this is set to true by default, but you can set it to false if you want to handle the session yourself + ) + ``` + - id: mfa-unenroll + title: 'mfa.unenroll()' + $ref: '@supabase/gotrue-js.GoTrueMFAApi.unenroll' + notes: | + Unenroll removes a MFA factor. A user has to have an `AAL2` authentication level in order to unenroll a verified factor. + params: + - name: factorId + isOptional: false + type: String + description: The id of the factor you want to unenroll. + examples: + - id: unenroll-a-factor + name: Unenroll a factor + isSpotlight: true + code: | + ```kotlin + supabase.auth.mfa.unenroll(factorId = "34e770dd-9ff9-416c-87fa-43b31d7ef225") + ``` + - id: mfa-get-authenticator-assurance-level + title: 'mfa.getAuthenticatorAssuranceLevel()' + $ref: '@supabase/gotrue-js.GoTrueMFAApi.getAuthenticatorAssuranceLevel' + notes: | + - Authenticator Assurance Level (AAL) is the measure of the strength of an authentication mechanism. + - In Supabase, having an AAL of `aal1` refers to having the 1st factor of authentication such as an email and password or OAuth sign-in while `aal2` refers to the 2nd factor of authentication such as a time-based, one-time-password (TOTP). + - If the user has a verified factor, the `next` field will return `AuthenticatorAssuranceLevel.AAL2`, else, it will return `AuthenticatorAssuranceLevel.AAL1`. + examples: + - id: get-aal + name: Get the AAL details of the current session + isSpotlight: true + code: | + ```kotlin + val (current, next) = supabase.auth.mfa.getAuthenticatorAssuranceLevel() + ``` + - id: aal-enabled + name: Check whether the user has at least one verified factor + isSpotlight: true + code: | + ```kotlin + val (enabled, _) = supabase.auth.mfa.status + //flow variant, automatically emitting new values on session changes + val statusFlow = supabase.auth.mfa.statusFlow + ``` + - id: aal-enabled-for-current-session + name: Check whether the user is logged in using AAL2 + isSpotlight: true + code: | + ```kotlin + val (_, active) = supabase.auth.mfa.status + //flow variant, automatically emitting new values on session changes + val statusFlow = supabase.auth.mfa.statusFlow + ``` + - id: admin-api + title: 'Overview' + notes: | + - Any method under the `supabase.auth.admin` namespace requires a `service_role` key. + - These methods are considered admin methods and should be called on a trusted server. Never expose your `service_role` key in the browser. + examples: + - id: create-auth-admin-client + name: Create server-side auth client + isSpotlight: true + code: | + ```kotlin + val supabase = createSupabaseClient( + supabaseUrl = "https://id.supabase.co", + supabaseKey = "supabaseKey" + ) { + install(Auth) { + minimalSettings() //disables session saving and auto-refreshing + } + // install other plugins (these will use the service role key) + } + supabase.auth.importAuthToken("service_role") + + // Access auth admin api + val adminAuthClient = supabase.auth.admin + ``` + + - id: get-user-by-id + title: 'getUserById()' + $ref: '@supabase/gotrue-js.GoTrueAdminApi.getUserById' + notes: | + Fetches the user object from the database based on the user's id. + - The `retrieveUserById()` method requires the user's id which maps to the `auth.users.id` column. + params: + - name: uid + isOptional: false + type: String + description: The id of the user you want to retrieve. + examples: + - id: fetch-the-user-object-using-the-access-token-jwt + name: Fetch the user object using the access_token jwt + isSpotlight: true + code: | + ```kotlin + val user = supabase.auth.admin.retrieveUserById(uid = "f2a0b0a0-6b1a-4b7a-8f1a-4b7a6b1a8f1a") + ``` + + - id: list-users + title: 'listUsers()' + $ref: '@supabase/gotrue-js.GoTrueAdminApi.listUsers' + notes: | + Retrieves a list of users. + - Defaults to return 50 users per page. + params: + - name: page + isOptional: true + type: Int + description: The page number to retrieve. + - name: perPage + isOptional: true + type: Int + description: The number of users to retrieve per page. + examples: + - id: get-a-full-list-of-users + name: Get a page of users + isSpotlight: true + code: | + ```kotlin + val users = supabase.auth.admin.retrieveUsers() + ``` + - id: get-paginated-list-of-users + name: Paginated list of users + isSpotlight: false + code: | + ```kotlin + val users = supabase.auth.admin.retrieveUsers( + page = 1, + perPage = 100 + ) + ``` + - id: create-user + title: 'createUser()' + $ref: '@supabase/gotrue-js.GoTrueAdminApi.createUser' + notes: | + Creates a new user. + - To confirm the user's email address or phone number, set `autoConfirm` to true. Both arguments default to false. + params: + - name: builder + isOptional: false + type: AdminUserBuilder.Email.() -> Unit or AdminUserBuilder.Phone.() -> Unit + description: The builder to create a new user. + subContent: + - name: email/phone + isOptional: true + type: String + description: The new user's email or phone. + - name: password + isOptional: false + type: String + description: The user's password. + - name: autoConfirm + isOptional: true + type: Boolean + description: Whether to auto-confirm the user's email or phone number. + - name: userMetadata + isOptional: true + type: JsonObject? + description: Custom user metadata. + - name: appMetadata + isOptional: true + type: JsonObject? + description: Custom app metadata. + examples: + - id: create-a-new-user-with-email-custom-user-metadata + name: Create user with email + isSpotlight: true + code: | + ```kotlin + val userWithEmail = supabase.auth.admin.createUserWithEmail { + email = "example@email.com" + password = "secretpassword" + userMetadata { + put("name", "John") + } + } + ``` + - id: create-a-new-user-with-phone-custom-user-metadata + name: Create user with phone + isSpotlight: true + code: | + ```kotlin + val userWithPhone = supabase.auth.admin.createUserWithPhone { + phone = "+49123456789" + password = "secretpassword" + userMetadata { + put("name", "John") + } + } + ``` + - id: auto-confirm-the-users-email + name: Auto-confirm the user's email + code: | + ```kotlin + val userWithEmail = supabase.auth.admin.createUserWithEmail { + email = "example@email.com" + password = "secretpassword" + autoConfirm = true + } + ``` + - id: auto-confirm-the-users-phone-number + name: Auto-confirm the user's phone number + code: | + ```kotlin + val userWithPhone = supabase.auth.admin.createUserWithPhone { + phone = "+49123456789" + password = "secretpassword" + autoConfirm = true + } + ``` + - id: delete-user + title: 'deleteUser()' + $ref: '@supabase/gotrue-js.GoTrueAdminApi.deleteUser' + notes: | + Deletes a user from the database. + - The `deleteUser()` method requires the user's ID, which maps to the `auth.users.id` column. + params: + - name: uid + isOptional: false + type: String + description: The id of the user you want to delete. + examples: + - id: removes-a-user + name: Removes a user + isSpotlight: true + code: | + ```kotlin + supabase.auth.admin.deleteUser(uid = "uid") + ``` + + - id: invite-user-by-email + title: 'inviteUserByEmail()' + $ref: '@supabase/gotrue-js.GoTrueAdminApi.inviteUserByEmail' + notes: | + Sends an invite link to the user's email address. + params: + - name: email + isOptional: false + type: String + description: The email to send the invite to. + - name: redirectTo + isOptional: true + type: String + description: The redirect url to use. If you don't specify this, the platform specific will be used, like deeplinks on android. + - name: data + isOptional: true + type: JsonObject + description: Custom data to create the user with. + examples: + - id: invite-a-user + name: Invite a user + isSpotlight: true + code: | + ```kotlin + supabase.auth.admin.inviteUserByEmail( + email = "example@email.com", + //optional: + redirectTo = "https://example.com/redirect", + data = buildJsonObject { + put("custom", "value") + } + ) + ``` + + - id: generate-link + title: 'generateLink()' + $ref: '@supabase/gotrue-js.GoTrueAdminApi.generateLink' + notes: | + Generates email links and OTPs to be sent via a custom email provider. + params: + - name: type + isOptional: false + type: LinkType + description: The type of link to generate, e.g. `LinkType.Signup`. + - name: redirectTo + isOptional: true + type: String + description: The redirect url to use. If you don't specify this, the platform specific will be used, like deeplinks on android. + - name: config + isOptional: true + type: C.() -> Unit + description: The builder to create a new link. + examples: + - id: generate-a-signup-link + name: Generate a signup link + isSpotlight: true + code: | + ```kotlin + val (url, user) = supabase.auth.admin.generateLinkFor(LinkType.Signup) { + email = "example@email.com" + password = "secretpassword" + } + ``` + - id: generate-an-invite-link + name: Generate an invite link + isSpotlight: false + code: | + ```kotlin + val (url, user) = supabase.auth.admin.generateLinkFor(LinkType.Invite) { + email = "example@email.com" + } + ``` + - id: generate-a-magic-link + name: Generate a magic link + isSpotlight: false + code: | + ```kotlin + val (url, user) = supabase.auth.admin.generateLinkFor(LinkType.MagicLink) { + email = "example@email.com" + } + ``` + - id: generate-a-recovery-link + name: Generate a recovery link + isSpotlight: false + code: | + ```kotlin + val (url, user) = supabase.auth.admin.generateLinkFor(LinkType.Recovery) { + email = "example@email.com" + } + ``` + - id: generate-links-to-change-current-email-address + name: Generate links to change current email address + isSpotlight: false + code: | + ```kotlin + // generate an email change link to be sent to the current email address + val (url, user) = supabase.auth.admin.generateLinkFor(LinkType.EmailChangeCurrent) { + email = "example@email.com" + newEmail = "newEmail@email.com" + } + + // generate an email change link to be sent to the new email address + val (url, user) = supabase.auth.admin.generateLinkFor(LinkType.EmailChangeNew) { + email = "example@email.com" + newEmail = "newEmail@email.com" + } + ``` + + - id: update-user-by-id + title: 'updateUserById()' + $ref: '@supabase/gotrue-js.GoTrueAdminApi.updateUserById' + notes: | + Updates the user data. + params: + - name: uid + isOptional: false + type: String + description: The id of the user you want to update. + - name: builder + isOptional: false + type: AdminUserUpdateBuilder.() -> Unit + description: The builder to update the user. + subContent: + - name: email + isOptional: true + type: String + description: The new email. + - name: phone + isOptional: true + type: String + description: The new phone number. + - name: password + isOptional: true + type: String + description: The new password. + - name: userMetadata + isOptional: true + type: JsonObject? + description: Custom user metadata. + - name: appMetadata + isOptional: true + type: JsonObject? + description: Custom app metadata. + - name: emailConfirm + isOptional: true + type: Boolean + description: Whether to confirm the user's email. + - name: phoneConfirm + isOptional: true + type: Boolean + description: Whether to confirm the user's phone number. + - name: banDuration + isOptional: true + type: String + description: The format for the ban duration follows a strict sequence of decimal numbers with a unit suffix. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + - name: role + isOptional: true + type: String + description: The `role` claim set in the user's access token JWT. When a user signs up, this role is set to `authenticated` by default. You should only modify the `role` if you need to provision several levels of admin access that have different permissions on individual columns in your database. + examples: + - id: updates-a-users-email + name: Updates a user's email + isSpotlight: false + code: | + ```kotlin + supabase.auth.admin.updateUserById(uid = "id") { + email = "example@email.com" + } + ``` + - id: updates-a-users-password + name: Updates a user's password + isSpotlight: false + code: | + ```js + supabase.auth.admin.updateUserById(uid = "id") { + password = "password" + } + ``` + - id: updates-a-users-metadata + name: Updates a user's metadata + isSpotlight: true + code: | + ```kotlin + supabase.auth.admin.updateUserById(uid = "id") { + userMetadata = buildJsonObject { + put("key", "value") + } + } + ``` + - id: updates-a-users-app-metadata + name: Updates a user's app_metadata + isSpotlight: false + code: | + ```kotlin + supabase.auth.admin.updateUserById(uid = "id") { + appMetadata = buildJsonObject { + put("key", "value") + } + } + ``` + - id: confirms-a-users-email-address + name: Confirms a user's email address + isSpotlight: false + code: | + ```kotlin + supabase.auth.admin.updateUserById(uid = "id") { + emailConfirm = true + } + ``` + - id: confirms-a-users-phone-number + name: Confirms a user's phone number + isSpotlight: false + code: | + ```kotlin + supabase.auth.admin.updateUserById(uid = "id") { + phoneConfirm = true + } + ``` + - id: mfa-list-factors + title: 'mfa.listFactors()' + notes: | + Lists all factors associated to a user. + params: + - name: uid + isOptional: false + type: String + description: The id of the user you want to list factors for. + examples: + - id: list-factors + name: List all factors for a user + isSpotlight: true + code: | + ```kotlin + const factors = supabase.auth.admin.retrieveFactors(uid = "id") + ``` + - id: mfa-delete-factor + title: 'mfa.deleteFactor()' + notes: | + Deletes a factor on a user. This will log the user out of all active sessions if the deleted factor was verified. + params: + - name: uid + isOptional: false + type: String + description: The id of the user you want to delete a factor for. + - name: factorId + isOptional: false + type: String + description: The id of the factor you want to delete. + examples: + - id: delete-factor + name: Delete a factor for a user + isSpotlight: true + code: | + ```kotlin + supabase.auth.admin.deleteFactor(uid = "id", factorId = "factor_id") + ``` + - id: invoke + title: 'invoke()' + description: | + Invokes a Supabase Function. See the [guide](/docs/guides/functions) for details on writing Functions. + - When invoking a function with parameters, you have to provide a [serializable value](/docs/reference/kotlin/installing#serialization) in the function parameter. + notes: | + - Requires an Authorization header. + params: + - name: function + isOptional: false + type: String + description: The name of the function to invoke. + - name: body + isOptional: true + type: T + description: The body to send with the request. T can be any serializable type. + - name: region + isOptional: true + type: FunctionRegion + description: The region where the function is invoked. Defaults to `Functions.Config#defaultRegion`. + - name: headers + isOptional: true + type: Headers + description: The headers to send with the request. + examples: + - id: basic-invocation + name: Basic invocation + isSpotlight: true + code: | + ```kotlin + val response = supabase.functions.invoke("function_name") + + // Decode the response body to a serializable class + val data = response.body() + ``` + - id: basic-invocation-with-body + name: Basic invocation with body + isSpotlight: true + code: | + ```kotlin + supabase.functions.invoke( + function = "function_name", + body = buildJsonObject { + put("foo", "bar") + }, + headers = Headers.build { + append(HttpHeaders.ContentType, "application/json") + } + ) + ``` + description: | + The return type of the invoking methods is an `HttpResponse`. You can access the response body via `response.body()`, where `T` can be any serializable type. + - id: reuse-function + name: Reuse function by saving it to a variable + isSpotlight: true + code: | + ```kotlin + val function = supabase.functions.buildEdgeFunction( + function = "function", + headers = Headers.build { + /*Default headers*/ + //when you are sending a body you may want to add this header: + append(HttpHeaders.ContentType, "application/json") + } + ) + //invoke it: + function() + //invoke it with a body: + function(body) + //invoke it with custom request options: + function(body) { + header("Header", "Value") + parameter("Key", "Value") //url parameter + } + ``` + - id: stream + title: 'stream()' + description: | + Return real-time data from your table as a [Flow](https://kotlinlang.org/docs/flow.html). + - Realtime is disabled by default for new tables. You can turn it on by [managing replication](/docs/guides/realtime/postgres-changes#replication-setup). + - `selectAsFlow` and `selectSingleValueAsFlow` will emit the initial data and then listen for changes. + - Takes in a `filter` parameter to filter the data and a `primaryKey` parameter to cache the data by the primary key. + - This method requires both the `Realtime` and `Postgrest` plugins to be installed. + - The type parameter `T` must be a [serializable class](/docs/reference/kotlin/installing#serialization). + - If you want more control over the realtime updates, you can use the `Realtime` plugin directly. + params: + - name: primaryKey + isOptional: false + type: KProperty1 or PrimaryKey + description: The primary key to cache the data by. Can be a property reference or a custom primary key. + - name: channelName + isOptional: true + type: String + description: The name of the channel to use for the realtime updates. If null, a channel name following the format "schema:table:id" will be used + - name: filter + isOptional: true + type: PostgrestFilterBuilder.() -> Unit or FilterOperation + description: The filter to apply to the data. + examples: + - id: select-as-flow + name: Listen for changes in multiple rows + isSpotlight: true + code: | + ```kotlin + val flow: Flow> = supabase.from("countries").selectAsFlow(Country::id) + flow.collect { + for (country in it) { + println(country.name) + } + } + ``` + - id: select-as-flow-filter + name: Listen for changes in multiple rows with a filter + isSpotlight: true + code: | + ```kotlin + val flow: Flow> = supabase.from("countries").selectAsFlow( + Country::id, + filter = FilterOperation("name", FilterOperator.ILIKE, "a%") + ) + flow.collect { + for (country in it) { + println(country.name) + } + } + ``` + - id: select-single-value-as-flow + name: Listen for changes in a single row + isSpotlight: true + code: | + ```kotlin + val flow: Flow = supabase.from("countries").selectSingleValueAsFlow(Country::id) { + //You can use the same filter methods as in the `select` method, but the result is limited to a single row + Country::id eq 1 + //or + eq("id", 1) + } + flow.collect { + println("My country is $it") + } + ``` + description: | + The flow will end if the row is deleted. + - id: subscribe + description: | + Subscribe to realtime changes in your database. + title: 'on().subscribe()' + notes: | + - Realtime is disabled by default for new Projects for better database performance and security. You can turn it on by [managing replication](/docs/guides/database/api#managing-realtime). + - If you want to receive the "previous" data for updates and deletes, you will need to set `REPLICA IDENTITY` to `FULL`, like this: `ALTER TABLE your_table REPLICA IDENTITY FULL;` + - When using a method with a generic type like `track`, `broadcast` or `broadcastFlow`, you have to provide a [serializable class](/docs/reference/kotlin/installing#serialization) as the type parameter. + - Presence, Broadcast and Database updates are sent through a [Flow](https://kotlinlang.org/docs/flow.html) + examples: + - id: liste-to-broadcasts + name: Listen to broadcasts + code: | + ```kotlin + @Serializable + data class Message(val content: String, val sender: String) + + val channel = supabase.channel("channelId") { + // optional config + } + + val broadcastFlow = channel.broadcastFlow(event = "message") + + // Collect the flow + broadcastFlow.onEach { // it: Message + println(it) + }.launchIn(coroutineScope) // launch a new coroutine to collect the flow + + channel.subscribe(blockUntilSubscribed = true) + + channel.broadcast(event = "message", Message("I joined!", "John")) + ``` + - id: listen-to-presence-updates + name: Listen to presence updates + code: | + ```kotlin + @Serializable + data class PresenceState(val username: String) + + val channel = supabase.channel("channelId") { + //optional config + } + + //if you want more control and want to receive the raw data, use the `presenceChangeFlow` method + val presenceFlow = channel.presenceDataFlow() + + //Collect the flow + presenceFlow.onEach { + for (presence in it) { + println(presence.username) + } + }.launchIn(coroutineScope) // launch a new coroutine to collect the flow + + channel.subscribe(blockUntilSubscribed = true) + //send your own state + channel.track(PresenceState(username = "John")) + description: | + You have two methods you can use to listen for presence updates: + - `presenceDataFlow` takes care of caching presences based on their id and automatically emits the updated list. + - `presenceChangeFlow` emits the raw data received by realtime. Can be used if more control is required. + - id: listen-to-all-database-changes + name: Listen to all database changes + code: | + ```kotlin + val channel = supabase.channel("channelId") { + //optional config + } + val changeFlow = channel.postgresChangeFlow(schema = "public") + + //Collect the flow + changeFlow.onEach { + when(it) { + is PostgresAction.Delete -> println("Deleted: ${it.oldRecord}") + is PostgresAction.Insert -> println("Inserted: ${it.record}") + is PostgresAction.Select -> println("Selected: ${it.record}") + is PostgresAction.Update -> println("Updated: ${it.oldRecord} with ${it.record}") + } + }.launchIn(coroutineScope) // launch a new coroutine to collect the flow + + channel.subscribe() + ``` + - id: listen-to-a-specific-table + name: Listen to a specific table + code: | + ```kotlin + val channel = supabase.channel("channelId") { + //optional config + } + + //This flow will automatically emit the initial data of all messages (which can be restricted by specifying a filter) and then listens for updates. + //The data is cached automatically by the primary key you specify + val users: Flow> = channel.postgresListDataFlow(schema = "public", table = "users", primaryKey = User::id) + + //If you want more control and want to receive the raw data use the `postgresChangeFlow` method: + val changeFlow = channel.postgresChangeFlow(schema = "public") { + table = "users" + } + + //Collect the flow + changeFlow.onEach { + when(it) { + is PostgresAction.Delete -> println("Deleted: ${it.oldRecord}") + is PostgresAction.Insert -> println("Inserted: ${it.record}") + is PostgresAction.Select -> println("Selected: ${it.record}") + is PostgresAction.Update -> println("Updated: ${it.oldRecord} with ${it.record}") + } + }.launchIn(coroutineScope) // launch a new coroutine to collect the flow + + channel.subscribe() + ``` + description: | + You have two methods you can use if you want to listen for database updates on a specific table: + - `postgresListDataFlow` fetches the initial data of all database entries matching your filter (or all entries if you didn't specify one) and then caches the data based on their primary key. Inserts, Updates and Deletes are handled automatically and you will get the updated list through the flow. + - `postgresChangeFlow` emits the raw data received by realtime. Events can be filtered by specifying a `PostgresAction` type parameter (see examples), or all by specifying only `PostgresAction`. Can be used if more control is required. + - id: listen-to-inserts + name: Listen to inserts + code: | + ```kotlin + val channel = supabase.channel("channelId") { + //optional config + } + val changeFlow = channel.postgresChangeFlow(schema = "public") { + table = "users" + } + + //Collect the flow + changeFlow.onEach { + println(it.record) + }.launchIn(coroutineScope) // launch a new coroutine to collect the flow + + channel.subscribe() + ``` + - id: listen-to-updates + name: Listen to updates + description: | + By default, Supabase will send only the updated record. If you want to receive the previous values as well you can + enable full replication for the table you are listening too: + + ```sql + alter table "your_table" replica identity full; + ``` + code: | + ```kotlin + val channel = supabase.channel("channelId") { + //optional config + } + + val changeFlow = channel.postgresChangeFlow(schema = "public") { + table = "users" + } + + //Collect the flow + changeFlow.onEach { + println(it.record) + println(it.oldRecord) + }.launchIn(coroutineScope) // launch a new coroutine to collect the flow + + channel.subscribe() + ``` + - id: listen-to-deletes + name: Listen to deletes + description: | + By default, Supabase does not send deleted records. If you want to receive the deleted record you can + enable full replication for the table you are listening too: + + ```sql + alter table "your_table" replica identity full; + ``` + code: | + ```kotlin + val channel = supabase.channel("channelId") { + //optional config + } + + val changeFlow = channel.postgresChangeFlow(schema = "public") { + table = "users" + } + + //Collect the flow + changeFlow.onEach { + println(it.oldRecord) + }.launchIn(coroutineScope) // launch a new coroutine to collect the flow + + channel.subscribe() + ``` + - id: listening-to-row-level-changes + name: Listen to row level changes + code: | + ```kotlin + val channel = supabase.channel("channelId") { + //optional config + } + + //This flow will automatically emit the user's initial data and then emits new values on update. The flow ends on deletion + val user: Flow = channel.postgresSingleDataFlow(schema = "public", table = "users", primaryKey = User::id) { + //This is the same filter as used in postgrest, so you could use complex queries, but only one entry is fetched + eq("id", 1) + } + + //If you want more control, use the `postgresChangeFlow` method which works the same as the other examples + val changeFlow = channel.postgresChangeFlow(schema = "public") { + table = "users" + filter("id", FilterOperator.EQ, 1) + } + + //Collect the flow + changeFlow.onEach { + println(it.oldRecord) + }.launchIn(coroutineScope) // launch a new coroutine to collect the flow + + channel.subscribe() + ``` + description: | + You have two methods you can use if you want to listen for updates on a specific database row: + - `postgresSingleDataFlow` fetches the initial data matching the filter and then emits updates based on realtime events. + - `postgresChangeFlow` emits the raw data received by realtime. To filter the realtime queries, use the `filter` method. + + - id: remove-channel + description: | + Unsubscribes and removes Realtime channel from Realtime client. + title: 'removeChannel()' + notes: | + - Removing a channel is a great way to maintain the performance of your project's Realtime service as well as your database if you're listening to Postgres changes. + - Supabase will automatically handle cleanup 30 seconds after a client is disconnected, but unused channels may cause degradation as more clients are simultaneously subscribed. + - If you removed all channels, the client automatically disconnects from the Realtime websocket. This can be disabled in the Realtime config by setting `disconnectOnNoSubscriptions` to false. + examples: + - id: removes-a-channel + name: Remove a channel + isSpotlight: true + code: | + ```kotlin + val channel = supabase.channel("channelId") { + //optional config + } + //... + supabase.realtime.removeChannel(channel) + ``` + - id: unsubscribe-channel + name: Unsubscribe from a channel + isSpotlight: true + code: | + ```kotlin + val channel = supabase.channel("channelId") { + //optional config + } + //... + channel.unsubscribe() + ``` + - id: remove-all-channels + title: removeAllChannels() + $ref: '@supabase/supabase-js.index.SupabaseClient.removeAllChannels' + notes: | + Unsubscribes and removes all Realtime channels from Realtime client. + - Removing channels is a great way to maintain the performance of your project's Realtime service as well as your database if you're listening to Postgres changes. Supabase will automatically handle cleanup 30 seconds after a client is disconnected, but unused channels may cause degradation as more clients are simultaneously subscribed. + - If you removed all channels, the client automatically disconnects from the Realtime websocket. This can be disabled in the Realtime config by setting `disconnectOnNoSubscriptions` to false. + examples: + - id: remove-all-channels + name: Remove all channels + isSpotlight: true + code: | + ```kotlin + supabase.realtime.removeAllChannels() + ``` + - id: get-channels + title: getChannels() + $ref: '@supabase/supabase-js.index.SupabaseClient.getChannels' + notes: | + Returns all Realtime channels. + examples: + - id: get-all-channels + name: Get all channels + isSpotlight: true + code: | + ```kotlin + val channels = supabase.realtime.subscriptions.entries + ``` + - id: list-buckets + title: listBuckets() + $ref: '@supabase/storage-js.packages/StorageBucketApi.default.listBuckets' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: `select` + - `objects` table permissions: none + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + examples: + - id: list-buckets + name: List buckets + isSpotlight: true + code: | + ```kotlin + val buckets = supabase.storage.retrieveBuckets() + ``` + + - id: get-bucket + title: getBucket() + $ref: '@supabase/storage-js.packages/StorageBucketApi.default.getBucket' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: `select` + - `objects` table permissions: none + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + examples: + - id: get-bucket + name: Get bucket + isSpotlight: true + code: | + ```kotlin + val bucket = supabase.storage.retrieveBucketById(bucketId = "avatars") + ``` + + - id: create-bucket + title: createBucket() + $ref: '@supabase/storage-js.packages/StorageBucketApi.default.createBucket' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: `insert` + - `objects` table permissions: none + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: id + isOptional: false + type: String + description: The id of the bucket you want to create. + - name: builder + isOptional: true + type: BucketBuilder.() -> Unit + description: The builder to create a new bucket. + subContent: + - name: public + isOptional: true + type: Boolean + description: Whether the bucket is public or not. + - name: fileSizeLimit + isOptional: true + type: FileSizeLimit + description: The maximum file size. + examples: + - id: create-bucket + name: Create bucket + isSpotlight: true + code: | + ```kotlin + supabase.storage.createBucket(id = "icons") { + public = true + fileSizeLimit = 5.megabytes + } + ``` + - id: update-bucket + title: updateBucket() + $ref: '@supabase/storage-js.packages/StorageBucketApi.default.updateBucket' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: `select` and `update` + - `objects` table permissions: none + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: id + isOptional: false + type: String + description: The id of the bucket you want to create. + - name: builder + isOptional: true + type: BucketBuilder.() -> Unit + description: The builder to create a new bucket. + subContent: + - name: public + isOptional: true + type: Boolean + description: Whether the bucket is public or not. + - name: fileSizeLimit + isOptional: true + type: FileSizeLimit + description: The maximum file size. + examples: + - id: update-bucket + name: Update bucket + isSpotlight: true + code: | + ```kotlin + supabase.storage.updateBucket("cards") { + public = false + fileSizeLimit = 20.megabytes + allowedMimeTypes(ContentType.Image.PNG, ContentType.Image.JPEG) + } + ``` + + - id: empty-bucket + title: emptyBucket() + $ref: '@supabase/storage-js.packages/StorageBucketApi.default.emptyBucket' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: `select` + - `objects` table permissions: `select` and `delete` + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: bucketId + isOptional: false + type: String + description: The id of the bucket you want to empty. + examples: + - id: empty-bucket + name: Empty bucket + isSpotlight: true + code: | + ```kotlin + supabase.storage.emptyBucket(bucketId = "icons") + ``` + - id: delete-bucket + title: deleteBucket() + $ref: '@supabase/storage-js.packages/StorageBucketApi.default.deleteBucket' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: `select` and `delete` + - `objects` table permissions: none + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: bucketId + isOptional: false + type: String + description: The id of the bucket you want to delete. + examples: + - id: delete-bucket + name: Delete bucket + isSpotlight: true + code: | + ```kotlin + supabase.storage.deleteBucket(bucketId = "icons") + ``` + + - id: from-upload + title: from.upload() + $ref: '@supabase/storage-js.packages/StorageFileApi.default.upload' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: `insert` + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + - Resumable uploads use a `Disk` cache by default to store the upload urls. You can customize that in the Auth config by changing the `resumable.cache` property. + params: + - name: path + isOptional: false + type: String + description: The path of the file you want to upload. + - name: data + isOptional: false + type: ByteArray + description: The data of the file you want to upload. + - name: options + isOptional: true + type: UploadOptionBuilder.() -> Unit + description: Additional options for the upload. + subContent: + - name: upsert + isOptional: true + type: Boolean + description: Whether to overwrite the file if it already exists. + - name: contentType + isOptional: true + type: ContentType + description: The content type of the file. Will be automatically inferred using the file extension if not provided. + examples: + - id: upload-file + name: Upload file + isSpotlight: true + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + bucket.upload("myIcon.png", byteArray) { + upsert = false + } + //on JVM you can use java.io.File + bucket.upload("myIcon.png", file) { + upsert = false + } + ``` + - id: upload-file-with-progress + name: Upload file with progress + isSpotlight: true + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + bucket.uploadAsFlow("test.png", byteArrayOf()).collect { + when(it) { + is UploadStatus.Progress -> println("Progress: ${it.totalBytesSend.toFloat() / it.contentLength * 100}%") + is UploadStatus.Success -> println("Success") + } + } + ``` + - id: create-resumable-upload + name: Create resumable upload + isSpotlight: true + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + //JVM/Android: + val upload = bucket.resumable.createOrContinueUpload("icon.png", File("icon.png")) + //Other platforms: + val upload = bucket.resumable.createOrContinueUpload(data = byteArray, source = "this is for continuing previous uploads later", path = "icon.png") + val upload = bucket.resumable.createOrContinueUpload( //Probably better to write an extension function + channel = { offset -> /* create ByteReadChannel and seek to offset */ }, + source = "this is for continuing previous uploads later", + size = dataSize, + path = "icon.png" + ) + ``` + - id: start-resumable-upload + name: Start and resumable upload + isSpotlight: true + code: | + ```kotlin + upload.startOrResumeUploading() + ``` + - id: pause-resumable-upload + name: Pause resumable upload + code: | + ```kotlin + upload.pause() + ``` + - id: cancel-resumable-upload + name: Cancel resumable upload + code: | + ```kotlin + upload.cancel() + ``` + description: | + This will also remove the upload url from the cache + - id: listen-to-upload-state + name: Listen to the resumable upload state + code: | + ```kotlin + upload.stateFlow.collect { + println("Progress: ${it.progress * 100}%") + println("Paused: ${it.paused}") + println("Is done: ${it.isDone}") + } + ``` + - id: continue-previous-upload + name: Continue previous uploads + isSpotlight: true + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + + //only on JVM/Android: + bucket.resumable.continuePreviousFileUploads() + .map { it.await() } //await all uploads. This just makes sure the uploads have an update-to-date url. You can also do this in parallel + .forEach { upload -> + upload.startOrResumeUploading() + } + + //on other platforms you may have to continue uploads from the source (Probably better to write an extension function): + bucket.resumable.continuePreviousUploads { source, offset -> + //create ByteReadChannel from source and seek to offset + } + .map { it.await() } //await all uploads. This just makes sure the uploads have an update-to-date url. You can also do this in parallel + .forEach { upload -> + upload.startOrResumeUploading() + } + ``` + + - id: from-update + title: from.update() + $ref: '@supabase/storage-js.packages/StorageFileApi.default.update' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: `update` and `select` + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: path + isOptional: false + type: String + description: The path of the file you want to upload. + - name: data + isOptional: false + type: ByteArray + description: The data of the file you want to upload. + - name: options + isOptional: true + type: UploadOptionBuilder.() -> Unit + description: Additional options for the upload. + subContent: + - name: upsert + isOptional: true + type: Boolean + description: Whether to overwrite the file if it already exists. + - name: contentType + isOptional: true + type: ContentType + description: The content type of the file. Will be automatically inferred using the file extension if not provided. + examples: + - id: update-file + name: Update file + isSpotlight: true + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + bucket.update("myIcon.png", byteArray) { + upsert = false + } + //on JVM you can use java.io.File + bucket.update("myIcon.png", file) { + upsert = false + } + ``` + + - id: from-move + title: from.move() + $ref: '@supabase/storage-js.packages/StorageFileApi.default.move' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: `update` and `select` + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: from + isOptional: false + type: String + description: The path of the file you want to move. + - name: to + isOptional: false + type: String + description: The new path of the file. + - name: destinationBucket + isOptional: true + type: String + description: The destination bucket of the file. + examples: + - id: move-file + name: Move file + isSpotlight: true + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + bucket.move("icon1.png", "icon2.png") + ``` + - id: from-copy + title: from.copy() + $ref: '@supabase/storage-js.packages/StorageFileApi.default.copy' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: `insert` and `select` + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: from + isOptional: false + type: String + description: The path of the file you want to copy. + - name: to + isOptional: false + type: String + description: The new path of the file. + - name: destinationBucket + isOptional: true + type: String + description: The destination bucket of the file. + examples: + - id: copy-file + name: Copy file + isSpotlight: true + code: | + ```kotlin + supabase.storage.from("test").copy(from = "avatar.png", to = "avatar2.png") + ``` + - id: from-create-signed-url + title: from.createSignedUrl() + $ref: '@supabase/storage-js.packages/StorageFileApi.default.createSignedUrl' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: `select` + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: path + isOptional: false + type: String + description: The path of the file you want to create a signed url for. + - name: expiresIn + isOptional: false + type: Duration + description: The duration the signed url should be valid for. + - name: builder + isOptional: true + type: ImageTransformation.() -> Unit + description: The transformation to apply to the image. + subContent: + - name: width + isOptional: true + type: Int + description: The width of the image. + - name: height + isOptional: true + type: Int + description: The height of the image. + - name: resize + isOptional: true + type: Resize + description: The resize mode of the image. + - name: quality + isOptional: true + type: Int + description: The quality of the image. (Percentage 1-100, defaults to 80) + - name: format + isOptional: true + type: String + description: Specify in which format you want the image to receive. (Defaults to 'origin', which means the original format) + examples: + - id: create-signed-url + name: Create Signed URL + isSpotlight: true + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + val url = bucket.createSignedUrl(path = "icon.png", expiresIn = 3.minutes) + ``` + - id: create-signed-url-with-transformation + name: Create Signed URL with transformation + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + val url = bucket.createSignedUrl(path = "icon.png", expiresIn = 3.minutes) { + size(100, 100) + fill() + quality = 80 + } + ``` + - id: from-create-signed-urls + title: from.createSignedUrls() + notes: | + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: `select` + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: expiresIn + isOptional: false + type: Duration + description: The duration the signed url should be valid for. + - name: paths + isOptional: false + type: vararg String + description: The paths of the files you want to create signed urls for. + examples: + - id: create-signed-urls + name: Create Signed URLs + isSpotlight: true + code: | + ```kotlin + val urls = supabase.storage.from("avatars").createSignedUrls(20.minutes, "avata1.jpg", "avatar2.jpg") + ``` + - id: from-create-signed-upload-url + title: from.createSignedUploadUrl() + $ref: '@supabase/storage-js.packages/StorageFileApi.default.createSignedUploadUrl' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: `insert` + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: path + isOptional: false + type: String + description: The path of the file you want to upload. + - name: upsert + isOptional: true + type: Boolean + description: Whether to overwrite the file if it already exists. + examples: + - id: create-signed-upload-url + name: Create Signed Upload URL + isSpotlight: true + code: | + ```kotlin + val url = supabase.storage.from("avatars").createSignedUploadUrl("avatar.png") + ``` + - id: from-upload-to-signed-url + title: from.uploadToSignedUrl() + $ref: '@supabase/storage-js.packages/StorageFileApi.default.uploadToSignedUrl' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: none + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: path + isOptional: false + type: String + description: The path of the file you want to upload. + - name: token + isOptional: false + type: String + description: The token you received from `createSignedUploadUrl`. + - name: data + isOptional: true + type: ByteArray + description: The data of the file you want to upload. + - name: options + isOptional: true + type: UploadOptionBuilder.() -> Unit + description: Additional options for the upload. + subContent: + - name: upsert + isOptional: true + type: Boolean + description: Whether to overwrite the file if it already exists. + - name: contentType + isOptional: true + type: ContentType + description: The content type of the file. Will be automatically inferred using the file extension if not provided. + examples: + - id: upload-to-signed-url + name: Upload to a signed URL + isSpotlight: true + code: | + ```kotlin + supabase.storage.from("avatars").uploadToSignedUrl(path = "avatar.jpg", token = "token-from-createSignedUploadUrl", data = bytes) + //or on JVM: + supabase.storage.from("avatars").uploadToSignedUrl(path = "avatar.jpg", token = "token-from-createSignedUploadUrl", file = File("avatar.jpg")) + ``` + - id: from-get-public-url + title: from.getPublicUrl() + $ref: '@supabase/storage-js.packages/StorageFileApi.default.getPublicUrl' + notes: | + - The bucket needs to be set to public, either via [updateBucket()](/docs/reference/kotlin/storage-updatebucket) or by going to Storage on [supabase.com/dashboard](https://supabase.com/dashboard), clicking the overflow menu on a bucket and choosing "Make public" + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: none + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: path + isOptional: false + type: String + description: The path of the file you want to get the public url for. + examples: + - id: returns-the-url-for-an-asset-in-a-public-bucket + name: Returns the URL for an asset in a public bucket + isSpotlight: true + code: | + ```kotlin + val url = supabase.storage.from("public-bucket").publicUrl("folder/avatar1.png") + ``` + - id: transform-asset-in-public-bucket + name: Returns the URL for an asset in a public bucket with transformations + isSpotlight: true + code: | + ```kotlin + val url = supabase.storage.from("public-bucket").publicRenderUrl("folder/avatar1.png") { + size(100, 100) + } + ``` + + - id: from-download + title: from.download() + $ref: '@supabase/storage-js.packages/StorageFileApi.default.download' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: `select` + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: path + isOptional: false + type: String + description: The path of the file you want to download. + - name: options + isOptional: true + type: DownloadOptionBuilder.() -> Unit + description: Additional options for the download. + subContent: + - name: transform + isOptional: true + type: ImageTransformation.() -> Unit = {} + description: Image transformation. + examples: + - id: download-file-authenticated + name: Download file from non-public bucket + isSpotlight: true + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + val bytes = bucket.downloadAuthenticated("test.png") + //or on JVM: + bucket.downloadAuthenticatedTo("test.png", File("test.png")) + ``` + - id: download-file-public + name: Download file from public bucket + isSpotlight: true + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + val bytes = bucket.downloadPublic("test.png") + //or on JVM: + bucket.downloadPublicTo("test.png", File("test.png")) + ``` + - id: download-with-transformation + name: Download file with transformation + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + val bytes = bucket.downloadPublic("test.png") { + transform { + size(100, 100) + fill() + quality = 100 + } + } + //or on JVM: + bucket.downloadPublicTo("test.png", File("test.png")) { + transform { + size(100, 100) + fill() + quality = 100 + } + } + ``` + - id: download-with-progress + name: Download file with progress + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + bucket.downloadAuthenticatedAsFlow("icon.png").collect { + when(it) { + is DownloadStatus.ByteData -> println("Downloaded ${it.data.size} bytes") + is DownloadStatus.Progress -> println("Downloaded ${it.totalBytesReceived.toFloat() / it.contentLength * 100}%") + DownloadStatus.Success -> println("Downloaded successfully") + } + } + //or on JVM: + bucket.downloadAuthenticatedToAsFlow("icon.png", File("icon.png")).collect { + when(it) { + is DownloadStatus.Progress -> println("Downloaded ${it.totalBytesReceived.toFloat() / it.contentLength * 100}%") + DownloadStatus.Success -> println("Downloaded successfully") + else -> {} //The ByteData status will never occur as we are writing directly to a file + } + } + ``` + - id: from-remove + title: from.remove() + $ref: '@supabase/storage-js.packages/StorageFileApi.default.remove' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: `delete` and `select` + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + params: + - name: paths + isOptional: false + type: vararg String + description: The paths of the files you want to remove. + examples: + - id: delete-file + name: Delete file + isSpotlight: true + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + bucket.delete("test.png", "test2.png") + ``` + + - id: from-list + title: from.list() + $ref: '@supabase/storage-js.packages/StorageFileApi.default.list' + notes: | + - RLS policy permissions required: + - `buckets` table permissions: none + - `objects` table permissions: `select` + - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works + examples: + - id: list-files-in-a-bucket + name: List files in a bucket + isSpotlight: true + code: | + ```kotlin + val bucket = supabase.storage.from("avatars") + val files = bucket.list() + ``` diff --git a/packages/common/constants/client-libraries.ts b/packages/common/constants/client-libraries.ts index 45c4d367006..10df14dd5db 100644 --- a/packages/common/constants/client-libraries.ts +++ b/packages/common/constants/client-libraries.ts @@ -136,8 +136,8 @@ export const CLIENT_LIBRARIES = [ url: 'https://github.com/supabase-community/supabase-kt/tree/master/Postgrest', }, { - name: 'gotrue-kt', - url: 'https://github.com/supabase-community/supabase-kt/tree/master/GoTrue', + name: 'auth-kt', + url: 'https://github.com/supabase-community/supabase-kt/tree/master/Auth', }, { name: 'storage-kt', From bc203b9078c27bea84bbd59908ae12aba5d1f995 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 2 Oct 2024 14:22:46 +0800 Subject: [PATCH 12/13] Chore/user management fixes (#29635) * Fix waiting for verification column as cell was not re-rendering after users are loaded * Fix waiting for verification column as cell was not re-rendering after users are loaded * Fix grid, overview panel and linking to providers for SAML users --- .../Auth/AuthProvidersForm/ProviderForm.tsx | 2 +- .../interfaces/Auth/Users/UserOverview.tsx | 11 +++++++---- .../interfaces/Auth/Users/Users.utils.tsx | 15 ++++++++++----- .../components/interfaces/Auth/Users/UsersV2.tsx | 8 +++++--- .../studio/components/ui/Forms/FormsContainer.tsx | 6 ++++-- .../studio/pages/project/[ref]/auth/providers.tsx | 2 +- 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx index 02d85706700..13a620195e6 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersForm/ProviderForm.tsx @@ -184,7 +184,7 @@ const ProviderForm = ({ config, provider }: ProviderFormProps) => { } useEffect(() => { - if (urlProvider === provider.title.toLowerCase()) { + if (urlProvider?.toLowerCase() === provider.title.toLowerCase()) { setOpen(true) ref.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) } diff --git a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx index ef4f296b09f..66e2bf42511 100644 --- a/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UserOverview.tsx @@ -48,9 +48,9 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => { const isAnonUser = user.is_anonymous const isBanned = user.banned_until !== null - const providers = (user.raw_app_meta_data?.providers ?? []).map((provider) => { + const providers = (user.raw_app_meta_data?.providers ?? []).map((provider: string) => { return { - name: provider, + name: provider.startsWith('sso') ? 'SAML' : provider, icon: provider === 'email' ? `${BASE_PATH}/img/icons/email-icon2.svg` @@ -285,10 +285,13 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {

{provider.name}

- Signed in with a {providerName} account via OAuth + Signed in with a {providerName} account via{' '} + {providerName === 'SAML' ? 'SSO' : 'OAuth'}

diff --git a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx index 76ba3bec05b..bc5fbcdde4b 100644 --- a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx +++ b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx @@ -21,7 +21,10 @@ export const isAtBottom = ({ currentTarget }: UIEvent): boolean export const formatUsersData = (users: User[]) => { return users.map((user) => { const provider: string = user.raw_app_meta_data?.provider ?? '' - const providers: string[] = user.raw_app_meta_data?.providers ?? [] + const providers: string[] = (user.raw_app_meta_data?.providers ?? []).map((x: string) => { + if (x.startsWith('sso')) return 'SAML' + return x + }) return { id: user.id, @@ -34,9 +37,11 @@ export const formatUsersData = (users: User[]) => { .map((p) => { return p === 'email' ? `${BASE_PATH}/img/icons/email-icon2.svg` - : providerIconMap[p] - ? `${BASE_PATH}/img/icons/${providerIconMap[p]}.svg` - : undefined + : p === 'SAML' + ? `${BASE_PATH}/img/icons/saml-icon.svg` + : providerIconMap[p] + ? `${BASE_PATH}/img/icons/${providerIconMap[p]}.svg` + : undefined }) .filter(Boolean), // I think it's alright to just check via the main provider since email and phone should be mutually exclusive @@ -207,7 +212,7 @@ export const formatUserColumns = ({ : Array.isArray(value) ? value.join(', ') : value - const isConfirmed = user?.email_confirmed_at || user?.phone_confirmed_at + const isConfirmed = !!user?.confirmed_at if (col.id === 'img') { return ( diff --git a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx index 1cb9d0a34c6..48038aa5908 100644 --- a/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx +++ b/apps/studio/components/interfaces/Auth/Users/UsersV2.tsx @@ -93,6 +93,7 @@ export const UsersV2 = () => { const { data, error, + isSuccess, isLoading, isRefetching, isError, @@ -171,8 +172,9 @@ export const UsersV2 = () => { useEffect(() => { if ( - isSuccessStorage || - (isErrorStorage && (errorStorage as Error).message.includes('data is undefined')) + !isRefetching && + (isSuccessStorage || + (isErrorStorage && (errorStorage as Error).message.includes('data is undefined'))) ) { const columns = formatUserColumns({ config: columnConfiguration ?? [], @@ -186,7 +188,7 @@ export const UsersV2 = () => { } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSuccessStorage, isErrorStorage, errorStorage]) + }, [isSuccess, isRefetching, isSuccessStorage, isErrorStorage, errorStorage]) return (
diff --git a/apps/studio/components/ui/Forms/FormsContainer.tsx b/apps/studio/components/ui/Forms/FormsContainer.tsx index d2368eb4918..f7764c5f3a5 100644 --- a/apps/studio/components/ui/Forms/FormsContainer.tsx +++ b/apps/studio/components/ui/Forms/FormsContainer.tsx @@ -1,6 +1,8 @@ -function FormsContainer(props: { children: React.ReactNode; header?: string }) { +import { cn } from 'ui' + +function FormsContainer(props: { children: React.ReactNode; header?: string; className?: string }) { return ( -
+
{props.header &&

{props.header}

}
{props.children}
diff --git a/apps/studio/pages/project/[ref]/auth/providers.tsx b/apps/studio/pages/project/[ref]/auth/providers.tsx index 272d892082a..0d43687ae8e 100644 --- a/apps/studio/pages/project/[ref]/auth/providers.tsx +++ b/apps/studio/pages/project/[ref]/auth/providers.tsx @@ -15,7 +15,7 @@ const PageLayout: NextPageWithLayout = () => { return } else { return ( - + ) From 3bcbd9cae5141b6a4a2fe54b5e4ef1a004b80faf Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 2 Oct 2024 15:44:10 +0800 Subject: [PATCH 13/13] Fix filtering on SAML provider (#29636) --- .../Auth/AuthProvidersFormValidation.tsx | 2 +- apps/studio/data/auth/users-count-query.ts | 14 +++++++++++--- apps/studio/data/auth/users-infinite-query.ts | 14 +++++++++++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx index 5f051bfc438..753baf9d8e2 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersFormValidation.tsx @@ -1383,7 +1383,7 @@ const PROVIDER_SAML = { $schema: JSON_SCHEMA_VERSION, type: 'object', title: 'SAML 2.0', - link: 'https://supabase.com/docs/guides/auth/enterprise-sso', + link: 'https://supabase.com/docs/guides/auth/enterprise-sso/auth-sso-saml', properties: { SAML_ENABLED: { title: 'Enable SAML 2.0 Single Sign-on', diff --git a/apps/studio/data/auth/users-count-query.ts b/apps/studio/data/auth/users-count-query.ts index a85e80cbbda..1051154f9c8 100644 --- a/apps/studio/data/auth/users-count-query.ts +++ b/apps/studio/data/auth/users-count-query.ts @@ -41,9 +41,17 @@ const getUsersCountSQl = ({ } if (providers && providers.length > 0) { - conditions.push( - `(raw_app_meta_data->>'providers')::jsonb ?| array[${providers.map((p) => `'${p}'`).join(', ')}]` - ) + // [Joshen] This is arguarbly not fully optimized, but at the same time not commonly used + // JFYI in case we do eventually run into performance issues here when filtering for SAML provider + if (providers.includes('saml 2.0')) { + conditions.push( + `(select jsonb_agg(case when value ~ '^sso' then 'sso' else value end) from jsonb_array_elements_text((raw_app_meta_data ->> 'providers')::jsonb)) ?| array[${providers.map((p) => (p === 'saml 2.0' ? `'sso'` : `'${p}'`)).join(', ')}]`.trim() + ) + } else { + conditions.push( + `(raw_app_meta_data->>'providers')::jsonb ?| array[${providers.map((p) => `'${p}'`).join(', ')}]` + ) + } } const combinedConditions = conditions.map((x) => `(${x})`).join(' and ') diff --git a/apps/studio/data/auth/users-infinite-query.ts b/apps/studio/data/auth/users-infinite-query.ts index 95e98727aaa..91b2003ccfb 100644 --- a/apps/studio/data/auth/users-infinite-query.ts +++ b/apps/studio/data/auth/users-infinite-query.ts @@ -56,9 +56,17 @@ const getUsersSQL = ({ } if (providers && providers.length > 0) { - conditions.push( - `(raw_app_meta_data->>'providers')::jsonb ?| array[${providers.map((p) => `'${p}'`).join(', ')}]` - ) + // [Joshen] This is arguarbly not fully optimized, but at the same time not commonly used + // JFYI in case we do eventually run into performance issues here when filtering for SAML provider + if (providers.includes('saml 2.0')) { + conditions.push( + `(select jsonb_agg(case when value ~ '^sso' then 'sso' else value end) from jsonb_array_elements_text((raw_app_meta_data ->> 'providers')::jsonb)) ?| array[${providers.map((p) => (p === 'saml 2.0' ? `'sso'` : `'${p}'`)).join(', ')}]`.trim() + ) + } else { + conditions.push( + `(raw_app_meta_data->>'providers')::jsonb ?| array[${providers.map((p) => `'${p}'`).join(', ')}]` + ) + } } const combinedConditions = conditions.map((x) => `(${x})`).join(' and ')