diff --git a/.dockerignore b/.dockerignore index b5924c32836..ff561a782b4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,20 @@ -node_modules/ +# Root folder of this turbo monorepo +# Doesn't send useless context to Docker daemon +.git +.github +about +apps +docker +examples +i18n +**/node_modules +spec +supabase +tests + +**/.next .vercel -.next -.env.local -.env.production -.env.dev -.env.* \ No newline at end of file +.env.* + +.dockerignore +studio/Dockerfile diff --git a/.github/workflows/avoid-typos.yml b/.github/workflows/avoid-typos.yml new file mode 100644 index 00000000000..cd8d982f4aa --- /dev/null +++ b/.github/workflows/avoid-typos.yml @@ -0,0 +1,20 @@ +name: reviewdog +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + misspell: + name: runner / misspell + runs-on: ubuntu-latest + steps: + - name: Check out code. + uses: actions/checkout@v1 + - name: misspell + uses: reviewdog/action-misspell@v1 + with: + github_token: ${{ secrets.github_token }} + locale: "US" + reporter: github-pr-review + level: error + exclude: "*.css" diff --git a/apps/docs/components/CodeBlock/CodeBlock.tsx b/apps/docs/components/CodeBlock/CodeBlock.tsx index f4e962bb2aa..c29120c7bfb 100644 --- a/apps/docs/components/CodeBlock/CodeBlock.tsx +++ b/apps/docs/components/CodeBlock/CodeBlock.tsx @@ -6,6 +6,7 @@ import { Button, IconCheck, IconCopy } from 'ui' import js from 'react-syntax-highlighter/dist/cjs/languages/hljs/javascript' import ts from 'react-syntax-highlighter/dist/cjs/languages/hljs/typescript' +import csharp from 'react-syntax-highlighter/dist/cjs/languages/hljs/csharp' import py from 'react-syntax-highlighter/dist/cjs/languages/hljs/python' import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql' import bash from 'react-syntax-highlighter/dist/cjs/languages/hljs/bash' @@ -17,7 +18,7 @@ import { useTheme } from 'common/Providers' interface Props { title?: string - language: 'js' | 'jsx' | 'sql' | 'py' | 'bash' | 'ts' | 'dart' | 'json' + language: 'js' | 'jsx' | 'sql' | 'py' | 'bash' | 'ts' | 'dart' | 'json' | 'csharp' linesToHighlight?: number[] hideCopy?: boolean hideLineNumbers?: boolean @@ -62,6 +63,7 @@ const CodeBlock: FC = ({ SyntaxHighlighter.registerLanguage('sql', sql) SyntaxHighlighter.registerLanguage('bash', bash) SyntaxHighlighter.registerLanguage('dart', dart) + SyntaxHighlighter.registerLanguage('csharp', csharp) SyntaxHighlighter.registerLanguage('json', json) const large = false diff --git a/apps/docs/components/CustomHTMLElements/Heading.tsx b/apps/docs/components/CustomHTMLElements/Heading.tsx index f1248c38d1c..74f3511b110 100644 --- a/apps/docs/components/CustomHTMLElements/Heading.tsx +++ b/apps/docs/components/CustomHTMLElements/Heading.tsx @@ -14,12 +14,22 @@ import { useInView } from 'react-intersection-observer' */ interface Props { - tag: string + tag?: string + parseAnchors?: boolean + customAnchor?: string } -const Heading: React.FC = ({ tag, children }) => { +/** + * This TOC is used in .mdx files and in .tsx files. + * In mdx files, we need to parse the content and format them to match the + * expected tocList format (text, link,level). + * + * In tsx files, we can generate this tocList directly. For these files, we don't + * need to parse the and generate anchors + */ +const Heading: React.FC = ({ tag, parseAnchors = false, customAnchor, children }) => { const HeadingTag = `${tag}` as any - const anchor = getAnchor(children) + const anchor = customAnchor ? customAnchor : getAnchor(children) const link = `#${anchor}` const { ref } = useInView({ @@ -33,7 +43,7 @@ const Heading: React.FC = ({ tag, children }) => { return ( - {removeAnchor(children)} + {parseAnchors ? removeAnchor(children) : children} {anchor && ( # diff --git a/apps/docs/components/MDX/storage_management.mdx b/apps/docs/components/MDX/storage_management.mdx new file mode 100644 index 00000000000..d9df719486d --- /dev/null +++ b/apps/docs/components/MDX/storage_management.mdx @@ -0,0 +1,108 @@ +If you upload additional profile photos, they'll accumulate +in the `avatars` bucket because of their random names with only the latest being referenced +from `public.profiles` and the older versions getting orphaned. + +To automatically remove obsolete storage objects, extend the database +triggers. Note that it is not sufficient to delete the objects from the +`storage.objects` table because that would orphan and leak the actual storage objects in +the S3 backend. Instead, invoke the storage API within Postgres via the `http` extension. + +Enable the [http extension for the `extensions` schema](https://app.supabase.com/project/_/database/extensions) in the Dashboard. +Then, define the following SQL functions in the SQL Editor to delete +storage objects via the API: + +```SQL +create or replace function delete_storage_object(bucket text, object text, out status int, out content varchar) +returns record +language 'plpgsql' +security definer +as $$ +declare + project_url varchar := ''; + service_role_key varchar := ''; -- full access needed + url varchar := project_url||'/storage/v1/object/'||bucket||'/'||object; +begin + select + into status, content + result.status::int, result.content::varchar + FROM extensions.http(( + 'DELETE', + url, + ARRAY[extensions.http_header('authorization','Bearer '||service_role_key)], + NULL, + NULL)::extensions.http_request) as result; +end; +$$; + +create or replace function delete_avatar(avatar_url text, out status int, out content varchar) +returns record +language 'plpgsql' +security definer +as $$ +begin + select + into status, content + result.status, result.content + from public.delete_storage_object('avatars', avatar_url) as result; +end; +$$; + +``` + +Next, add a trigger that removes any obsolete avatar whenever the +profile is updated or deleted: + +```SQL +create or replace function delete_old_avatar() +returns trigger +language 'plpgsql' +security definer +as $$ +declare + status int; + content varchar; +begin + if coalesce(old.avatar_url, '') <> '' + and (tg_op = 'DELETE' or (old.avatar_url <> new.avatar_url)) then + select + into status, content + result.status, result.content + from public.delete_avatar(old.avatar_url) as result; + if status <> 200 then + raise warning 'Could not delete avatar: % %', status, content; + end if; + end if; + if tg_op = 'DELETE' then + return old; + end if; + return new; +end; +$$; + +create trigger before_profile_changes + before update of avatar_url or delete on public.profiles + for each row execute function public.delete_old_avatar(); + +``` + +Finally, delete the `public.profile` row before a user is deleted. +If this step is omitted, you won't be able to delete users without +first manually deleting their avatar image. + +```SQL +create or replace function delete_old_profile() +returns trigger +language 'plpgsql' +security definer +as $$ +begin + delete from public.profiles where id = old.id; + return old; +end; +$$; + +create trigger before_delete_user + before delete on auth.users + for each row execute function public.delete_old_profile(); + +``` diff --git a/apps/docs/components/Navigation/Footer.tsx b/apps/docs/components/Navigation/Footer.tsx index cbbb215e7a6..c21718dce72 100644 --- a/apps/docs/components/Navigation/Footer.tsx +++ b/apps/docs/components/Navigation/Footer.tsx @@ -1,6 +1,6 @@ import Image from 'next/image' import Link from 'next/link' -import { Button, IconGitHub, IconTwitter } from '~/../../packages/ui' +import { Button, IconGitHub, IconTwitter, IconYoutube } from '~/../../packages/ui' import footerData from '~/data/footer.json' import { useTheme } from 'common/Providers' @@ -20,6 +20,16 @@ const Footer = () => { ))}
+