diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index c0e952b351b45..29b12e7ca05ab 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -2132,6 +2132,7 @@ export const deployment: NavMenuConstant = { name: 'Environments', items: [ { name: 'Managing environments', url: '/guides/deployment/managing-environments' }, + { name: 'Database migrations', url: '/guides/deployment/database-migrations' }, { name: 'Branching', url: '/guides/deployment/branching' }, ], }, diff --git a/apps/docs/content/guides/deployment/database-migrations.mdx b/apps/docs/content/guides/deployment/database-migrations.mdx new file mode 100644 index 0000000000000..0f9ba80f34d1e --- /dev/null +++ b/apps/docs/content/guides/deployment/database-migrations.mdx @@ -0,0 +1,257 @@ +--- +id: 'database-migrations' +title: 'Database Migrations' +description: 'How to manage schema migrations for your Supabase project.' +subtitle: 'How to manage schema migrations for your Supabase project.' +video: 'https://www.youtube-nocookie.com/v/vyHyYpvjaks' +tocVideo: 'vyHyYpvjaks' +--- + +Database schema changes are managed through "migrations". Database migrations are a common way of tracking changes to your database over time. + +## Schema migrations + +For this guide, we'll create a table called `employees` and see how we can make changes to it. + + + + + + + To get started, generate a [new migration](/docs/reference/cli/supabase-migration-new) to store the SQL needed to create our `employees` table. + + + + + +```bash +supabase migration new create_employees_table +``` + + + + + + + + + + + This creates a new migration: supabase/migrations/\ + _create_employees_table.sql. + + To that file, add the SQL to create this `employees` table + + + + +```sql +create table employees ( + id bigint primary key generated always as identity, + name text, + email text, + created_at timestamptz default now() +); +``` + + + + + + + + + + + Now that you have a migration file, you can run this migration and create the `employees` table. + + Use the `reset` command here to reset the database to the current migrations + + + + +```bash +supabase db reset +``` + + + + + + + + + + + Now you can visit your new `employees` table in the Dashboard. + + Next, modify your `employees` table by adding a column for department. Create a new migration file for that. + + + + +```bash +supabase migration new add_department_to_employees_table +``` + + + + + + + + + + + This creates a new migration file: supabase/migrations/\ + _add_department_to_employees_table.sql. + + To that file, add the SQL to create a new department column + + + + +```sql +alter table if exists public.employees +add department text default 'Hooli'; +``` + + + + + + +### Add sample data + +Now that you are managing your database with migrations scripts, it would be great have some seed data to use every time you reset the database. + +For this, you can create a seed script in `supabase/seed.sql`. + + + + + + Insert data into your `employees` table with your `supabase/seed.sql` file. + + + + +```sql +insert into public.employees + (name) +values + ('Erlich Bachman'), + ('Richard Hendricks'), + ('Monica Hall'); +``` + + + + + + + + + + + Reset your database (apply current migrations), and populate with seed data + + + + +```bash +supabase db reset +``` + + + + + + +You should now see the `employees` table, along with your seed data in the Dashboard! All of your database changes are captured in code, and you can reset to a known state at any time, complete with seed data. + +### Diffing changes + +This workflow is great if you know SQL and are comfortable creating tables and columns. If not, you can still use the Dashboard to create tables and columns, and then use the CLI to diff your changes and create migrations. + +Create a new table called `cities`, with columns `id`, `name` and `population`. To see the corresponding SQL for this, you can use the `supabase db diff --schema public` command. This will show you the SQL that will be run to create the table and columns. The output of `supabase db diff` will look something like this: + +``` +Diffing schemas: public +Finished supabase db diff on branch main. + +create table "public"."cities" ( + "id" bigint primary key generated always as identity, + "name" text, + "population" bigint +); + +``` + +Alternately, you can view your table definitions directly from the Table Editor: + +![SQL Definition](/docs/img/guides/cli/sql-definitions.png) + +You can then copy this SQL into a new migration file, and run `supabase db reset` to apply the changes. + +The last step is deploying these changes to a live Supabase project. + +## Deploy your project + +You've been developing your project locally, making changes to your tables via migrations. It's time to deploy your project to the Supabase Platform and start scaling up to millions of users! Head over to [Supabase](https://supabase.com/dashboard) and create a new project to deploy to. + +### Log in to the Supabase CLI + + + +```bash Terminal +supabase login +``` + +```bash npx +npx supabase login +``` + + + +### Link your project + +Associate your project with your remote project using [`supabase link`](/docs/reference/cli/usage#supabase-link). + +```bash +supabase link --project-ref +# You can get from your project's dashboard URL: https://supabase.com/dashboard/project/ + +supabase db pull +# Capture any changes that you have made to your remote database before you went through the steps above +# If you have not made any changes to the remote database, skip this step +``` + +`supabase/migrations` is now populated with a migration in `_remote_schema.sql`. +This migration captures any changes required for your local database to match the schema of your remote Supabase project. + +Review the generated migration file and once happy, apply the changes to your local instance: + +```bash +# To apply the new migration to your local database: +supabase migration up + +# To reset your local database completely: +supabase db reset +``` + + + +There are a few commands required to link your project. We are in the process of consolidating these commands into a single command. Bear with us! + + + +### Deploy database changes + +Deploy any local database migrations using [`db push`](/docs/reference/cli/usage#supabase-db-push): + +```sh +supabase db push +``` + +Visiting your live project on [Supabase](https://supabase.com/dashboard), you'll see a new `employees` table, complete with the `department` column you added in the second migration above. diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx index f26e612eb74d1..d109b86be045a 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx @@ -41,8 +41,11 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { onSuccess: (res) => setValidationResult(res), }) - const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation({ - onError: (error) => toast.error(`Failed to update email templates: ${error.message}`), + const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation({ + onError: (error) => { + setIsSavingTemplate(false) + toast.error(`Failed to update email templates: ${error.message}`) + }, }) const { id, properties } = template @@ -66,13 +69,16 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { const [validationResult, setValidationResult] = useState() const [bodyValue, setBodyValue] = useState((authConfig && authConfig[messageSlug]) ?? '') const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) + const [isSavingTemplate, setIsSavingTemplate] = useState(false) // eslint-disable-next-line react-hooks/exhaustive-deps - const debounceValidateSpam = useCallback(debounce(validateSpam, 1000), []) const spamRules = (validationResult?.rules ?? []).filter((rule) => rule.score > 0) const preventSaveFromSpamCheck = builtInSMTP && spamRules.length > 0 const onSubmit = (values: any, { resetForm }: any) => { + if (!projectRef) return console.error('Project ref is required') + + setIsSavingTemplate(true) const payload = { ...values } // Because the template content uses the code editor which is not a form component @@ -80,17 +86,44 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { delete payload[messageSlug] if (messageProperty) payload[messageSlug] = bodyValue - updateAuthConfig( - { projectRef: projectRef!, config: payload }, + const [subjectKey] = Object.keys(properties) + + validateSpam( { - onSuccess: () => { - toast.success('Successfully updated settings') - resetForm({ - values: values, - initialValues: values, - }) - setHasUnsavedChanges(false) // Reset the unsaved changes state + projectRef, + template: { + subject: payload[subjectKey], + content: payload[messageSlug], }, + }, + { + onSuccess: (res) => { + const spamRules = (res?.rules ?? []).filter((rule) => rule.score > 0) + const preventSaveFromSpamCheck = builtInSMTP && spamRules.length > 0 + + if (preventSaveFromSpamCheck) { + setIsSavingTemplate(false) + toast.error( + 'Please rectify all spam warnings before saving while using the built-in email service' + ) + } else { + updateAuthConfig( + { projectRef: projectRef, config: payload }, + { + onSuccess: () => { + setIsSavingTemplate(false) + toast.success('Successfully updated settings') + resetForm({ + values: values, + initialValues: values, + }) + setHasUnsavedChanges(false) // Reset the unsaved changes state + }, + } + ) + } + }, + onError: () => setIsSavingTemplate(false), } ) } @@ -164,14 +197,6 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { ) : null } - onChange={(e) => { - if (projectRef) { - debounceValidateSpam({ - projectRef, - template: { subject: e.target.value, content: bodyValue }, - }) - } - }} disabled={!canUpdateConfig} /> @@ -219,14 +244,6 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { onInputChange={(e: string | undefined) => { setBodyValue(e ?? '') if (bodyValue !== e) setHasUnsavedChanges(true) - - if (projectRef) { - const [subjectKey] = Object.keys(values) - debounceValidateSpam({ - projectRef, - template: { subject: values[subjectKey], content: e ?? '' }, - }) - } }} options={{ wordWrap: 'on', contextmenu: false }} value={bodyValue} @@ -257,9 +274,9 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => { setBodyValue((authConfig && authConfig[messageSlug]) ?? '') }} form={formId} - isSubmitting={isUpdatingConfig} + isSubmitting={isSavingTemplate} hasChanges={hasChanges} - disabled={preventSaveFromSpamCheck || !canUpdateConfig} + disabled={!canUpdateConfig} helper={ preventSaveFromSpamCheck ? 'Please rectify all spam warnings before saving while using the built-in email service' diff --git a/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx b/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx index bfc8fb9a35b45..86a651a0da516 100644 --- a/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx +++ b/apps/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx @@ -11,7 +11,6 @@ import { setProjectStatus } from 'data/projects/projects-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { PROJECT_STATUS } from 'lib/constants' import { - Alert, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, @@ -165,14 +164,16 @@ const PITRSelection = () => { - - Any changes made to your database after this point in time will be lost. This includes - any changes to your project's storage and authentication. - + + + + This action cannot be undone, not canceled once started + + + Any changes made to your database after this point in time will be lost. This includes + any changes to your project's storage and authentication. + + diff --git a/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx index 27e1999a161bb..d00fd8f68ffa8 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/AvailableIntegrations.tsx @@ -1,9 +1,10 @@ +import { Search } from 'lucide-react' +import { parseAsString, useQueryState } from 'nuqs' + import AlertError from 'components/ui/AlertError' import NoSearchResults from 'components/ui/NoSearchResults' -import { Search } from 'lucide-react' -import { useState } from 'react' import { buttonVariants, cn, Tabs_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui' -import { Admonition } from 'ui-patterns' +import { Admonition } from 'ui-patterns/admonition' import { Input } from 'ui-patterns/DataInputs/Input' import { IntegrationCard, IntegrationLoadingCard } from './IntegrationCard' import { useInstalledIntegrations } from './useInstalledIntegrations' @@ -11,8 +12,14 @@ import { useInstalledIntegrations } from './useInstalledIntegrations' type IntegrationCategory = 'all' | 'wrapper' | 'postgres_extensions' | 'custom' export const AvailableIntegrations = () => { - const [search, setSearch] = useState('') - const [selectedCategory, setSelectedCategory] = useState('all') + const [selectedCategory, setSelectedCategory] = useQueryState( + 'category', + parseAsString.withDefault('all').withOptions({ clearOnDefault: true }) + ) + const [search, setSearch] = useQueryState( + 'search', + parseAsString.withDefault('').withOptions({ clearOnDefault: true }) + ) const { availableIntegrations, installedIntegrations, error, isError, isLoading, isSuccess } = useInstalledIntegrations() diff --git a/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx b/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx index c80e7e5b632d8..de282e97098ed 100644 --- a/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx +++ b/apps/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.tsx @@ -112,7 +112,7 @@ export const generateDatabaseMenu = ( { name: 'Wrappers', key: 'wrappers', - url: `/project/${ref}/integrations/wrappers`, + url: `/project/${ref}/integrations?category=wrapper`, rightIcon: , items: [], }, @@ -121,7 +121,7 @@ export const generateDatabaseMenu = ( { name: 'Webhooks', key: 'hooks', - url: `/project/${ref}/integrations/hooks`, + url: `/project/${ref}/integrations/webhooks/overview`, rightIcon: , items: [], }, diff --git a/apps/studio/next.config.js b/apps/studio/next.config.js index 4a6a4a7e86b57..2b2f4e7a15ba7 100644 --- a/apps/studio/next.config.js +++ b/apps/studio/next.config.js @@ -402,12 +402,12 @@ const nextConfig = { { permanent: true, source: '/project/:ref/database/webhooks', - destination: '/project/:ref/integrations/webhooks', + destination: '/project/:ref/integrations/webhooks/overview', }, { permanent: true, source: '/project/:ref/database/wrappers', - destination: '/project/:ref/integrations/wrappers', + destination: '/project/:ref/integrations?category=wrapper', }, { permanent: true, diff --git a/examples/user-management/nextjs-user-management/supabase/config.toml b/examples/user-management/nextjs-user-management/supabase/config.toml index b6c41a93177f7..0784def3a53e1 100644 --- a/examples/user-management/nextjs-user-management/supabase/config.toml +++ b/examples/user-management/nextjs-user-management/supabase/config.toml @@ -7,9 +7,9 @@ project_id = "nextjs-user-management" port = 54321 # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API # endpoints. public and storage are always included. -schemas = [] +schemas = ["public", "graphql_public"] # Extra schemas to add to the search_path of every request. -extra_search_path = ["extensions"] +extra_search_path = ["public", "extensions"] # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size # for accidental or malicious requests. max_rows = 1000