├── supabase ├── seed.sql ├── .gitignore ├── migrations │ ├── 20220117141359_app_schema.sql │ ├── 20230417141004_dbdev_short_desc_typo.sql │ ├── 20230831172915_allow_anon_access_to_package_views.sql │ ├── 20220117141357_extensions.sql │ ├── 20230411104448_download_metrics_computed_relation.sql │ ├── 20220117141942_email_address_type.sql │ ├── 20230829125510_fix_view_permissions.sql │ ├── 20230413130634_popular_packages_function.sql │ ├── 20240108072747_update_provider_id.sql │ ├── 20250804111152_remove_dbdev_from_popular_packages.sql │ ├── 20230413140356_update_profile_function.sql │ ├── 20230508165641_packages_order_version.sql │ ├── 20230405083103_fix_auth_schema_values.sql │ ├── 20240705083738_remove_contact_email.sql │ ├── 20220117142138_developer_tools.sql │ ├── 20240605122023_fix_view_permissions.sql │ ├── 20220117141645_valid_name_type.sql │ ├── 20230622212339_langchain_headerkit_config_dump.sql │ ├── 20230405085810_fix_avatars_handle.sql │ ├── 20250217100252_restrict_accounts_and_orgs.sql │ ├── 20220117141507_semver.sql │ ├── 20250106073735_jwt_secret_from_vault.sql │ ├── 20231207071422_new_package_name.sql │ ├── 20220117155720_views.sql │ ├── 20220117142137_package_tables.sql │ ├── 20230405163940_download_metrics.sql │ ├── 20220117142141_security_utilities.sql │ ├── 20230906111353_publish_package.sql │ ├── 20230323180034_reserved_user_accts.sql │ ├── 20231207113857_olirice@read_once-0.3.2.sql │ ├── 20231205051816_add_default_version.sql │ └── 20230331163909_olirice-read_once.sql ├── tests │ └── database │ │ └── 02_anon_users_can_view_popular_packages.sql └── config.toml ├── website ├── README.md ├── .prettierignore ├── public │ ├── favicon.ico │ └── images │ │ ├── dbdev-darkmode.png │ │ ├── dbdev-lightmode.png │ │ └── dbdev-text.svg ├── .env.example ├── .prettierrc ├── postcss.config.js ├── .eslintrc.json ├── lib │ ├── dayjs.ts │ ├── supabase.ts │ ├── supabase-admin.ts │ ├── types.ts │ ├── validations.ts │ ├── utils.tsx │ ├── avatars.ts │ ├── zod-form-validator-utils.ts │ └── auth.tsx ├── next-env.d.ts ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── typography │ │ │ ├── li.tsx │ │ │ ├── p.tsx │ │ │ ├── code.tsx │ │ │ ├── span.tsx │ │ │ ├── strong.tsx │ │ │ ├── h1.tsx │ │ │ ├── a.tsx │ │ │ ├── h3.tsx │ │ │ └── h2.tsx │ │ ├── link.tsx │ │ ├── loader.tsx │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ ├── toaster.tsx │ │ ├── badge.tsx │ │ ├── spinner.tsx │ │ ├── avatar.tsx │ │ ├── copy-button.tsx │ │ ├── tabs.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ └── markdown.tsx │ ├── search │ │ ├── SearchInput.tsx │ │ ├── SearchPackageRow.tsx │ │ └── Search.tsx │ ├── layouts │ │ ├── Layout.tsx │ │ └── Footer.tsx │ ├── themes │ │ ├── ThemeSwitcher.tsx │ │ └── ThemeContext.tsx │ ├── forms │ │ ├── FormButton.tsx │ │ ├── Form.tsx │ │ └── FormInput.tsx │ ├── access-tokens │ │ └── AccessTokenCard.tsx │ └── packages │ │ └── PackageCard.tsx ├── components.json ├── pages │ ├── installer.tsx │ ├── _document.tsx │ ├── 404.tsx │ ├── _app.tsx │ ├── forgot-password.tsx │ ├── reset-password.tsx │ ├── sign-in.tsx │ └── sign-up.tsx ├── .gitignore ├── tsconfig.json ├── data │ ├── utils.ts │ ├── query-client.ts │ ├── auth │ │ ├── sign-out-mutation.ts │ │ ├── sign-in-mutation.ts │ │ ├── password-update-mutation.ts │ │ ├── forgot-password-mutation.ts │ │ └── sign-up-mutation.ts │ ├── static-path-queries.ts │ ├── access-tokens │ │ ├── create-access-token.ts │ │ ├── delete-access-token.ts │ │ └── access-tokens-query.ts │ ├── profiles │ │ ├── update-profile-mutation.ts │ │ └── profile-query.ts │ ├── packages │ │ ├── popular-packages-query.ts │ │ ├── packages-query.ts │ │ ├── package-query.ts │ │ └── packages-search-query.ts │ ├── organizations │ │ └── users-organizations-query.ts │ └── package-versions │ │ └── package-versions-query.ts ├── next.config.js ├── styles │ └── globals.css ├── package.json └── tailwind.config.js ├── docs ├── requirements_docs.txt ├── assets │ └── favicon.ico ├── index.md ├── install-a-package.md ├── cli.md └── publish-extension.md ├── assets ├── erd.png └── dbdev-banner.jpg ├── cli ├── src │ ├── commands │ │ ├── mod.rs │ │ ├── login.rs │ │ ├── uninstall.rs │ │ └── list.rs │ ├── secret.rs │ ├── util.rs │ └── credential_store.rs ├── Cargo.toml └── README.md ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── pgTAP.yaml │ ├── cli.yaml │ ├── pre-commit_hooks.yaml │ ├── prettier.yaml │ ├── manual-release-brew-and-scoop.yaml │ └── release-scoop-bucket.yaml ├── .pre-commit-config.yaml └── mkdocs.yaml /supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # database.dev website 2 | -------------------------------------------------------------------------------- /website/.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | pnpm-lock.yaml 3 | -------------------------------------------------------------------------------- /docs/requirements_docs.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | mkdocs-material 3 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | -------------------------------------------------------------------------------- /assets/erd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/dbdev/HEAD/assets/erd.png -------------------------------------------------------------------------------- /assets/dbdev-banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/dbdev/HEAD/assets/dbdev-banner.jpg -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/dbdev/HEAD/docs/assets/favicon.ico -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/dbdev/HEAD/website/public/favicon.ico -------------------------------------------------------------------------------- /website/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SUPABASE_URL="" 2 | NEXT_PUBLIC_SUPABASE_ANON_KEY="" 3 | SUPABASE_SERVICE_ROLE_KEY="" 4 | -------------------------------------------------------------------------------- /website/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /website/public/images/dbdev-darkmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/dbdev/HEAD/website/public/images/dbdev-darkmode.png -------------------------------------------------------------------------------- /website/public/images/dbdev-lightmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase/dbdev/HEAD/website/public/images/dbdev-lightmode.png -------------------------------------------------------------------------------- /cli/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod add; 2 | pub mod install; 3 | pub mod list; 4 | pub mod login; 5 | pub mod publish; 6 | pub mod uninstall; 7 | -------------------------------------------------------------------------------- /website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "@next/next/no-img-element": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /website/lib/dayjs.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import relativeTime from 'dayjs/plugin/relativeTime' 3 | 4 | dayjs.extend(relativeTime) 5 | 6 | export default dayjs 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | **/supabase/.branches 3 | **/supabase/.temp 4 | **/supabase/.env 5 | cli/seed.sh 6 | *.swp 7 | venv 8 | .env 9 | *.egg-info/ 10 | __pycache__/ 11 | *.pyc 12 | .DS_Store 13 | target/ 14 | -------------------------------------------------------------------------------- /supabase/migrations/20220117141359_app_schema.sql: -------------------------------------------------------------------------------- 1 | create schema app; 2 | 3 | grant usage on schema app to authenticated, anon; 4 | 5 | alter default privileges in schema app grant select on tables to authenticated, anon; 6 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /supabase/migrations/20230417141004_dbdev_short_desc_typo.sql: -------------------------------------------------------------------------------- 1 | update app.packages 2 | -- Fix typo in spelling of "packages" 3 | set control_description = 'Install packages from the database.dev registry' 4 | where handle = 'supabase' and partial_name = 'dbdev'; 5 | -------------------------------------------------------------------------------- /supabase/migrations/20230831172915_allow_anon_access_to_package_views.sql: -------------------------------------------------------------------------------- 1 | alter view public.packages set (security_invoker=false); 2 | alter view public.package_versions set (security_invoker=false); 3 | alter view public.package_upgrades set (security_invoker=false); 4 | -------------------------------------------------------------------------------- /cli/src/commands/login.rs: -------------------------------------------------------------------------------- 1 | use crate::credential_store::{get_secret_from_stdin, Credentials}; 2 | 3 | pub(crate) fn login(registry_name: &str) -> anyhow::Result<()> { 4 | let secret = get_secret_from_stdin()?; 5 | Credentials::write(registry_name, &secret)?; 6 | Ok(()) 7 | } 8 | -------------------------------------------------------------------------------- /supabase/migrations/20220117141357_extensions.sql: -------------------------------------------------------------------------------- 1 | create extension if not exists pg_stat_statements with schema extensions; 2 | create extension if not exists pg_trgm with schema extensions; 3 | create extension if not exists citext with schema extensions; 4 | create extension if not exists pg_cron; 5 | -------------------------------------------------------------------------------- /supabase/migrations/20230411104448_download_metrics_computed_relation.sql: -------------------------------------------------------------------------------- 1 | create or replace function public.download_metrics(public.packages) 2 | returns setof public.download_metrics rows 1 3 | language sql stable 4 | as $$ 5 | select * 6 | from public.download_metrics dm 7 | where dm.package_id = $1.id; 8 | $$; 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | target-branch: "master" 8 | - package-ecosystem: "cargo" 9 | directory: "/cli/" 10 | schedule: 11 | interval: "weekly" 12 | target-branch: "master" 13 | -------------------------------------------------------------------------------- /supabase/migrations/20220117141942_email_address_type.sql: -------------------------------------------------------------------------------- 1 | create domain app.email_address 2 | -- https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email) 3 | AS citext 4 | check ( value ~ '^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$' 5 | ); 6 | -------------------------------------------------------------------------------- /website/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '~/lib/utils' 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /website/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "~/components", 14 | "utils": "~/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /supabase/migrations/20230829125510_fix_view_permissions.sql: -------------------------------------------------------------------------------- 1 | alter view public.accounts set (security_invoker=true); 2 | alter view public.organizations set (security_invoker=true); 3 | alter view public.members set (security_invoker=true); 4 | alter view public.packages set (security_invoker=true); 5 | alter view public.package_versions set (security_invoker=true); 6 | alter view public.package_upgrades set (security_invoker=true); 7 | -------------------------------------------------------------------------------- /supabase/migrations/20230413130634_popular_packages_function.sql: -------------------------------------------------------------------------------- 1 | create or replace function public.popular_packages() 2 | returns setof public.packages 3 | language sql stable 4 | as $$ 5 | select * from public.packages p 6 | order by ( 7 | select (dm.downloads_30_day * 5) + (dm.downloads_90_days * 2) + dm.downloads_180_days 8 | from public.download_metrics dm 9 | where dm.package_id = p.id 10 | ) desc nulls last, p.created_at desc; 11 | $$; 12 | -------------------------------------------------------------------------------- /supabase/migrations/20240108072747_update_provider_id.sql: -------------------------------------------------------------------------------- 1 | -- For email provider the provider_id should be the lowercase email 2 | -- which is availble in the email column 3 | -- This migration was necessecitated by a recent change in identities 4 | -- table schema by gotrue: 5 | -- https://github.com/supabase/gotrue/blob/master/migrations/20231117164230_add_id_pkey_identities.up.sql 6 | update auth.identities 7 | set provider_id = email 8 | where provider = 'email'; 9 | -------------------------------------------------------------------------------- /.github/workflows/pgTAP.yaml: -------------------------------------------------------------------------------- 1 | name: pgTAP Tests 2 | on: 3 | pull_request: 4 | branches: [ master ] 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6 14 | - uses: supabase/setup-cli@v1 15 | with: 16 | version: 1.127.4 17 | - name: Supabase Start 18 | run: supabase start 19 | - name: Run Tests 20 | run: supabase test db 21 | -------------------------------------------------------------------------------- /website/components/ui/typography/li.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, forwardRef } from 'react' 2 | import { cn } from '~/lib/utils' 3 | 4 | export interface LiProps extends ComponentPropsWithoutRef<'li'> {} 5 | 6 | const Li = forwardRef( 7 | ({ className, children, ...props }) => ( 8 |
  • 9 | {children} 10 |
  • 11 | ) 12 | ) 13 | 14 | Li.displayName = 'Li' 15 | 16 | export default Li 17 | -------------------------------------------------------------------------------- /supabase/migrations/20250804111152_remove_dbdev_from_popular_packages.sql: -------------------------------------------------------------------------------- 1 | create or replace function public.popular_packages() 2 | returns setof public.packages 3 | language sql stable 4 | as $$ 5 | select * from public.packages p 6 | where p.package_name != 'supabase-dbdev' 7 | order by ( 8 | select (dm.downloads_30_day * 5) + (dm.downloads_90_days * 2) + dm.downloads_180_days 9 | from public.download_metrics dm 10 | where dm.package_id = p.id 11 | ) desc nulls last, p.created_at desc; 12 | $$; 13 | -------------------------------------------------------------------------------- /website/pages/installer.tsx: -------------------------------------------------------------------------------- 1 | import Layout from '~/components/layouts/Layout' 2 | import { NextPageWithLayout } from '~/lib/types' 3 | 4 | const InstallerPage: NextPageWithLayout = () => { 5 | return null 6 | } 7 | 8 | export async function getServerSideProps() { 9 | return { 10 | redirect: { 11 | destination: 'https://supabase.github.io/dbdev/install-in-db-client', 12 | permanent: false, 13 | }, 14 | } 15 | } 16 | 17 | InstallerPage.getLayout = (page) => {page} 18 | 19 | export default InstallerPage 20 | -------------------------------------------------------------------------------- /website/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from 'next/document' 2 | 3 | const CustomDocument = () => { 4 | return ( 5 | 6 | 7 | 11 | 12 | 13 |
    14 | 15 | 16 | 17 | ) 18 | } 19 | 20 | export default CustomDocument 21 | -------------------------------------------------------------------------------- /website/components/ui/typography/p.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, forwardRef } from 'react' 2 | import { cn } from '~/lib/utils' 3 | 4 | export interface PProps extends ComponentPropsWithoutRef<'p'> {} 5 | 6 | const P = forwardRef( 7 | ({ className, children, ...props }, ref) => ( 8 |

    13 | {children} 14 |

    15 | ) 16 | ) 17 | 18 | P.displayName = 'P' 19 | 20 | export default P 21 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.github/workflows/cli.yaml: -------------------------------------------------------------------------------- 1 | name: CLI 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | permissions: 10 | contents: read 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v6 21 | - name: Build 22 | run: cargo build --release --verbose 23 | working-directory: ./cli 24 | - name: Run tests 25 | run: cargo test --release --verbose 26 | working-directory: ./cli 27 | -------------------------------------------------------------------------------- /website/components/ui/typography/code.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, forwardRef } from 'react' 2 | import { cn } from '~/lib/utils' 3 | 4 | export interface CodeProps extends ComponentPropsWithoutRef<'code'> {} 5 | 6 | const Code = forwardRef( 7 | ({ className, children, ...props }, ref) => ( 8 | 13 | {children} 14 | 15 | ) 16 | ) 17 | 18 | Code.displayName = 'Code' 19 | 20 | export default Code 21 | -------------------------------------------------------------------------------- /website/components/ui/typography/span.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, forwardRef } from 'react' 2 | import { cn } from '~/lib/utils' 3 | 4 | export interface SpanProps extends ComponentPropsWithoutRef<'span'> {} 5 | 6 | const Span = forwardRef( 7 | ({ className, children, ...props }, ref) => ( 8 | 13 | {children} 14 | 15 | ) 16 | ) 17 | 18 | Span.displayName = 'Span' 19 | 20 | export default Span 21 | -------------------------------------------------------------------------------- /website/lib/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | import { Database } from '~/data/database.types' 3 | 4 | if (!process.env.NEXT_PUBLIC_SUPABASE_URL) { 5 | throw new Error('Missing NEXT_PUBLIC_SUPABASE_URL environment variable') 6 | } 7 | 8 | if (!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) { 9 | throw new Error('Missing NEXT_PUBLIC_SUPABASE_ANON_KEY environment variable') 10 | } 11 | 12 | const supabase = createClient( 13 | process.env.NEXT_PUBLIC_SUPABASE_URL, 14 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY 15 | ) 16 | 17 | export default supabase 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.0.1 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-merge-conflict 9 | - id: check-added-large-files 10 | args: ['--maxkb=500'] 11 | - id: mixed-line-ending 12 | args: ['--fix=lf'] 13 | 14 | - repo: https://github.com/Lucas-C/pre-commit-hooks 15 | rev: v1.1.10 16 | hooks: 17 | - id: remove-tabs 18 | name: Tabs-to-Spaces 19 | include: ^supabase/migrations 20 | -------------------------------------------------------------------------------- /website/lib/supabase-admin.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | import { Database } from '~/data/database.types' 3 | 4 | if (!process.env.NEXT_PUBLIC_SUPABASE_URL) { 5 | throw new Error('Missing NEXT_PUBLIC_SUPABASE_URL environment variable') 6 | } 7 | 8 | if (!process.env.SUPABASE_SERVICE_ROLE_KEY) { 9 | throw new Error('Missing SUPABASE_SERVICE_ROLE_KEY environment variable') 10 | } 11 | 12 | const supabaseAdmin = createClient( 13 | process.env.NEXT_PUBLIC_SUPABASE_URL, 14 | process.env.SUPABASE_SERVICE_ROLE_KEY 15 | ) 16 | 17 | export default supabaseAdmin 18 | -------------------------------------------------------------------------------- /website/components/ui/typography/strong.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, forwardRef } from 'react' 2 | import { cn } from '~/lib/utils' 3 | 4 | export interface StrongProps extends ComponentPropsWithoutRef<'strong'> {} 5 | 6 | const Strong = forwardRef( 7 | ({ className, children, ...props }, ref) => ( 8 | 13 | {children} 14 | 15 | ) 16 | ) 17 | 18 | Strong.displayName = 'Strong' 19 | 20 | export default Strong 21 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit_hooks.yaml: -------------------------------------------------------------------------------- 1 | name: pre-commit hooks 2 | 3 | on: [push] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v6 15 | 16 | - name: set up python 3.10 17 | uses: actions/setup-python@v6 18 | with: 19 | python-version: "3.10" 20 | 21 | - name: install pre-commit 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install pre-commit 25 | 26 | - name: run static analysis 27 | run: | 28 | pre-commit run --all-files 29 | -------------------------------------------------------------------------------- /website/components/ui/typography/h1.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, forwardRef } from 'react' 2 | import { cn } from '~/lib/utils' 3 | 4 | export interface H1Props extends ComponentPropsWithoutRef<'h1'> {} 5 | 6 | const H1 = forwardRef( 7 | ({ className, children, ...props }, ref) => ( 8 |

    16 | {children} 17 |

    18 | ) 19 | ) 20 | 21 | H1.displayName = 'H1' 22 | 23 | export default H1 24 | -------------------------------------------------------------------------------- /website/components/ui/typography/a.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, forwardRef } from 'react' 2 | import { cn } from '~/lib/utils' 3 | 4 | export interface AProps extends ComponentPropsWithoutRef<'a'> {} 5 | 6 | const A = forwardRef( 7 | ({ className, children, target = '_blank', ...props }) => ( 8 | 16 | {children} 17 | 18 | ) 19 | ) 20 | 21 | A.displayName = 'A' 22 | 23 | export default A 24 | -------------------------------------------------------------------------------- /supabase/migrations/20230413140356_update_profile_function.sql: -------------------------------------------------------------------------------- 1 | create or replace function public.update_profile( 2 | handle app.valid_name, 3 | display_name text default null, 4 | bio text default null 5 | ) 6 | returns void 7 | language plpgsql 8 | as $$ 9 | declare 10 | v_is_org boolean; 11 | begin 12 | update app.accounts a 13 | set display_name = coalesce($2, a.display_name), 14 | bio = coalesce($3, a.bio) 15 | where a.handle = $1; 16 | 17 | update app.organizations o 18 | set display_name = coalesce($2, o.display_name), 19 | bio = coalesce($3, o.bio) 20 | where o.handle = $1; 21 | end; 22 | $$; 23 | -------------------------------------------------------------------------------- /supabase/tests/database/02_anon_users_can_view_popular_packages.sql: -------------------------------------------------------------------------------- 1 | begin; 2 | 3 | select plan(1); 4 | 5 | select tests.clear_authentication(); 6 | 7 | select results_eq( 8 | $$ select package_name from public.popular_packages() order by package_name $$, 9 | $$ values 10 | ('burggraf-pg_headerkit'), 11 | ('langchain-embedding_search'), 12 | ('langchain-hybrid_search'), 13 | ('michelp-adminpack'), 14 | ('olirice-asciiplot'), 15 | ('olirice-index_advisor'), 16 | ('olirice-read_once') 17 | $$, 18 | 'Anon can view popular packages' 19 | ); 20 | 21 | select * from finish(); 22 | 23 | rollback; 24 | -------------------------------------------------------------------------------- /website/components/ui/typography/h3.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, forwardRef } from 'react' 2 | import { cn } from '~/lib/utils' 3 | 4 | export interface H3Props extends ComponentPropsWithoutRef<'h1'> {} 5 | 6 | const H3 = forwardRef( 7 | ({ className, children, ...props }, ref) => ( 8 |

    16 | {children} 17 |

    18 | ) 19 | ) 20 | 21 | H3.displayName = 'H3' 22 | 23 | export default H3 24 | -------------------------------------------------------------------------------- /.github/workflows/prettier.yaml: -------------------------------------------------------------------------------- 1 | name: Check Website Code Formatting with Prettier 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'master' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | check-prettier: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out repo 16 | uses: actions/checkout@v6 17 | with: 18 | ref: ${{ github.head_ref }} 19 | - name: Run Prettier 20 | uses: creyD/prettier_action@v4.6 21 | with: 22 | # Prettier CLI arguments 23 | prettier_options: '--config ./website/.prettierrc --ignore-path ./website/.prettierignore --check ./website' 24 | -------------------------------------------------------------------------------- /cli/src/secret.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | #[serde(transparent)] 7 | pub struct Secret { 8 | inner: T, 9 | } 10 | 11 | impl From for Secret { 12 | fn from(inner: T) -> Self { 13 | Secret { inner } 14 | } 15 | } 16 | 17 | impl Secret { 18 | pub fn expose(&self) -> &T { 19 | &self.inner 20 | } 21 | } 22 | 23 | impl fmt::Debug for Secret { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | f.debug_struct("Secret") 26 | .field("inner", &"REDACTED") 27 | .finish() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /supabase/migrations/20230508165641_packages_order_version.sql: -------------------------------------------------------------------------------- 1 | create or replace view public.packages as 2 | select 3 | pa.id, 4 | pa.package_name, 5 | pa.handle, 6 | pa.partial_name, 7 | newest_ver.version as latest_version, 8 | newest_ver.description_md, 9 | pa.control_description, 10 | pa.control_requires, 11 | pa.created_at 12 | from 13 | app.packages pa, 14 | lateral ( 15 | select * 16 | from app.package_versions pv 17 | where pv.package_id = pa.id 18 | order by pv.version_struct desc 19 | limit 1 20 | ) newest_ver; 21 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "~/*": ["./*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /supabase/migrations/20230405083103_fix_auth_schema_values.sql: -------------------------------------------------------------------------------- 1 | update auth.users 2 | set 3 | created_at = now(), 4 | updated_at = now(), 5 | email_confirmed_at = now(), 6 | confirmation_token = '', 7 | recovery_token = '', 8 | email_change_token_new = '', 9 | email_change = ''; 10 | 11 | insert into 12 | auth.identities ( 13 | id, 14 | provider_id, 15 | provider, 16 | user_id, 17 | identity_data, 18 | created_at, 19 | updated_at 20 | ) 21 | select 22 | gen_random_uuid(), 23 | email, 24 | 'email' as provider, 25 | id as user_id, 26 | jsonb_build_object('sub', id, 'email', email) as identity_data, 27 | created_at, 28 | updated_at 29 | from 30 | auth.users; 31 | -------------------------------------------------------------------------------- /website/components/search/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react' 2 | import { Input } from '~/components/ui/input' 3 | 4 | export type SearchInputProps = { 5 | value: string 6 | onChange: (value: string) => void 7 | } 8 | 9 | const SearchInput = forwardRef( 10 | function SearchInput({ value, onChange }, ref) { 11 | return ( 12 | <> 13 | onChange(e.target.value)} 21 | /> 22 | 23 | ) 24 | } 25 | ) 26 | 27 | export default SearchInput 28 | -------------------------------------------------------------------------------- /website/components/ui/link.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from 'next/link' 2 | import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react' 3 | import { cn } from '~/lib/utils' 4 | 5 | export interface LinkProps extends ComponentPropsWithoutRef {} 6 | 7 | const Link = forwardRef, LinkProps>( 8 | ({ className, children, ...props }, ref) => { 9 | return ( 10 | 18 | {children} 19 | 20 | ) 21 | } 22 | ) 23 | 24 | Link.displayName = 'Link' 25 | 26 | export default Link 27 | -------------------------------------------------------------------------------- /.github/workflows/manual-release-brew-and-scoop.yaml: -------------------------------------------------------------------------------- 1 | name: Release CLI on Homebrew and Scoop 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag: 7 | description: "Tag to release (e.g. v1.2.3)" 8 | required: true 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | call-release-homebrew-tap: 15 | uses: ./.github/workflows/release-homebrew-tap.yaml 16 | with: 17 | tag: ${{ github.event.inputs.tag }} 18 | secrets: 19 | homebrew_tap_rw: ${{ secrets.HOMEBREW_TAP_RW }} 20 | 21 | call-release-scoop-bucket: 22 | uses: ./.github/workflows/release-scoop-bucket.yaml 23 | with: 24 | tag: ${{ github.event.inputs.tag }} 25 | secrets: 26 | scoop_bucket_rw: ${{ secrets.SCOOP_BUCKET_RW }} 27 | -------------------------------------------------------------------------------- /supabase/migrations/20240705083738_remove_contact_email.sql: -------------------------------------------------------------------------------- 1 | drop view if exists public.accounts; 2 | 3 | create view 4 | public.accounts 5 | with 6 | (security_invoker = true) as 7 | select 8 | acc.id, 9 | acc.handle, 10 | obj.name as avatar_path, 11 | acc.display_name, 12 | acc.bio, 13 | acc.created_at 14 | from 15 | app.accounts acc 16 | left join storage.objects obj on acc.avatar_id = obj.id; 17 | 18 | drop view if exists public.organizations; 19 | 20 | create view 21 | public.organizations 22 | with 23 | (security_invoker = true) as 24 | select 25 | org.id, 26 | org.handle, 27 | obj.name as avatar_path, 28 | org.display_name, 29 | org.bio, 30 | org.created_at 31 | from 32 | app.organizations org 33 | left join storage.objects obj on org.avatar_id = obj.id; 34 | -------------------------------------------------------------------------------- /website/components/layouts/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react' 2 | import { cn } from '~/lib/utils' 3 | import Navbar from './Navbar' 4 | import Footer from './Footer' 5 | 6 | export type LayoutProps = { 7 | gradientBg?: boolean 8 | containerWidth?: 'md' | 'full' 9 | } 10 | 11 | const Layout = ({ 12 | containerWidth = 'md', 13 | children, 14 | }: PropsWithChildren) => { 15 | return ( 16 |
    17 | 18 | 19 |
    25 | {children} 26 |
    27 | 28 |
    29 |
    30 | ) 31 | } 32 | 33 | export default Layout 34 | -------------------------------------------------------------------------------- /website/data/utils.ts: -------------------------------------------------------------------------------- 1 | export class NotFoundError extends Error { 2 | constructor(message?: string) { 3 | super(message) 4 | this.name = 'NotFoundError' 5 | } 6 | } 7 | 8 | export class NotImplementedError extends Error { 9 | constructor(message?: string) { 10 | super(message) 11 | this.name = 'NotImplementedError' 12 | } 13 | } 14 | 15 | export class UnknownError extends Error { 16 | constructor(message?: string) { 17 | super(message) 18 | this.name = 'UnknownError' 19 | } 20 | } 21 | 22 | export const DEFAULT_PAGE_SIZE = 20 23 | 24 | export function getPagination(page?: number, size: number = DEFAULT_PAGE_SIZE) { 25 | const limit = size 26 | const from = page ? page * limit : 0 27 | const to = page ? from + size - 1 : size - 1 28 | 29 | return { from, to } 30 | } 31 | -------------------------------------------------------------------------------- /website/components/ui/loader.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from './skeleton' 2 | 3 | interface LoaderRowProps { 4 | className?: string 5 | delayIndex?: number 6 | animationDelay?: number 7 | } 8 | 9 | const LoaderRow = ({ 10 | className = '', 11 | delayIndex = 0, 12 | animationDelay = 150, 13 | }: LoaderRowProps) => { 14 | return ( 15 | 21 | ) 22 | } 23 | 24 | const ShimmeringLoader = () => { 25 | return ( 26 |
    27 | 28 | 29 | 30 |
    31 | ) 32 | } 33 | 34 | export default ShimmeringLoader 35 | -------------------------------------------------------------------------------- /supabase/migrations/20220117142138_developer_tools.sql: -------------------------------------------------------------------------------- 1 | create or replace function app.simulate_login(email citext) 2 | returns void 3 | language sql 4 | as $$ 5 | /* 6 | Simulated JWT of logged in user 7 | */ 8 | 9 | select 10 | set_config( 11 | 'request.jwt.claims', 12 | ( 13 | select 14 | json_build_object( 15 | 'sub', 16 | id, 17 | 'role', 18 | 'authenticated' 19 | )::text 20 | from 21 | auth.users 22 | where 23 | email = $1 24 | ), 25 | true 26 | ), 27 | set_config('role', 'authenticated', true) 28 | $$; 29 | -------------------------------------------------------------------------------- /website/components/themes/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { MoonIcon, SunIcon } from '@heroicons/react/24/outline' 2 | import { useThemeContext } from './ThemeContext' 3 | 4 | const ThemeSwitcher = () => { 5 | const { theme, setTheme } = useThemeContext() 6 | 7 | const handleThemeChange = () => { 8 | const nextTheme = theme === 'dark' ? 'light' : 'dark' 9 | setTheme(nextTheme) 10 | } 11 | 12 | return ( 13 | 20 | ) 21 | } 22 | 23 | export default ThemeSwitcher 24 | -------------------------------------------------------------------------------- /website/components/search/SearchPackageRow.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import dayjs from '~/lib/dayjs' 3 | 4 | export type SearchPackageRowProps = { 5 | handle: string 6 | partialName: string 7 | createdAt: string 8 | } 9 | 10 | const SearchPackageRow = ({ 11 | handle, 12 | partialName, 13 | createdAt, 14 | }: SearchPackageRowProps) => { 15 | const name = `${handle}/${partialName}` 16 | 17 | return ( 18 | 22 | {name} 23 | 24 | {dayjs(createdAt).format('YYYY-MM-DD HH:mm:ss')} 25 | 26 | 27 | ) 28 | } 29 | 30 | export default SearchPackageRow 31 | -------------------------------------------------------------------------------- /website/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as LabelPrimitive from '@radix-ui/react-label' 3 | import { cva, type VariantProps } from 'class-variance-authority' 4 | 5 | import { cn } from '~/lib/utils' 6 | 7 | const labelVariants = cva( 8 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /supabase/migrations/20240605122023_fix_view_permissions.sql: -------------------------------------------------------------------------------- 1 | -- set view security_invoker=true to fix linter errors 2 | alter view public.packages set (security_invoker=true); 3 | alter view public.package_versions set (security_invoker=true); 4 | alter view public.package_upgrades set (security_invoker=true); 5 | 6 | -- create policies to allow anon role to read from the views 7 | create policy packages_select_policy_anon 8 | on app.packages 9 | as permissive 10 | for select 11 | to anon 12 | using (true); 13 | 14 | create policy package_versions_select_policy_anon 15 | on app.package_versions 16 | as permissive 17 | for select 18 | to anon 19 | using (true); 20 | 21 | create policy package_upgrades_select_policy_anon 22 | on app.package_upgrades 23 | as permissive 24 | for select 25 | to anon 26 | using (true); 27 | -------------------------------------------------------------------------------- /supabase/migrations/20220117141645_valid_name_type.sql: -------------------------------------------------------------------------------- 1 | create extension if not exists citext with schema extensions; 2 | 3 | create domain app.valid_name 4 | as extensions.citext 5 | check ( 6 | -- 3 to 15 chars, A-z with underscores 7 | value ~ '^[A-z][A-z0-9\_]{2,32}$' 8 | ); 9 | 10 | create or replace function app.exception(message text) 11 | returns text 12 | language plpgsql 13 | as $$ 14 | begin 15 | raise exception using errcode='22000', message=message; 16 | end; 17 | $$; 18 | 19 | /* 20 | create domain app.valid_name 21 | as extensions.citext 22 | check ( 23 | -- 3 to 15 chars, A-z with underscores 24 | case 25 | when value ~ '^[A-z][A-z0-9\_]{2,14}$' then True 26 | else app.exception('Bad name ' || value)::bool 27 | end 28 | ); 29 | */ 30 | -------------------------------------------------------------------------------- /website/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '~/lib/utils' 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = 'Input' 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /website/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as SeparatorPrimitive from '@radix-ui/react-separator' 3 | 4 | import { cn } from '~/lib/utils' 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = 'horizontal', decorative = true, ...props }, 12 | ref 13 | ) => ( 14 | 25 | ) 26 | ) 27 | Separator.displayName = SeparatorPrimitive.Root.displayName 28 | 29 | export { Separator } 30 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const cspHeader = ` 4 | default-src 'self' 'unsafe-eval' ${process.env.NEXT_PUBLIC_SUPABASE_URL}; 5 | style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com/ https://fonts.google.com/; 6 | img-src 'self' data: ${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/; 7 | object-src 'none'; 8 | base-uri 'none'; 9 | frame-ancestors 'none'; 10 | ` 11 | 12 | const nextConfig = { 13 | reactStrictMode: true, 14 | async headers() { 15 | return [ 16 | { 17 | source: '/(.*)', 18 | headers: [ 19 | { 20 | key: 'Content-Security-Policy', 21 | value: cspHeader.replace(/\n/g, ''), 22 | }, 23 | { 24 | key: 'X-Frame-Options', 25 | value: 'SAMEORIGIN', 26 | }, 27 | ], 28 | }, 29 | ] 30 | }, 31 | } 32 | 33 | module.exports = nextConfig 34 | -------------------------------------------------------------------------------- /website/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | import { useToast } from '~/hooks/use-toast' 2 | import { 3 | Toast, 4 | ToastClose, 5 | ToastDescription, 6 | ToastProvider, 7 | ToastTitle, 8 | ToastViewport, 9 | } from '~/components/ui/toast' 10 | 11 | export function Toaster() { 12 | const { toasts } = useToast() 13 | 14 | return ( 15 | 16 | {toasts.map(function ({ id, title, description, action, ...props }) { 17 | return ( 18 | 19 |
    20 | {title && {title}} 21 | {description && ( 22 | {description} 23 | )} 24 |
    25 | {action} 26 | 27 |
    28 | ) 29 | })} 30 | 31 |
    32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /website/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentType, ReactElement, ReactNode } from 'react' 2 | import type { NextPage } from 'next' 3 | import type { AppProps } from 'next/app' 4 | 5 | /* Next Types */ 6 | 7 | export type NextPageWithLayout

    = NextPage & { 8 | getLayout?: (page: ReactElement) => ReactNode 9 | } 10 | 11 | export function isNextPageWithLayout( 12 | Component: ComponentType | NextPageWithLayout 13 | ): Component is NextPageWithLayout { 14 | return 'getLayout' in Component && typeof Component.getLayout === 'function' 15 | } 16 | 17 | export type AppPropsWithLayout = AppProps & { 18 | Component: NextPageWithLayout 19 | } 20 | 21 | /* Utility Types */ 22 | 23 | export type NonNullableObject = { 24 | [K in keyof T]: T[K] extends Array 25 | ? Array> 26 | : T[K] extends object 27 | ? NonNullableObject 28 | : NonNullable 29 | } 30 | -------------------------------------------------------------------------------- /website/components/forms/FormButton.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react' 2 | import { useFormState } from 'react-final-form' 3 | import { cn } from '~/lib/utils' 4 | import { Button, ButtonProps } from '~/components/ui/button' 5 | 6 | export interface FormButtonProps extends ButtonProps {} 7 | 8 | const FormButton = forwardRef( 9 | ({ children, className, ...props }, ref) => { 10 | const { submitting } = useFormState() 11 | 12 | return ( 13 | 24 | ) 25 | } 26 | ) 27 | 28 | FormButton.displayName = 'FormButton' 29 | 30 | export default FormButton 31 | -------------------------------------------------------------------------------- /cli/src/commands/uninstall.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use sqlx::postgres::{PgConnection, PgRow}; 3 | use sqlx::Row; 4 | 5 | pub async fn uninstall(extension_name: &str, mut conn: PgConnection) -> anyhow::Result<()> { 6 | let quoted_extension_name: String = sqlx::query("select quote_ident($1) as ident") 7 | .bind(extension_name) 8 | .map(|row: PgRow| row.get("ident")) 9 | .fetch_one(&mut conn) 10 | .await 11 | .context("Failed to get quoted identifier")?; 12 | 13 | sqlx::query(&format!("drop extension if exists {quoted_extension_name}")) 14 | .execute(&mut conn) 15 | .await 16 | .context(format!("failed to drop extension {extension_name}"))?; 17 | 18 | sqlx::query("select 1 from pgtle.uninstall_extension($1)") 19 | .bind(extension_name) 20 | .execute(&mut conn) 21 | .await 22 | .context(format!("failed to uninstall extension {extension_name}"))?; 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /website/data/query-client.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query' 2 | import { useState } from 'react' 3 | import { NotFoundError, NotImplementedError } from './utils' 4 | 5 | export function createQueryClient() { 6 | return new QueryClient({ 7 | defaultOptions: { 8 | queries: { 9 | retry: (failureCount, error) => { 10 | // Don't retry on 404s or if we haven't finished writing the code 11 | if ( 12 | error instanceof NotFoundError || 13 | error instanceof NotImplementedError 14 | ) { 15 | return false 16 | } 17 | 18 | if (failureCount < 3) { 19 | return true 20 | } 21 | 22 | return false 23 | }, 24 | }, 25 | }, 26 | }) 27 | } 28 | 29 | /** 30 | * useRootQueryClient creates a new query client 31 | */ 32 | export function useRootQueryClient() { 33 | const [queryClient] = useState(createQueryClient) 34 | 35 | return queryClient 36 | } 37 | -------------------------------------------------------------------------------- /website/components/ui/typography/h2.tsx: -------------------------------------------------------------------------------- 1 | import { cva, VariantProps } from 'class-variance-authority' 2 | import { ComponentPropsWithoutRef, forwardRef } from 'react' 3 | import { cn } from '~/lib/utils' 4 | 5 | const h2Variants = cva( 6 | 'mt-10 scroll-m-20 text-3xl font-semibold tracking-tight transition-colors first:mt-0 text-foreground', 7 | { 8 | variants: { 9 | variant: { 10 | normal: 'pb-2 border-b border-border', 11 | borderless: '', 12 | }, 13 | }, 14 | defaultVariants: { 15 | variant: 'normal', 16 | }, 17 | } 18 | ) 19 | 20 | export interface H2Props 21 | extends ComponentPropsWithoutRef<'h2'>, VariantProps {} 22 | 23 | const H2 = forwardRef( 24 | ({ className, variant, children, ...props }, ref) => ( 25 |

    26 | {children} 27 |

    28 | ) 29 | ) 30 | 31 | H2.displayName = 'H2' 32 | 33 | export default H2 34 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dbdev" 3 | version = "0.1.7" 4 | edition = "2021" 5 | authors = ["supabase"] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | anyhow = "1.0.100" 11 | clap = { version = "4.5.53", features = ["derive"] } 12 | dirs = "6.0.0" 13 | futures = "0.3" 14 | postgrest = "1.5.0" 15 | rand = "0.9.2" 16 | regex = "1.12" 17 | reqwest = { version = "0.12.9", features = ["json", "native-tls-vendored"] } 18 | rpassword = "7.4.0" 19 | serde = { version = "1.0.228", features = ["derive"] } 20 | serde_json = "1.0.145" 21 | sqlx = { version = "0.8.6", features = [ 22 | "postgres", 23 | "chrono", 24 | "uuid", 25 | "runtime-tokio-rustls", 26 | ] } 27 | tempfile = "3.23.0" 28 | thiserror = "2.0.17" 29 | tokio = { version = "1", features = ["full"] } 30 | toml = { version = "0.9.8", features = ["preserve_order"] } 31 | url = { version = "2.5.7", features = ["serde"] } 32 | uuid = { version = "1.16.0", features = ["serde"] } 33 | -------------------------------------------------------------------------------- /website/components/layouts/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export const rightLinks = [ 4 | { title: 'Open Source', url: 'https://supabase.com/open-source' }, 5 | { title: 'Privacy', url: 'https://supabase.com/privacy' }, 6 | { title: 'GitHub', url: 'https://github.com/supabase/dbdev/' }, 7 | ] 8 | 9 | const Footer = () => ( 10 |
    11 |
    12 |
    13 |
    14 | © Supabase, Inc. 15 |
    16 |
      17 | {rightLinks.map((link, index) => ( 18 |
    • 19 | {link.title} 20 |
    • 21 | ))} 22 |
    23 |
    24 |
    25 |
    26 | ) 27 | 28 | export default Footer 29 | -------------------------------------------------------------------------------- /website/data/auth/sign-out-mutation.ts: -------------------------------------------------------------------------------- 1 | import { AuthError } from '@supabase/supabase-js' 2 | import { 3 | useMutation, 4 | UseMutationOptions, 5 | useQueryClient, 6 | } from '@tanstack/react-query' 7 | import supabase from '~/lib/supabase' 8 | 9 | export async function signOut() { 10 | const { error } = await supabase.auth.signOut() 11 | 12 | if (error) { 13 | throw error 14 | } 15 | } 16 | 17 | type SignOutData = Awaited> 18 | type SignOutError = AuthError 19 | 20 | export const useSignOutMutation = ({ 21 | onSuccess, 22 | ...options 23 | }: Omit< 24 | UseMutationOptions, 25 | 'mutationFn' 26 | > = {}) => { 27 | const queryClient = useQueryClient() 28 | 29 | return useMutation(() => signOut(), { 30 | async onSuccess(data, variables, context) { 31 | await Promise.all([ 32 | queryClient.resetQueries(), 33 | await onSuccess?.(data, variables, context), 34 | ]) 35 | }, 36 | ...options, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /website/data/static-path-queries.ts: -------------------------------------------------------------------------------- 1 | import supabaseAdmin from '~/lib/supabase-admin' 2 | 3 | // [Alaister]: These functions are to be called server side only 4 | // as they bypass RLS. They will not work client side. 5 | 6 | export async function getAllProfiles() { 7 | const [{ data: organizations }, { data: accounts }] = await Promise.all([ 8 | supabaseAdmin 9 | .from('organizations') 10 | .select('handle') 11 | .order('created_at', { ascending: false }) 12 | .limit(500) 13 | .returns<{ handle: string }[]>(), 14 | supabaseAdmin 15 | .from('accounts') 16 | .select('handle') 17 | .order('created_at', { ascending: false }) 18 | .limit(500) 19 | .returns<{ handle: string }[]>(), 20 | ]) 21 | 22 | return [...(organizations ?? []), ...(accounts ?? [])] 23 | } 24 | 25 | export async function getAllPackages() { 26 | const { data } = await supabaseAdmin 27 | 28 | .from('packages') 29 | .select('handle,partial_name') 30 | .order('created_at', { ascending: false }) 31 | .limit(1000) 32 | .returns<{ handle: string; partial_name: string }[]>() 33 | 34 | return data ?? [] 35 | } 36 | -------------------------------------------------------------------------------- /mkdocs.yaml: -------------------------------------------------------------------------------- 1 | site_name: dbdev 2 | site_url: https://supabase.github.io/dbdev 3 | site_description: A package manager for PostgreSQL trusted language extensions 4 | 5 | repo_name: supabase/dbdev 6 | repo_url: https://github.com/supabase/dbdev 7 | 8 | nav: 9 | - Welcome: 'index.md' 10 | - Using the CLI: 'cli.md' 11 | - Publish a Package: 'publish-extension.md' 12 | - Install a Package: 'install-a-package.md' 13 | - Structure of a Postgres Extension: 'extension_structure.md' 14 | 15 | theme: 16 | name: 'material' 17 | favicon: 'assets/favicon.ico' 18 | logo: 'assets/favicon.ico' 19 | homepage: https://supabase.github.io/dbdev 20 | features: 21 | - navigation.expand 22 | - content.code.copy 23 | palette: 24 | primary: black 25 | accent: light green 26 | 27 | markdown_extensions: 28 | - pymdownx.highlight: 29 | linenums: true 30 | guess_lang: false 31 | use_pygments: true 32 | pygments_style: default 33 | - pymdownx.superfences 34 | - pymdownx.tabbed: 35 | alternate_style: true 36 | - pymdownx.snippets 37 | - pymdownx.tasklist 38 | - admonition 39 | -------------------------------------------------------------------------------- /website/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { cva, type VariantProps } from 'class-variance-authority' 3 | 4 | import { cn } from '~/lib/utils' 5 | 6 | const badgeVariants = cva( 7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', 13 | secondary: 14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 15 | destructive: 16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 17 | outline: 'text-foreground', 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: 'default', 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends 28 | React.HTMLAttributes, 29 | VariantProps {} 30 | 31 | function Badge({ className, variant, ...props }: BadgeProps) { 32 | return ( 33 |
    34 | ) 35 | } 36 | 37 | export { Badge, badgeVariants } 38 | -------------------------------------------------------------------------------- /website/components/ui/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority' 2 | import { cn } from '~/lib/utils' 3 | 4 | const spinnerVariants = cva('animate-spin', { 5 | variants: { 6 | size: { 7 | sm: 'w-4 h-4', 8 | md: 'w-5 h-5', 9 | lg: 'w-6 h-6', 10 | }, 11 | }, 12 | defaultVariants: { 13 | size: 'md', 14 | }, 15 | }) 16 | 17 | interface SpinnerProps extends VariantProps { 18 | className?: string 19 | } 20 | 21 | const Spinner = ({ size, className }: SpinnerProps) => { 22 | return ( 23 | 33 | 41 | 46 | 47 | ) 48 | } 49 | 50 | export default Spinner 51 | -------------------------------------------------------------------------------- /supabase/migrations/20230622212339_langchain_headerkit_config_dump.sql: -------------------------------------------------------------------------------- 1 | 2 | -- update dependencies for langchain 3 | 4 | UPDATE app.packages SET control_requires = '{vector}' WHERE handle = 'langchain'; 5 | 6 | INSERT INTO app.package_upgrades(package_id, from_version_struct, to_version_struct, sql) 7 | VALUES ( 8 | (SELECT id FROM app.packages WHERE handle = 'langchain' AND partial_name = 'embedding_search'), 9 | (1,1,0), 10 | (1,1,1), 11 | $langchain$ 12 | SELECT pg_extension_config_dump('documents', ''); 13 | SELECT pg_extension_config_dump('documents_id_seq', ''); 14 | $langchain$ 15 | ); 16 | 17 | INSERT INTO app.package_upgrades(package_id, from_version_struct, to_version_struct, sql) 18 | VALUES ( 19 | (SELECT id FROM app.packages WHERE handle = 'langchain' AND partial_name = 'hybrid_search'), 20 | (1,1,0), 21 | (1,1,1), 22 | $langchain$ 23 | SELECT pg_extension_config_dump('documents', ''); 24 | SELECT pg_extension_config_dump('documents_id_seq', ''); 25 | $langchain$ 26 | ); 27 | 28 | INSERT INTO app.package_upgrades(package_id, from_version_struct, to_version_struct, sql) 29 | VALUES ( 30 | (SELECT id FROM app.packages WHERE partial_name = 'pg_headerkit'), 31 | (1,0,0), 32 | (1,0,1), 33 | $hdr$ 34 | SELECT pg_extension_config_dump('hdr.allow_list', ''); 35 | SELECT pg_extension_config_dump('hdr.deny_list', ''); 36 | $hdr$ 37 | ); 38 | -------------------------------------------------------------------------------- /website/lib/validations.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const email = z 4 | .string() 5 | .email() 6 | .transform((str) => str.toLowerCase().trim()) 7 | 8 | export const password = z.string().min(8).max(255) 9 | 10 | export const handle = z.string().min(3).max(15) 11 | 12 | export const displayName = z.string().max(255) 13 | 14 | export const SignUpSchema = z.object({ 15 | displayName: displayName.nullable(), 16 | handle, 17 | email, 18 | password, 19 | }) 20 | 21 | export const UpdateProfileSchema = z.object({ 22 | displayName, 23 | handle, 24 | bio: z.string().max(255), 25 | }) 26 | 27 | export const SignInSchema = z.object({ 28 | email, 29 | password: z.string().min(1), 30 | }) 31 | 32 | export const ForgotPasswordSchema = z.object({ 33 | email, 34 | }) 35 | 36 | export const ResetPasswordSchema = z 37 | .object({ 38 | password: password, 39 | passwordConfirmation: password, 40 | }) 41 | .refine((data) => data.password === data.passwordConfirmation, { 42 | message: "Passwords don't match", 43 | path: ['passwordConfirmation'], // set the path of the error 44 | }) 45 | 46 | export const NewOrgSchema = z.object({ 47 | handle, 48 | displayName: displayName.nullable(), 49 | }) 50 | 51 | export const NewTokenSchema = z.object({ 52 | tokenName: z.string().max(64), 53 | }) 54 | -------------------------------------------------------------------------------- /supabase/migrations/20230405085810_fix_avatars_handle.sql: -------------------------------------------------------------------------------- 1 | create or replace function app.update_avatar_id() 2 | returns trigger 3 | language plpgsql 4 | security definer 5 | as $$ 6 | declare 7 | v_handle app.valid_name; 8 | v_affected_account app.accounts := null; 9 | begin 10 | select (string_to_array(new.name, '/'::text))[1]::app.valid_name into v_handle; 11 | 12 | update app.accounts 13 | set avatar_id = new.id 14 | where handle = v_handle 15 | returning * into v_affected_account; 16 | 17 | if not v_affected_account is null then 18 | update auth.users u 19 | set 20 | "raw_user_meta_data" = u.raw_user_meta_data || jsonb_build_object( 21 | 'avatar_path', new.name 22 | ) 23 | where u.id = v_affected_account.id; 24 | else 25 | update app.organizations 26 | set avatar_id = new.id 27 | where handle = v_handle; 28 | end if; 29 | 30 | return new; 31 | end; 32 | $$; 33 | 34 | alter policy storage_objects_insert_policy 35 | on storage.objects 36 | with check ( 37 | app.is_handle_maintainer( 38 | auth.uid(), 39 | (string_to_array(name, '/'::text))[1]::app.valid_name 40 | ) 41 | ); 42 | -------------------------------------------------------------------------------- /website/data/auth/sign-in-mutation.ts: -------------------------------------------------------------------------------- 1 | import { AuthError } from '@supabase/supabase-js' 2 | import { 3 | useMutation, 4 | UseMutationOptions, 5 | useQueryClient, 6 | } from '@tanstack/react-query' 7 | import supabase from '~/lib/supabase' 8 | 9 | type SignInVariables = { email: string; password: string } 10 | 11 | export async function signIn({ email, password }: SignInVariables) { 12 | const { 13 | error, 14 | data: { session }, 15 | } = await supabase.auth.signInWithPassword({ 16 | email, 17 | password, 18 | }) 19 | 20 | if (error) { 21 | throw error 22 | } 23 | 24 | return { session } 25 | } 26 | 27 | type SignInData = Awaited> 28 | type SignInError = AuthError 29 | 30 | export const useSignInMutation = ({ 31 | onSuccess, 32 | ...options 33 | }: Omit< 34 | UseMutationOptions, 35 | 'mutationFn' 36 | > = {}) => { 37 | const queryClient = useQueryClient() 38 | 39 | return useMutation( 40 | ({ email, password }) => signIn({ email, password }), 41 | { 42 | async onSuccess(data, variables, context) { 43 | await Promise.all([ 44 | queryClient.resetQueries(), 45 | await onSuccess?.(data, variables, context), 46 | ]) 47 | }, 48 | ...options, 49 | } 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /website/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import Link from 'next/link' 3 | import Layout from '~/components/layouts/Layout' 4 | import { Button } from '~/components/ui/button' 5 | import H1 from '~/components/ui/typography/h1' 6 | import P from '~/components/ui/typography/p' 7 | import { NextPageWithLayout } from '~/lib/types' 8 | 9 | export type FourOhFourPageProps = { 10 | title?: string 11 | } 12 | 13 | const FourOhFourPage: NextPageWithLayout = ({ 14 | title = 'Page not found', 15 | }) => { 16 | return ( 17 | <> 18 | 19 | {`404 ${title} | The Database Package Manager`} 20 | 21 | 22 |
    23 |
    24 |

    404

    25 |

    {title}

    26 |

    27 | Sorry, we couldn't find the page you're looking for. 28 |

    29 | 30 | 33 |
    34 |
    35 | 36 | ) 37 | } 38 | 39 | FourOhFourPage.getLayout = (page) => {page} 40 | 41 | export default FourOhFourPage 42 | -------------------------------------------------------------------------------- /website/data/auth/password-update-mutation.ts: -------------------------------------------------------------------------------- 1 | import { AuthError } from '@supabase/supabase-js' 2 | import { 3 | useMutation, 4 | UseMutationOptions, 5 | useQueryClient, 6 | } from '@tanstack/react-query' 7 | import supabase from '~/lib/supabase' 8 | 9 | type UpdatePasswordVariables = { newPassword: string } 10 | 11 | export async function updatePassword({ newPassword }: UpdatePasswordVariables) { 12 | const { error } = await supabase.auth.updateUser({ 13 | password: newPassword, 14 | }) 15 | 16 | if (error) { 17 | throw error 18 | } 19 | } 20 | 21 | type UpdatePasswordData = Awaited> 22 | type UpdatePasswordError = AuthError 23 | 24 | export const useUpdatePasswordMutation = ({ 25 | onSuccess, 26 | ...options 27 | }: Omit< 28 | UseMutationOptions< 29 | UpdatePasswordData, 30 | UpdatePasswordError, 31 | UpdatePasswordVariables 32 | >, 33 | 'mutationFn' 34 | > = {}) => { 35 | const queryClient = useQueryClient() 36 | 37 | return useMutation< 38 | UpdatePasswordData, 39 | UpdatePasswordError, 40 | UpdatePasswordVariables 41 | >(({ newPassword }) => updatePassword({ newPassword }), { 42 | async onSuccess(data, variables, context) { 43 | await Promise.all([ 44 | queryClient.resetQueries(), 45 | await onSuccess?.(data, variables, context), 46 | ]) 47 | }, 48 | ...options, 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /website/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google' 2 | import { Hydrate, QueryClientProvider } from '@tanstack/react-query' 3 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools' 4 | import { Toaster } from 'react-hot-toast' 5 | import { ThemeContextProvider } from '~/components/themes/ThemeContext' 6 | import { useRootQueryClient } from '~/data/query-client' 7 | import { AuthProvider } from '~/lib/auth' 8 | import { AppPropsWithLayout } from '~/lib/types' 9 | import { cn } from '~/lib/utils' 10 | import '~/styles/globals.css' 11 | 12 | const inter = Inter({ subsets: ['latin'] }) 13 | 14 | const CustomApp = ({ Component, pageProps }: AppPropsWithLayout) => { 15 | const queryClient = useRootQueryClient() 16 | 17 | const getLayout = Component.getLayout ?? ((page) => page) 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 |
    25 | {getLayout()} 26 |
    27 |
    28 |
    29 | 30 | 31 | 32 |
    33 |
    34 | ) 35 | } 36 | 37 | export default CustomApp 38 | -------------------------------------------------------------------------------- /website/data/auth/forgot-password-mutation.ts: -------------------------------------------------------------------------------- 1 | import { AuthError } from '@supabase/supabase-js' 2 | import { 3 | useMutation, 4 | UseMutationOptions, 5 | useQueryClient, 6 | } from '@tanstack/react-query' 7 | import supabase from '~/lib/supabase' 8 | 9 | type ForgotPasswordVariables = { email: string; redirectTo?: string } 10 | 11 | export async function forgotPassword({ 12 | email, 13 | redirectTo, 14 | }: ForgotPasswordVariables) { 15 | const { error } = await supabase.auth.resetPasswordForEmail(email, { 16 | redirectTo, 17 | }) 18 | 19 | if (error) { 20 | throw error 21 | } 22 | } 23 | 24 | type ForgotPasswordData = Awaited> 25 | type ForgotPasswordError = AuthError 26 | 27 | export const useForgotPasswordMutation = ({ 28 | onSuccess, 29 | ...options 30 | }: Omit< 31 | UseMutationOptions< 32 | ForgotPasswordData, 33 | ForgotPasswordError, 34 | ForgotPasswordVariables 35 | >, 36 | 'mutationFn' 37 | > = {}) => { 38 | const queryClient = useQueryClient() 39 | 40 | return useMutation< 41 | ForgotPasswordData, 42 | ForgotPasswordError, 43 | ForgotPasswordVariables 44 | >(({ email, redirectTo }) => forgotPassword({ email, redirectTo }), { 45 | async onSuccess(data, variables, context) { 46 | await Promise.all([ 47 | queryClient.resetQueries(), 48 | await onSuccess?.(data, variables, context), 49 | ]) 50 | }, 51 | ...options, 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /website/lib/utils.tsx: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from 'clsx' 2 | import { useRouter } from 'next/router' 3 | import { useEffect, useMemo, useState } from 'react' 4 | import { twMerge } from 'tailwind-merge' 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)) 8 | } 9 | 10 | type Params = { 11 | [k: string]: string | undefined 12 | } 13 | 14 | export function useParams(): Params { 15 | const { query } = useRouter() 16 | 17 | return useMemo( 18 | () => 19 | Object.fromEntries( 20 | Object.entries(query).map(([key, value]) => { 21 | if (Array.isArray(value)) { 22 | return [key, value[0]] 23 | } else { 24 | return [key, value] 25 | } 26 | }) 27 | ), 28 | [query] 29 | ) 30 | } 31 | 32 | export function firstStr(str: string | string[]) { 33 | if (Array.isArray(str)) { 34 | return str[0] 35 | } else { 36 | return str 37 | } 38 | } 39 | 40 | export function useDebounce(value: T, delay: number = 500): T { 41 | const [debouncedValue, setDebouncedValue] = useState(value) 42 | 43 | useEffect(() => { 44 | const timer = setTimeout(() => setDebouncedValue(value), delay) 45 | 46 | return () => { 47 | clearTimeout(timer) 48 | } 49 | }, [value, delay]) 50 | 51 | return debouncedValue 52 | } 53 | 54 | export function pluralize(count: number, singular: string, plural?: string) { 55 | return count === 1 ? singular : plural || singular + 's' 56 | } 57 | -------------------------------------------------------------------------------- /website/components/access-tokens/AccessTokenCard.tsx: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import { toast } from 'react-hot-toast' 3 | import { useDeleteAccessTokenMutation } from '~/data/access-tokens/delete-access-token' 4 | import { Button } from '~/components/ui/button' 5 | 6 | export interface ApiTokenCardProps { 7 | tokenId: string 8 | tokenName: string 9 | maskedToken: string 10 | createdAt: string 11 | } 12 | 13 | const AccessTokenCard = ({ 14 | tokenId, 15 | tokenName, 16 | maskedToken, 17 | createdAt, 18 | }: ApiTokenCardProps) => { 19 | const { mutate: deleteAccessToken, isLoading: isDeletingAccessToken } = 20 | useDeleteAccessTokenMutation({ 21 | onSuccess() { 22 | toast.success('Successfully revoked token!') 23 | }, 24 | }) 25 | 26 | return ( 27 |
    28 |
    29 |
    {tokenName}
    30 |
    {`Token: ${maskedToken}`}
    31 |
    {`Created ${dayjs( 32 | createdAt 33 | ).fromNow()}`}
    34 |
    35 | 42 |
    43 | ) 44 | } 45 | 46 | export default AccessTokenCard 47 | -------------------------------------------------------------------------------- /website/data/access-tokens/create-access-token.ts: -------------------------------------------------------------------------------- 1 | import { PostgrestError } from '@supabase/supabase-js' 2 | import { 3 | useMutation, 4 | UseMutationOptions, 5 | useQueryClient, 6 | } from '@tanstack/react-query' 7 | import supabase from '~/lib/supabase' 8 | import { accessTokensQueryKey } from './access-tokens-query' 9 | 10 | type NewAccessTokenVariables = { 11 | tokenName: string 12 | } 13 | 14 | export async function newAccessToken({ tokenName }: NewAccessTokenVariables) { 15 | const { data, error } = await supabase.rpc('new_access_token', { 16 | token_name: tokenName, 17 | }) 18 | 19 | if (error) throw error 20 | return data 21 | } 22 | 23 | type NewAccessTokenData = Awaited> 24 | type NewAccessTokenError = PostgrestError 25 | 26 | export const useNewAccessTokenMutation = ({ 27 | onSuccess, 28 | ...options 29 | }: Omit< 30 | UseMutationOptions< 31 | NewAccessTokenData, 32 | NewAccessTokenError, 33 | NewAccessTokenVariables 34 | >, 35 | 'mutationFn' 36 | > = {}) => { 37 | const queryClient = useQueryClient() 38 | 39 | return useMutation< 40 | NewAccessTokenData, 41 | NewAccessTokenError, 42 | NewAccessTokenVariables 43 | >(({ tokenName }) => newAccessToken({ tokenName }), { 44 | async onSuccess(data, variables, context) { 45 | await Promise.all([queryClient.invalidateQueries([accessTokensQueryKey])]) 46 | 47 | await onSuccess?.(data, variables, context) 48 | }, 49 | ...options, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /website/data/access-tokens/delete-access-token.ts: -------------------------------------------------------------------------------- 1 | import { PostgrestError } from '@supabase/supabase-js' 2 | import { 3 | useMutation, 4 | UseMutationOptions, 5 | useQueryClient, 6 | } from '@tanstack/react-query' 7 | import supabase from '~/lib/supabase' 8 | import { accessTokensQueryKey } from './access-tokens-query' 9 | 10 | type DeleteAccessTokenVariables = { 11 | tokenId: string 12 | } 13 | 14 | export async function deleteAccessToken({ 15 | tokenId, 16 | }: DeleteAccessTokenVariables) { 17 | const { data, error } = await supabase.rpc('delete_access_token', { 18 | token_id: tokenId, 19 | }) 20 | 21 | if (error) throw error 22 | return data 23 | } 24 | 25 | type DeleteAccessTokenData = Awaited> 26 | type DeleteAccessTokenError = PostgrestError 27 | 28 | export const useDeleteAccessTokenMutation = ({ 29 | onSuccess, 30 | ...options 31 | }: Omit< 32 | UseMutationOptions< 33 | DeleteAccessTokenData, 34 | DeleteAccessTokenError, 35 | DeleteAccessTokenVariables 36 | >, 37 | 'mutationFn' 38 | > = {}) => { 39 | const queryClient = useQueryClient() 40 | 41 | return useMutation< 42 | DeleteAccessTokenData, 43 | DeleteAccessTokenError, 44 | DeleteAccessTokenVariables 45 | >(({ tokenId }) => deleteAccessToken({ tokenId }), { 46 | async onSuccess(data, variables, context) { 47 | await Promise.all([queryClient.invalidateQueries([accessTokensQueryKey])]) 48 | 49 | await onSuccess?.(data, variables, context) 50 | }, 51 | ...options, 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /website/data/profiles/update-profile-mutation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useMutation, 3 | UseMutationOptions, 4 | useQueryClient, 5 | } from '@tanstack/react-query' 6 | import supabase from '~/lib/supabase' 7 | 8 | type UpdateProfileVariables = { 9 | handle: string 10 | displayName?: string | null 11 | bio?: string | null 12 | } 13 | 14 | export async function updateProfile({ 15 | handle, 16 | displayName, 17 | bio, 18 | }: UpdateProfileVariables) { 19 | const { data, error } = await supabase.rpc('update_profile', { 20 | handle, 21 | display_name: displayName ?? undefined, 22 | bio: bio ?? undefined, 23 | }) 24 | 25 | if (error) throw error 26 | return data 27 | } 28 | 29 | type UpdateProfileData = Awaited> 30 | type UpdateProfileError = any 31 | 32 | export const useUpdateProfileMutation = ({ 33 | onSuccess, 34 | ...options 35 | }: Omit< 36 | UseMutationOptions< 37 | UpdateProfileData, 38 | UpdateProfileError, 39 | UpdateProfileVariables 40 | >, 41 | 'mutationFn' 42 | > = {}) => { 43 | const queryClient = useQueryClient() 44 | 45 | return useMutation< 46 | UpdateProfileData, 47 | UpdateProfileError, 48 | UpdateProfileVariables 49 | >( 50 | ({ handle, displayName, bio }) => 51 | updateProfile({ handle, displayName, bio }), 52 | { 53 | async onSuccess(data, variables, context) { 54 | await Promise.all([ 55 | queryClient.resetQueries(), 56 | await onSuccess?.(data, variables, context), 57 | ]) 58 | }, 59 | ...options, 60 | } 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /website/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as AvatarPrimitive from '@radix-ui/react-avatar' 3 | 4 | import { cn } from '~/lib/utils' 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | )) 19 | Avatar.displayName = AvatarPrimitive.Root.displayName 20 | 21 | const AvatarImage = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 30 | )) 31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 32 | 33 | const AvatarFallback = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | )) 46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 47 | 48 | export { Avatar, AvatarImage, AvatarFallback } 49 | -------------------------------------------------------------------------------- /website/components/forms/Form.tsx: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, PropsWithoutRef } from 'react' 2 | import { 3 | Form as FinalForm, 4 | FormProps as FinalFormProps, 5 | } from 'react-final-form' 6 | import { z } from 'zod' 7 | import { cn } from '~/lib/utils' 8 | import { validateZodSchema } from '~/lib/zod-form-validator-utils' 9 | export { FORM_ERROR } from 'final-form' 10 | 11 | export interface FormProps> extends Omit< 12 | PropsWithoutRef, 13 | 'onSubmit' 14 | > { 15 | /** All your form fields */ 16 | schema?: S 17 | onSubmit: FinalFormProps>['onSubmit'] 18 | initialValues?: FinalFormProps>['initialValues'] 19 | } 20 | 21 | function Form>({ 22 | children, 23 | schema, 24 | initialValues, 25 | onSubmit, 26 | className, 27 | ...props 28 | }: PropsWithChildren>) { 29 | return ( 30 | ( 35 |
    40 | {submitError && ( 41 |
    42 | {submitError} 43 |
    44 | )} 45 | 46 | {/* Form fields supplied as children are rendered here */} 47 | {children} 48 |
    49 | )} 50 | /> 51 | ) 52 | } 53 | 54 | export default Form 55 | -------------------------------------------------------------------------------- /supabase/migrations/20250217100252_restrict_accounts_and_orgs.sql: -------------------------------------------------------------------------------- 1 | -- Only allow authenticated users to view their own accounts. 2 | alter policy accounts_select_policy 3 | on app.accounts 4 | to authenticated 5 | using (id = auth.uid()); 6 | 7 | -- Only allow organization maintainers to view their own organizations. 8 | alter policy organizations_select_policy 9 | on app.organizations 10 | to authenticated 11 | using (app.is_organization_maintainer(auth.uid(), id)); 12 | 13 | -- Allow authenticated users to get an account by handle. 14 | create or replace function public.get_account( 15 | handle text 16 | ) 17 | returns setof public.accounts 18 | language sql 19 | security definer 20 | strict 21 | as $$ 22 | select id, handle, avatar_path, display_name, bio, created_at 23 | from public.accounts a 24 | where a.handle = get_account.handle 25 | and auth.uid() is not null; 26 | $$; 27 | 28 | -- Allow authenticated users to get an organization by handle. 29 | create or replace function public.get_organization( 30 | handle text 31 | ) 32 | returns setof public.organizations 33 | language sql 34 | security definer 35 | strict 36 | as $$ 37 | select id, handle, avatar_path, display_name, bio, created_at 38 | from public.organizations o 39 | where o.handle = get_organization.handle 40 | and auth.uid() is not null; 41 | $$; 42 | 43 | -- Allow service role to read all accounts and organizations. 44 | grant select on app.accounts to service_role; 45 | grant select on app.organizations to service_role; 46 | 47 | -- Allow service role to read all packages. 48 | grant select on app.packages to service_role; 49 | grant select on app.package_versions to service_role; 50 | -------------------------------------------------------------------------------- /website/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 5.9% 10%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --card: 240 10% 3.9%; 33 | --card-foreground: 0 0% 98%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --primary: 0 0% 98%; 37 | --primary-foreground: 240 5.9% 10%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 240 3.7% 15.9%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 0% 98%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 240 4.9% 83.9%; 49 | } 50 | } 51 | 52 | @layer base { 53 | * { 54 | @apply border-border; 55 | } 56 | body { 57 | @apply bg-background text-foreground; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /website/data/auth/sign-up-mutation.ts: -------------------------------------------------------------------------------- 1 | import { AuthError } from '@supabase/supabase-js' 2 | import { 3 | useMutation, 4 | UseMutationOptions, 5 | useQueryClient, 6 | } from '@tanstack/react-query' 7 | import supabase from '~/lib/supabase' 8 | 9 | type SignUpVariables = { 10 | email: string 11 | password: string 12 | handle: string 13 | displayName?: string | null 14 | bio?: string | null 15 | } 16 | 17 | export async function signUp({ 18 | email, 19 | password, 20 | handle, 21 | displayName, 22 | bio, 23 | }: SignUpVariables) { 24 | const { 25 | error, 26 | data: { session }, 27 | } = await supabase.auth.signUp({ 28 | email, 29 | password, 30 | options: { 31 | data: { 32 | handle, 33 | display_name: displayName, 34 | bio, 35 | contact_email: email, 36 | }, 37 | }, 38 | }) 39 | 40 | if (error) { 41 | throw error 42 | } 43 | 44 | return { session } 45 | } 46 | 47 | type SignUpData = Awaited> 48 | type SignUpError = AuthError 49 | 50 | export const useSignUpMutation = ({ 51 | onSuccess, 52 | ...options 53 | }: Omit< 54 | UseMutationOptions, 55 | 'mutationFn' 56 | > = {}) => { 57 | const queryClient = useQueryClient() 58 | 59 | return useMutation( 60 | ({ email, password, handle, displayName, bio }) => 61 | signUp({ email, password, handle, displayName, bio }), 62 | { 63 | async onSuccess(data, variables, context) { 64 | await Promise.all([ 65 | queryClient.resetQueries(), 66 | await onSuccess?.(data, variables, context), 67 | ]) 68 | }, 69 | ...options, 70 | } 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /supabase/migrations/20220117141507_semver.sql: -------------------------------------------------------------------------------- 1 | -- https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions 2 | create type app.semver_struct as ( 3 | major smallint, 4 | minor smallint, 5 | patch smallint 6 | ); 7 | 8 | create or replace function app.is_valid(app.semver_struct) 9 | returns boolean 10 | immutable 11 | language sql 12 | as $$ 13 | select ( 14 | ($1).major is not null 15 | and ($1).minor is not null 16 | and ($1).patch is not null 17 | ) 18 | $$; 19 | 20 | create domain app.semver 21 | as app.semver_struct 22 | check ( 23 | app.is_valid(value) 24 | ); 25 | 26 | create function app.semver_exception(version text) 27 | returns app.semver_struct 28 | immutable 29 | language plpgsql 30 | as $$ 31 | begin 32 | raise exception using errcode='22000', message=format('Invalid semver %L', version); 33 | end; 34 | $$; 35 | 36 | 37 | -- Cast from Text 38 | create function app.text_to_semver(text) 39 | returns app.semver_struct 40 | immutable 41 | strict 42 | language sql 43 | as $$ 44 | with s(version) as ( 45 | select ( 46 | split_part($1, '.', 1), 47 | split_part($1, '.', 2), 48 | split_part(split_part(split_part($1, '.', 3), '-', 1), '+', 1) 49 | )::app.semver_struct 50 | ) 51 | select 52 | case app.is_valid(s.version) 53 | when true then s.version 54 | else app.semver_exception($1) 55 | end 56 | from 57 | s 58 | $$; 59 | 60 | 61 | create or replace function app.semver_to_text(app.semver) 62 | returns text 63 | immutable 64 | language sql 65 | as $$ 66 | select 67 | format('%s.%s.%s', $1.major, $1.minor, $1.patch) 68 | $$; 69 | -------------------------------------------------------------------------------- /website/components/ui/copy-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { CheckIcon, ClipboardDocumentIcon } from '@heroicons/react/24/outline' 4 | import { cva, VariantProps } from 'class-variance-authority' 5 | import { HTMLAttributes, useEffect, useState } from 'react' 6 | import { cn } from '~/lib/utils' 7 | import { Button } from './button' 8 | 9 | interface CopyButtonProps extends HTMLAttributes { 10 | getValue: () => string 11 | variant?: 'light' | 'dark' 12 | } 13 | 14 | async function copyToClipboardWithMeta(value: string) { 15 | navigator.clipboard.writeText(value) 16 | } 17 | 18 | const CopyButton = ({ 19 | getValue, 20 | className, 21 | variant = 'light', 22 | ...props 23 | }: CopyButtonProps) => { 24 | const [hasCopied, setHasCopied] = useState(false) 25 | 26 | useEffect(() => { 27 | let mounted = true 28 | 29 | const id = setTimeout(() => { 30 | if (mounted) { 31 | setHasCopied(false) 32 | } 33 | }, 2000) 34 | 35 | return () => { 36 | mounted = false 37 | clearTimeout(id) 38 | } 39 | }, [hasCopied]) 40 | 41 | return ( 42 | 63 | ) 64 | } 65 | 66 | export default CopyButton 67 | -------------------------------------------------------------------------------- /website/components/packages/PackageCard.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid' 2 | import Link from 'next/link' 3 | import { cn } from '~/lib/utils' 4 | 5 | export interface PackageCardProps { 6 | pkg: { 7 | handle: string 8 | package_name: string 9 | package_alias: string 10 | partial_name: string 11 | latest_version: string 12 | control_description: string 13 | } 14 | className?: string 15 | } 16 | 17 | const PackageCard = ({ pkg, className }: PackageCardProps) => { 18 | return ( 19 | 28 |
    29 |
    30 |

    31 | {pkg.partial_name}{' '} 32 |

    33 | 34 |

    35 | v{pkg.latest_version} 36 |

    37 |
    38 |
    43 |

    {pkg.handle}

    44 |

    45 | {pkg.control_description} 46 |

    47 | 48 | ) 49 | } 50 | 51 | export default PackageCard 52 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dbdev-website", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "prettier": "prettier --write ." 11 | }, 12 | "dependencies": { 13 | "@heroicons/react": "^2.2.0", 14 | "@radix-ui/react-avatar": "^1.1.10", 15 | "@radix-ui/react-dialog": "^1.1.14", 16 | "@radix-ui/react-dropdown-menu": "^2.1.15", 17 | "@radix-ui/react-label": "^2.1.7", 18 | "@radix-ui/react-separator": "^1.1.7", 19 | "@radix-ui/react-slot": "^1.2.3", 20 | "@radix-ui/react-tabs": "^1.1.12", 21 | "@radix-ui/react-toast": "^1.2.14", 22 | "@supabase/supabase-js": "^2.53.0", 23 | "@tanstack/react-query": "^4.40.1", 24 | "@tanstack/react-query-devtools": "^4.40.1", 25 | "@types/node": "^20.17.14", 26 | "@types/react": "^18.3.19", 27 | "@types/react-dom": "^18.3.3", 28 | "class-variance-authority": "^0.7.1", 29 | "clsx": "^2.1.1", 30 | "dayjs": "^1.11.13", 31 | "eslint": "^9.39.1", 32 | "eslint-config-next": "^15.4.5", 33 | "final-form": "^4.20.10", 34 | "lucide-react": "^0.536.0", 35 | "next": "^15.4.10", 36 | "react": "^18.3.1", 37 | "react-dom": "^18.3.1", 38 | "react-final-form": "^6.5.9", 39 | "react-hot-toast": "^2.5.2", 40 | "react-markdown": "^9.0.1", 41 | "rehype-highlight": "^7.0.2", 42 | "remark-gfm": "^4.0.1", 43 | "tailwind-merge": "^2.5.5", 44 | "tailwindcss-animate": "^1.0.7", 45 | "typescript": "^5.9.2", 46 | "zod": "^3.25.76" 47 | }, 48 | "devDependencies": { 49 | "@tailwindcss/forms": "^0.5.9", 50 | "@tailwindcss/typography": "^0.5.15", 51 | "autoprefixer": "^10.4.20", 52 | "postcss": "^8.4.49", 53 | "prettier": "^3.7.4", 54 | "tailwindcss": "^3.4.17" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /website/data/access-tokens/access-tokens-query.ts: -------------------------------------------------------------------------------- 1 | import { PostgrestError } from '@supabase/supabase-js' 2 | import { 3 | QueryClient, 4 | useQuery, 5 | useQueryClient, 6 | UseQueryOptions, 7 | } from '@tanstack/react-query' 8 | import { useCallback } from 'react' 9 | import supabase from '~/lib/supabase' 10 | 11 | export type AccessToken = { 12 | id: string 13 | token_name: string 14 | masked_token: string 15 | created_at: string 16 | } 17 | 18 | const SELECTED_COLUMNS = [ 19 | 'id', 20 | 'token_name', 21 | 'masked_token', 22 | 'created_at', 23 | ] as const 24 | 25 | export type AccessTokensResponse = Pick< 26 | AccessToken, 27 | (typeof SELECTED_COLUMNS)[number] 28 | >[] 29 | 30 | export async function getAccessTokens() { 31 | const { data, error } = await supabase.rpc('get_access_tokens', {}) 32 | 33 | if (error) { 34 | throw error 35 | } 36 | 37 | return (data as AccessTokensResponse) ?? [] 38 | } 39 | 40 | export const accessTokensQueryKey = 'access-tokens' 41 | 42 | export type AccessTokensData = Awaited> 43 | export type AccessTokensError = PostgrestError 44 | 45 | export const useAccessTokensQuery = ({ 46 | enabled = true, 47 | ...options 48 | }: UseQueryOptions = {}) => 49 | useQuery( 50 | [accessTokensQueryKey], 51 | ({}) => getAccessTokens(), 52 | { 53 | enabled: enabled, 54 | ...options, 55 | } 56 | ) 57 | 58 | export const prefetchAccessTokens = (client: QueryClient) => { 59 | return client.prefetchQuery([accessTokensQueryKey], ({}) => getAccessTokens()) 60 | } 61 | 62 | export const useAccessTokensPrefetch = () => { 63 | const client = useQueryClient() 64 | 65 | return useCallback(() => { 66 | prefetchAccessTokens(client) 67 | }, [client]) 68 | } 69 | -------------------------------------------------------------------------------- /website/lib/avatars.ts: -------------------------------------------------------------------------------- 1 | import supabase from './supabase' 2 | 3 | export const DEFAULT_AVATAR_SRC_URL = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2IiBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgNTkyIDU5MiI+PGNpcmNsZSBjeD0iMjk2IiBjeT0iMjk2IiByPSIyNzYiIGZpbGw9IiNmZmYiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSI0MCIvPjxtYXNrIGlkPSJhIiBmaWxsPSIjZmZmIj48cGF0aCBkPSJNNTcyIDI5NmMwIDE1Mi40MzEtMTIzLjU2OSAyNzYtMjc2IDI3Ni0xNTIuNDMxIDAtMjc2LTEyMy41NjktMjc2LTI3NkMyMCAxNDMuNTY5IDE0My41NjkgMjAgMjk2IDIwYzE1Mi40MzEgMCAyNzYgMTIzLjU2OSAyNzYgMjc2WiIvPjwvbWFzaz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNTcyIDI5NmMwIDE1Mi40MzEtMTIzLjU2OSAyNzYtMjc2IDI3Ni0xNTIuNDMxIDAtMjc2LTEyMy41NjktMjc2LTI3NkMyMCAxNDMuNTY5IDE0My41NjkgMjAgMjk2IDIwYzE1Mi40MzEgMCAyNzYgMTIzLjU2OSAyNzYgMjc2WiIvPjxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik05MyA0NzQuNUM5MyAzNjIuNjYyIDE4My42NjIgMjcyIDI5NS41IDI3MnYtNDBDMTYxLjU3MSAyMzIgNTMgMzQwLjU3MSA1MyA0NzQuNWg0MFpNMjk1LjUgMjcyQzQwNy4zMzggMjcyIDQ5OCAzNjIuNjYyIDQ5OCA0NzQuNWg0MEM1MzggMzQwLjU3MSA0MjkuNDI5IDIzMiAyOTUuNSAyMzJ2NDBaTTUzMiAyOTZjMCAxMzAuMzM5LTEwNS42NjEgMjM2LTIzNiAyMzZ2ODBjMTc0LjUyMiAwIDMxNi0xNDEuNDc4IDMxNi0zMTZoLTgwWk0yOTYgNTMyQzE2NS42NjEgNTMyIDYwIDQyNi4zMzkgNjAgMjk2aC04MGMwIDE3NC41MjIgMTQxLjQ3OCAzMTYgMzE2IDMxNnYtODBaTTYwIDI5NkM2MCAxNjUuNjYxIDE2NS42NjEgNjAgMjk2IDYwdi04MEMxMjEuNDc4LTIwLTIwIDEyMS40NzgtMjAgMjk2aDgwWk0yOTYgNjBjMTMwLjMzOSAwIDIzNiAxMDUuNjYxIDIzNiAyMzZoODBjMC0xNzQuNTIyLTE0MS40NzgtMzE2LTMxNi0zMTZ2ODBaIiBtYXNrPSJ1cmwoI2EpIi8+PGNpcmNsZSBjeD0iMjk2IiBjeT0iMjA0IiByPSI5NiIgZmlsbD0iI0RBREFEQSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjQwIi8+PC9zdmc+` 4 | 5 | export function getAvatarUrl(path: string | null) { 6 | if (path === null) { 7 | return DEFAULT_AVATAR_SRC_URL 8 | } 9 | 10 | return supabase.storage.from('avatars').getPublicUrl(path, { 11 | transform: { 12 | width: 256, 13 | height: 256, 14 | resize: 'cover', 15 | }, 16 | }).data.publicUrl 17 | } 18 | -------------------------------------------------------------------------------- /website/public/images/dbdev-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /website/data/packages/popular-packages-query.ts: -------------------------------------------------------------------------------- 1 | import { PostgrestError } from '@supabase/supabase-js' 2 | import { 3 | QueryClient, 4 | useQuery, 5 | useQueryClient, 6 | UseQueryOptions, 7 | } from '@tanstack/react-query' 8 | import { useCallback } from 'react' 9 | import supabase from '~/lib/supabase' 10 | import { Package } from './package-query' 11 | 12 | const SELECTED_COLUMNS = [ 13 | 'id', 14 | 'package_name', 15 | 'package_alias', 16 | 'handle', 17 | 'partial_name', 18 | 'latest_version', 19 | 'control_description', 20 | ] as const 21 | 22 | export type PopularPackagesResponse = Pick< 23 | Package, 24 | (typeof SELECTED_COLUMNS)[number] 25 | >[] 26 | 27 | export async function getPopularPackages(signal?: AbortSignal) { 28 | let query = supabase 29 | .rpc('popular_packages') 30 | .select(SELECTED_COLUMNS.join(',')) 31 | .limit(9) 32 | 33 | if (signal) { 34 | query = query.abortSignal(signal) 35 | } 36 | 37 | const { data, error } = await query.returns() 38 | 39 | if (error) { 40 | throw error 41 | } 42 | 43 | return data ?? [] 44 | } 45 | 46 | export type PopularPackagesData = Awaited> 47 | export type PopularPackagesError = PostgrestError 48 | 49 | export const usePopularPackagesQuery = ({ 50 | enabled = true, 51 | ...options 52 | }: UseQueryOptions = {}) => 53 | useQuery( 54 | ['popular-packages'], 55 | ({ signal }) => getPopularPackages(signal), 56 | { 57 | enabled, 58 | ...options, 59 | } 60 | ) 61 | 62 | export const prefetchPopularPackages = (client: QueryClient) => { 63 | return client.prefetchQuery(['popular-packages'], ({ signal }) => 64 | getPopularPackages(signal) 65 | ) 66 | } 67 | 68 | export const usePopularPackagesPrefetch = () => { 69 | const client = useQueryClient() 70 | 71 | return useCallback(() => { 72 | prefetchPopularPackages(client) 73 | }, [client]) 74 | } 75 | -------------------------------------------------------------------------------- /website/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as TabsPrimitive from '@radix-ui/react-tabs' 3 | 4 | import { cn } from '~/lib/utils' 5 | 6 | const Tabs = TabsPrimitive.Root 7 | 8 | const TabsList = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | TabsList.displayName = TabsPrimitive.List.displayName 22 | 23 | const TabsTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 35 | )) 36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName 37 | 38 | const TabsContent = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 | 50 | )) 51 | TabsContent.displayName = TabsPrimitive.Content.displayName 52 | 53 | export { Tabs, TabsList, TabsTrigger, TabsContent } 54 | -------------------------------------------------------------------------------- /website/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Slot } from '@radix-ui/react-slot' 3 | import { cva, type VariantProps } from 'class-variance-authority' 4 | 5 | import { cn } from '~/lib/utils' 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 13 | destructive: 14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 15 | outline: 16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 17 | secondary: 18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 19 | ghost: 'hover:bg-accent hover:text-accent-foreground', 20 | link: 'text-primary underline-offset-4 hover:underline', 21 | }, 22 | size: { 23 | default: 'h-10 px-4 py-2', 24 | sm: 'h-9 rounded-md px-3', 25 | lg: 'h-11 rounded-md px-8', 26 | icon: 'h-10 w-10', 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: 'default', 31 | size: 'default', 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends 38 | React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : 'button' 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = 'Button' 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # `dbdev` 2 | 3 |

    4 | PostgreSQL version 5 | License 6 | pgTAP Tests 7 | CLI 8 | 9 |

    10 | 11 | --- 12 | 13 | **Documentation**: https://supabase.github.io/dbdev 14 | 15 | **Source Code**: https://github.com/supabase/dbdev 16 | 17 | --- 18 | 19 | ## Overview 20 | 21 | dbdev is a package manager for Postgres [trusted language extensions](https://github.com/aws/pg_tle) (TLEs). It consists of: 22 | 23 | - [database.dev](https://database.dev): our first-party package registry to store and distribute TLEs. 24 | - dbdev CLI: a CLI for publishing TLEs to a registry. This CLI will continue to be available in the short term, but we plan to merge it into the Supabase CLI in the future. 25 | - dbdev client (deprecated): an in-database client for installing TLEs from registries. 26 | 27 | !!! warning 28 | 29 | The in-database client is deprecated and will be removed in the future. We recommend using the dbdev CLI's `dbdev add` command to generate the SQL needed to install a TLE, and then including that SQL in your database as a migration file. 30 | 31 | If you want to publish your own TLE or install and extension from the registry, you will need the dbdev CLI. Follow its [installation instructions](cli.md#installation) to get started. 32 | 33 | !!! warning 34 | 35 | Restoring a logical backup of a database with a TLE installed can fail. For this reason, dbdev should only be used with databases with physical backups enabled. 36 | -------------------------------------------------------------------------------- /website/components/forms/FormInput.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef, forwardRef, PropsWithoutRef } from 'react' 2 | import { useField, UseFieldConfig } from 'react-final-form' 3 | import { Input } from '~/components/ui/input' 4 | import { Label } from '../ui/label' 5 | import { cn } from '~/lib/utils' 6 | 7 | export interface FormInputProps extends PropsWithoutRef< 8 | JSX.IntrinsicElements['input'] 9 | > { 10 | /** Field name. */ 11 | name: string 12 | /** Field label. */ 13 | label: string 14 | /** Field type. Doesn't include radio buttons and checkboxes */ 15 | type?: 'text' | 'password' | 'email' | 'number' 16 | outerProps?: PropsWithoutRef 17 | labelProps?: ComponentPropsWithoutRef<'label'> 18 | fieldProps?: UseFieldConfig 19 | } 20 | 21 | const FormInput = forwardRef( 22 | ( 23 | { name, label, className, outerProps, fieldProps, labelProps, ...props }, 24 | ref 25 | ) => { 26 | const { 27 | input, 28 | meta: { touched, error, submitError, submitting }, 29 | } = useField(name, { 30 | parse: 31 | props.type === 'number' 32 | ? (Number as any) 33 | : // Converting `""` to `null` ensures empty values will be set to null in the DB 34 | (v) => (v === '' ? null : v), 35 | ...fieldProps, 36 | }) 37 | 38 | const normalizedError = Array.isArray(error) 39 | ? error.join(', ') 40 | : error || submitError 41 | 42 | return ( 43 |
    44 | 47 | 54 | 55 | {touched && normalizedError && ( 56 |
    57 | {normalizedError} 58 |
    59 | )} 60 |
    61 | ) 62 | } 63 | ) 64 | 65 | FormInput.displayName = 'FormInput' 66 | 67 | export default FormInput 68 | -------------------------------------------------------------------------------- /website/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '~/lib/utils' 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
    17 | )) 18 | Card.displayName = 'Card' 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
    29 | )) 30 | CardHeader.displayName = 'CardHeader' 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
    44 | )) 45 | CardTitle.displayName = 'CardTitle' 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
    56 | )) 57 | CardDescription.displayName = 'CardDescription' 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |
    64 | )) 65 | CardContent.displayName = 'CardContent' 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
    76 | )) 77 | CardFooter.displayName = 'CardFooter' 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /supabase/migrations/20250106073735_jwt_secret_from_vault.sql: -------------------------------------------------------------------------------- 1 | -- app.settings.jwt_secret has been removed, see https://github.com/orgs/supabase/discussions/30606 2 | -- now we fetch the secret from vault. The redeem_access_token function is the same as before 3 | -- (see file 20230906110845_access_token.sql) except the part where we fetch the jwt_secret. 4 | create or replace function public.redeem_access_token( 5 | access_token text 6 | ) 7 | returns text 8 | language plpgsql 9 | security definer 10 | strict 11 | as $$ 12 | declare 13 | token_id uuid; 14 | token bytea; 15 | tokens_row app.user_id_and_token_hash; 16 | token_valid boolean; 17 | now timestamp; 18 | one_hour_from_now timestamp; 19 | issued_at int; 20 | expiry_at int; 21 | jwt_secret text; 22 | begin 23 | -- validate access token 24 | if length(access_token) != 64 then 25 | raise exception 'Invalid token'; 26 | end if; 27 | 28 | if substring(access_token from 1 for 4) != 'dbd_' then 29 | raise exception 'Invalid token'; 30 | end if; 31 | 32 | token_id := substring(access_token from 5 for 32)::uuid; 33 | token := app.base64url_decode(substring(access_token from 37)); 34 | 35 | select t.user_id, t.token_hash 36 | into tokens_row 37 | from app.access_tokens t 38 | where t.id = token_id; 39 | 40 | -- TODO: do a constant time comparison 41 | if tokens_row.token_hash != sha256(token) then 42 | raise exception 'Invalid token'; 43 | end if; 44 | 45 | -- Generate JWT token 46 | now := current_timestamp; 47 | one_hour_from_now := now + interval '1 hour'; 48 | issued_at := date_part('epoch', now); 49 | expiry_at := date_part('epoch', one_hour_from_now); 50 | 51 | -- read the jwt secret from vault 52 | select decrypted_secret 53 | into jwt_secret 54 | from vault.decrypted_secrets 55 | where name = 'app.jwt_secret'; 56 | 57 | return sign(json_build_object( 58 | 'aud', 'authenticated', 59 | 'role', 'authenticated', 60 | 'iss', 'database.dev', 61 | 'sub', tokens_row.user_id, 62 | 'iat', issued_at, 63 | 'exp', expiry_at 64 | ), jwt_secret); 65 | end; 66 | $$; 67 | -------------------------------------------------------------------------------- /website/data/packages/packages-query.ts: -------------------------------------------------------------------------------- 1 | import { PostgrestError } from '@supabase/supabase-js' 2 | import { 3 | QueryClient, 4 | useQuery, 5 | useQueryClient, 6 | UseQueryOptions, 7 | } from '@tanstack/react-query' 8 | import { useCallback } from 'react' 9 | import supabase from '~/lib/supabase' 10 | import { NonNullableObject } from '~/lib/types' 11 | import { Database } from '../database.types' 12 | 13 | export type PackagesVariables = { 14 | handle?: string 15 | } 16 | 17 | export type PackagesResponse = NonNullableObject< 18 | Database['public']['Views']['packages']['Row'] 19 | >[] 20 | 21 | export async function getPackages( 22 | { handle }: PackagesVariables, 23 | signal?: AbortSignal 24 | ) { 25 | if (!handle) { 26 | throw new Error('handle is required') 27 | } 28 | 29 | let query = supabase.from('packages').select('*').eq('handle', handle) 30 | 31 | if (signal) { 32 | query = query.abortSignal(signal) 33 | } 34 | 35 | const { data, error } = await query.returns() 36 | 37 | if (error) { 38 | throw error 39 | } 40 | 41 | return data ?? [] 42 | } 43 | 44 | export type PackagesData = Awaited> 45 | export type PackagesError = PostgrestError 46 | 47 | export const usePackagesQuery = ( 48 | { handle }: PackagesVariables, 49 | { 50 | enabled = true, 51 | ...options 52 | }: UseQueryOptions = {} 53 | ) => 54 | useQuery( 55 | ['packages', { handle }], 56 | ({ signal }) => getPackages({ handle }, signal), 57 | { 58 | enabled: enabled && typeof handle !== 'undefined', 59 | ...options, 60 | } 61 | ) 62 | 63 | export const prefetchPackages = ( 64 | client: QueryClient, 65 | { handle }: PackagesVariables 66 | ) => { 67 | return client.prefetchQuery(['packages', { handle }], ({ signal }) => 68 | getPackages({ handle }, signal) 69 | ) 70 | } 71 | 72 | export const usePackagesPrefetch = ({ handle }: PackagesVariables) => { 73 | const client = useQueryClient() 74 | 75 | return useCallback(() => { 76 | if (handle) { 77 | prefetchPackages(client, { handle }) 78 | } 79 | }, [client, handle]) 80 | } 81 | -------------------------------------------------------------------------------- /docs/install-a-package.md: -------------------------------------------------------------------------------- 1 | You can install extensions available on the database.dev registry using the dbdev CLI's `add` command. The dbdev client is itself an extension which you can install by following the instructions below. 2 | 3 | ## Pre-requisites 4 | 5 | Before you can install a package, ensure you have the `pg_tle` extension installed in your database. 6 | 7 | !!! note 8 | 9 | If your database is running on Supabase, `pg_tle` is already installed. 10 | 11 | !!! warning 12 | 13 | Restoring a logical backup of a database with a TLE installed can fail. For this reason, dbdev should only be used with databases with physical backups enabled. 14 | 15 | ```sql 16 | create extension if not exists pg_tle; 17 | ``` 18 | 19 | ## Use 20 | 21 | Once the prerequisites are met, you can create a migration file to install a TLE available on database.dev by running the following dbdev command: 22 | 23 | ```bash 24 | dbdev add -o -v -s package -n 25 | ``` 26 | 27 | For example, to install `kiwicopple@pg_idkit` version 0.0.4 in `extensions` schema run: 28 | 29 | ```bash 30 | dbdev add -o "./migrations/" -v 0.0.4 -s extensions package -n kiwicopple@pg_idkit 31 | ``` 32 | 33 | To create a migration file to update to the latest version of a package, you need to specify the `-c` flag with the connection string to your database. The connection is used to check the current version of the package installed in the database and generate a migration file that will update it to the latest version available on database.dev. 34 | 35 | ```bash 36 | dbdev add -c -o package -n 37 | ``` 38 | To update the `kiwicopple@pg_idkit` package to the latest version, you can run: 39 | 40 | ```bash 41 | dbdev add -c "postgresql://postgres:[YOUR-PASSWORD]@[YOUR-HOST]:5432/postgres" -o "./migrations/" package -n kiwicopple@pg_idkit 42 | ``` 43 | 44 | !!! warning 45 | 46 | To generate a correct update migration file, ensure that before running the `dbdev add` command, all existing migrations in the `migrations` folder have been applied to the database. The `dbdev add` command looks for existing installed extensions in the database and generates a migration file that will install the TLE if it is not already installed. 47 | -------------------------------------------------------------------------------- /website/components/themes/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { 3 | createContext, 4 | PropsWithChildren, 5 | useCallback, 6 | useContext, 7 | useEffect, 8 | useMemo, 9 | useState, 10 | } from 'react' 11 | 12 | export const LOCAL_STORAGE_KEY = 'dbdev_theme' 13 | export const DEFAULT_THEME = 14 | typeof window !== 'undefined' && 15 | window.matchMedia('(prefers-color-scheme: dark)').matches 16 | ? 'dark' 17 | : 'light' 18 | 19 | export type Theme = 'light' | 'dark' 20 | export function isValidTheme(theme: string): theme is Theme { 21 | return theme === 'light' || theme === 'dark' 22 | } 23 | 24 | type ThemeContextType = { 25 | theme: Theme 26 | setTheme: (theme: Theme) => void 27 | } 28 | 29 | const ThemeContext = createContext({ 30 | theme: 'light', 31 | setTheme: () => {}, 32 | }) 33 | 34 | export default ThemeContext 35 | 36 | export const useThemeContext = () => useContext(ThemeContext) 37 | 38 | export const useTheme = () => useThemeContext().theme 39 | 40 | type ThemeContextProviderProps = {} 41 | 42 | export const ThemeContextProvider = ({ 43 | children, 44 | }: PropsWithChildren) => { 45 | const [theme, setTheme] = useState('light') 46 | 47 | useEffect(() => { 48 | const item = window.localStorage.getItem(LOCAL_STORAGE_KEY) 49 | if (item && isValidTheme(item)) { 50 | setTheme(item) 51 | } else { 52 | setTheme(DEFAULT_THEME) 53 | } 54 | }, []) 55 | 56 | useEffect(() => { 57 | if (theme === 'dark') document.body.classList.replace('light', 'dark') 58 | if (theme === 'light') document.body.classList.replace('dark', 'light') 59 | }, [theme]) 60 | 61 | const onSetTheme = useCallback((theme: Theme) => { 62 | setTheme(theme) 63 | window.localStorage.setItem(LOCAL_STORAGE_KEY, theme) 64 | }, []) 65 | 66 | const value = useMemo( 67 | () => ({ 68 | theme, 69 | setTheme: onSetTheme, 70 | }), 71 | [theme, onSetTheme] 72 | ) 73 | 74 | return ( 75 | <> 76 | 77 | 81 | 82 | 83 | {children} 84 | 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /supabase/migrations/20231207071422_new_package_name.sql: -------------------------------------------------------------------------------- 1 | create or replace function app.to_package_name(handle app.valid_name, partial_name app.valid_name) 2 | returns text 3 | immutable 4 | language sql 5 | as $$ 6 | select format('%s@%s', $1, $2) 7 | $$; 8 | 9 | alter table app.packages 10 | add column package_alias text null; 11 | 12 | update app.packages 13 | set package_alias = format('%s@%s', handle, partial_name); 14 | 15 | -- add package_alias column to the views 16 | create or replace view public.packages as 17 | select 18 | pa.id, 19 | pa.package_name, 20 | pa.handle, 21 | pa.partial_name, 22 | newest_ver.version as latest_version, 23 | newest_ver.description_md, 24 | pa.control_description, 25 | pa.control_requires, 26 | pa.created_at, 27 | pa.default_version, 28 | pa.package_alias 29 | from 30 | app.packages pa, 31 | lateral ( 32 | select * 33 | from app.package_versions pv 34 | where pv.package_id = pa.id 35 | order by pv.version_struct desc 36 | limit 1 37 | ) newest_ver; 38 | 39 | create or replace view public.package_versions as 40 | select 41 | pv.id, 42 | pv.package_id, 43 | pa.package_name, 44 | pv.version, 45 | pv.sql, 46 | pv.description_md, 47 | pa.control_description, 48 | pa.control_requires, 49 | pv.created_at, 50 | pa.package_alias 51 | from 52 | app.packages pa 53 | join app.package_versions pv 54 | on pa.id = pv.package_id; 55 | 56 | create or replace view public.package_upgrades 57 | as 58 | select 59 | pu.id, 60 | pu.package_id, 61 | pa.package_name, 62 | pu.from_version, 63 | pu.to_version, 64 | pu.sql, 65 | pu.created_at, 66 | pa.package_alias 67 | from 68 | app.packages pa 69 | join app.package_upgrades pu 70 | on pa.id = pu.package_id; 71 | 72 | create or replace function public.register_download(package_name text) 73 | returns void 74 | language sql 75 | security definer 76 | as 77 | $$ 78 | insert into app.downloads(package_id) 79 | select id 80 | from app.packages ap 81 | where ap.package_name = $1 or ap.package_alias = $1 82 | $$; 83 | -------------------------------------------------------------------------------- /supabase/migrations/20220117155720_views.sql: -------------------------------------------------------------------------------- 1 | create view public.accounts as 2 | select 3 | acc.id, 4 | acc.handle, 5 | obj.name as avatar_path, 6 | acc.display_name, 7 | acc.bio, 8 | acc.contact_email, 9 | acc.created_at 10 | from 11 | app.accounts acc 12 | left join storage.objects obj 13 | on acc.avatar_id = obj.id; 14 | 15 | create view public.organizations as 16 | select 17 | org.id, 18 | org.handle, 19 | obj.name as avatar_path, 20 | org.display_name, 21 | org.bio, 22 | org.contact_email, 23 | org.created_at 24 | from 25 | app.organizations org 26 | left join storage.objects obj 27 | on org.avatar_id = obj.id; 28 | 29 | create view public.members as 30 | select 31 | aio.organization_id, 32 | aio.account_id, 33 | aio.role, 34 | aio.created_at 35 | from 36 | app.members aio; 37 | 38 | create view public.packages as 39 | select 40 | pa.id, 41 | pa.package_name, 42 | pa.handle, 43 | pa.partial_name, 44 | newest_ver.version as latest_version, 45 | newest_ver.description_md, 46 | pa.control_description, 47 | pa.control_requires, 48 | pa.created_at 49 | from 50 | app.packages pa, 51 | lateral ( 52 | select * 53 | from app.package_versions pv 54 | where pv.package_id = pa.id 55 | order by pv.version_struct 56 | limit 1 57 | ) newest_ver; 58 | 59 | create view public.package_versions as 60 | select 61 | pv.id, 62 | pv.package_id, 63 | pa.package_name, 64 | pv.version, 65 | pv.sql, 66 | pv.description_md, 67 | pa.control_description, 68 | pa.control_requires, 69 | pv.created_at 70 | from 71 | app.packages pa 72 | join app.package_versions pv 73 | on pa.id = pv.package_id; 74 | 75 | create view public.package_upgrades 76 | as 77 | select 78 | pu.id, 79 | pu.package_id, 80 | pa.package_name, 81 | pu.from_version, 82 | pu.to_version, 83 | pu.sql, 84 | pu.created_at 85 | from 86 | app.packages pa 87 | join app.package_upgrades pu 88 | on pa.id = pu.package_id; 89 | -------------------------------------------------------------------------------- /website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ['class'], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: '2rem', 14 | screens: { 15 | '2xl': '1400px', 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: 'hsl(var(--border))', 21 | input: 'hsl(var(--input))', 22 | ring: 'hsl(var(--ring))', 23 | background: 'hsl(var(--background))', 24 | foreground: 'hsl(var(--foreground))', 25 | primary: { 26 | DEFAULT: 'hsl(var(--primary))', 27 | foreground: 'hsl(var(--primary-foreground))', 28 | }, 29 | secondary: { 30 | DEFAULT: 'hsl(var(--secondary))', 31 | foreground: 'hsl(var(--secondary-foreground))', 32 | }, 33 | destructive: { 34 | DEFAULT: 'hsl(var(--destructive))', 35 | foreground: 'hsl(var(--destructive-foreground))', 36 | }, 37 | muted: { 38 | DEFAULT: 'hsl(var(--muted))', 39 | foreground: 'hsl(var(--muted-foreground))', 40 | }, 41 | accent: { 42 | DEFAULT: 'hsl(var(--accent))', 43 | foreground: 'hsl(var(--accent-foreground))', 44 | }, 45 | popover: { 46 | DEFAULT: 'hsl(var(--popover))', 47 | foreground: 'hsl(var(--popover-foreground))', 48 | }, 49 | card: { 50 | DEFAULT: 'hsl(var(--card))', 51 | foreground: 'hsl(var(--card-foreground))', 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)', 58 | }, 59 | keyframes: { 60 | 'accordion-down': { 61 | from: { height: 0 }, 62 | to: { height: 'var(--radix-accordion-content-height)' }, 63 | }, 64 | 'accordion-up': { 65 | from: { height: 'var(--radix-accordion-content-height)' }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | 'accordion-down': 'accordion-down 0.2s ease-out', 71 | 'accordion-up': 'accordion-up 0.2s ease-out', 72 | }, 73 | }, 74 | }, 75 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')], 76 | } 77 | -------------------------------------------------------------------------------- /website/lib/zod-form-validator-utils.ts: -------------------------------------------------------------------------------- 1 | // From: https://github.com/blitz-js/blitz/blob/main/packages/blitz/src/utils/zod.ts 2 | 3 | import type { ZodError } from 'zod' 4 | 5 | export type ParserType = 'sync' | 'async' 6 | 7 | export function recursiveFormatZodErrors(errors: any): any { 8 | let formattedErrors: any[] | Record = {} 9 | 10 | for (const key in errors) { 11 | if (key === '_errors') { 12 | continue 13 | } 14 | 15 | if (errors[key]?._errors?.[0]) { 16 | if (!isNaN(key as any) && !Array.isArray(formattedErrors)) { 17 | formattedErrors = [] 18 | } 19 | ;(formattedErrors as any)[key] = errors[key]._errors[0] 20 | } else { 21 | if (!isNaN(key as any) && !Array.isArray(formattedErrors)) { 22 | formattedErrors = [] 23 | } 24 | ;(formattedErrors as any)[key] = recursiveFormatZodErrors(errors[key]) 25 | } 26 | } 27 | 28 | return formattedErrors 29 | } 30 | 31 | export function formatZodError(error: ZodError) { 32 | if (!error || typeof error.format !== 'function') { 33 | throw new Error( 34 | 'The argument to formatZodError must be a zod error with error.format()' 35 | ) 36 | } 37 | 38 | const errors = error.format() 39 | return recursiveFormatZodErrors(errors) 40 | } 41 | 42 | const validateZodSchemaSync = 43 | (schema: any): any => 44 | (values: any) => { 45 | if (!schema) return {} 46 | try { 47 | schema.parse(values) 48 | return {} 49 | } catch (error: any) { 50 | return error.format ? formatZodError(error) : error.toString() 51 | } 52 | } 53 | 54 | const validateZodSchemaAsync = (schema: any) => async (values: any) => { 55 | if (!schema) return {} 56 | try { 57 | await schema.parseAsync(values) 58 | return {} 59 | } catch (error: any) { 60 | return error.format ? formatZodError(error) : error.toString() 61 | } 62 | } 63 | 64 | // type zodSchemaReturn = typeof validateZodSchemaAsync | typeof validateZodSchemaSync 65 | // : (((values:any) => any) | ((values:any) => Promise)) => 66 | export function validateZodSchema( 67 | schema: any, 68 | parserType: 'sync' 69 | ): (values: any) => any 70 | export function validateZodSchema( 71 | schema: any, 72 | parserType: 'async' 73 | ): (values: any) => Promise 74 | export function validateZodSchema(schema: any): (values: any) => Promise 75 | export function validateZodSchema( 76 | schema: any, 77 | parserType: ParserType = 'async' 78 | ) { 79 | if (parserType === 'sync') { 80 | return validateZodSchemaSync(schema) 81 | } else { 82 | return validateZodSchemaAsync(schema) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /website/data/organizations/users-organizations-query.ts: -------------------------------------------------------------------------------- 1 | import { PostgrestError } from '@supabase/supabase-js' 2 | import { 3 | QueryClient, 4 | useQuery, 5 | useQueryClient, 6 | UseQueryOptions, 7 | } from '@tanstack/react-query' 8 | import { useCallback } from 'react' 9 | import supabase from '~/lib/supabase' 10 | import { NonNullableObject } from '~/lib/types' 11 | import { Database } from '../database.types' 12 | 13 | export type UsersOrganizationsVariables = { 14 | userId?: string 15 | } 16 | 17 | export type UsersOrganizationsResponse = NonNullableObject< 18 | Database['public']['Views']['organizations']['Row'] 19 | >[] 20 | 21 | export async function getUsersOrganizations( 22 | { userId }: UsersOrganizationsVariables, 23 | signal?: AbortSignal 24 | ) { 25 | if (!userId) { 26 | throw new Error('userId is required') 27 | } 28 | 29 | let query = supabase 30 | .from('organizations') 31 | .select('*,members!inner(account_id)') 32 | .eq('members.account_id', userId) 33 | .order('created_at', { ascending: false }) 34 | 35 | if (signal) { 36 | query = query.abortSignal(signal) 37 | } 38 | 39 | const { data, error } = await query.returns() 40 | 41 | if (error) { 42 | throw error 43 | } 44 | 45 | return data ?? [] 46 | } 47 | 48 | export type UsersOrganizationsData = Awaited< 49 | ReturnType 50 | > 51 | export type UsersOrganizationsError = PostgrestError 52 | 53 | export const useUsersOrganizationsQuery = ( 54 | { userId }: UsersOrganizationsVariables, 55 | { 56 | enabled = true, 57 | ...options 58 | }: UseQueryOptions< 59 | UsersOrganizationsData, 60 | UsersOrganizationsError, 61 | TData 62 | > = {} 63 | ) => 64 | useQuery( 65 | ['users', userId, 'organizations'], 66 | ({ signal }) => getUsersOrganizations({ userId }, signal), 67 | { 68 | enabled: enabled && typeof userId !== 'undefined', 69 | ...options, 70 | } 71 | ) 72 | 73 | export const prefetchUsersOrganizations = ( 74 | client: QueryClient, 75 | { userId }: UsersOrganizationsVariables 76 | ) => { 77 | return client.prefetchQuery( 78 | ['users', userId, 'organizations'], 79 | ({ signal }) => getUsersOrganizations({ userId }, signal) 80 | ) 81 | } 82 | 83 | export const useUsersOrganizationsPrefetch = ({ 84 | userId, 85 | }: UsersOrganizationsVariables) => { 86 | const client = useQueryClient() 87 | 88 | return useCallback(() => { 89 | if (userId) { 90 | prefetchUsersOrganizations(client, { userId }) 91 | } 92 | }, [client, userId]) 93 | } 94 | -------------------------------------------------------------------------------- /supabase/migrations/20220117142137_package_tables.sql: -------------------------------------------------------------------------------- 1 | insert into storage.buckets (id, name) 2 | values 3 | ('package_versions', 'package_versions'), 4 | ('package_upgrades', 'package_upgrades'); 5 | 6 | create function app.to_package_name(handle app.valid_name, partial_name app.valid_name) 7 | returns text 8 | immutable 9 | language sql 10 | as $$ 11 | select format('%s-%s', $1, $2) 12 | $$; 13 | 14 | create table app.packages( 15 | id uuid primary key default gen_random_uuid(), 16 | package_name text not null generated always as (app.to_package_name(handle, partial_name)) stored, 17 | handle app.valid_name not null references app.handle_registry(handle), 18 | partial_name app.valid_name not null, -- ex: math 19 | control_description varchar(1000), 20 | control_relocatable bool not null default false, 21 | control_requires varchar(128)[] default '{}'::varchar(128)[], 22 | created_at timestamptz not null default now(), 23 | unique (handle, partial_name) 24 | ); 25 | create index packages_partial_name_search_idx on app.packages using gin (partial_name extensions.gin_trgm_ops); 26 | create index packages_handle_search_idx on app.packages using gin (handle extensions.gin_trgm_ops); 27 | 28 | create table app.package_versions( 29 | id uuid primary key default gen_random_uuid(), 30 | package_id uuid not null references app.packages(id), 31 | version_struct app.semver not null, 32 | version text not null generated always as (app.semver_to_text(version_struct)) stored, 33 | sql varchar(250000), 34 | description_md varchar(250000), 35 | created_at timestamptz not null default now(), 36 | unique(package_id, version_struct) 37 | ); 38 | 39 | create table app.package_upgrades( 40 | id uuid primary key default gen_random_uuid(), 41 | package_id uuid not null references app.packages(id), 42 | from_version_struct app.semver not null, 43 | from_version text not null generated always as (app.semver_to_text(from_version_struct)) stored, 44 | to_version_struct app.semver not null, 45 | to_version text not null generated always as (app.semver_to_text(to_version_struct)) stored, 46 | sql varchar(250000), 47 | created_at timestamptz not null default now(), 48 | unique(package_id, from_version_struct, to_version_struct) 49 | ); 50 | 51 | create function app.version_text_to_handle(version text) 52 | returns app.valid_name 53 | immutable 54 | language sql 55 | as $$ 56 | select split_part($1, '-', 1) 57 | $$; 58 | 59 | create function app.version_text_to_package_partial_name(version text) 60 | returns app.valid_name 61 | immutable 62 | language sql 63 | as $$ 64 | select split_part($1, '--', 2) 65 | $$; 66 | -------------------------------------------------------------------------------- /cli/src/commands/list.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use futures::TryStreamExt; 4 | use sqlx::PgConnection; 5 | 6 | use crate::commands::install::update_paths; 7 | 8 | pub(crate) async fn list(conn: &mut PgConnection) -> anyhow::Result<()> { 9 | let available_extension_versions = available_extensions_versions(conn).await?; 10 | let available_extensions = available_extensions(conn).await?; 11 | 12 | for (extension_name, versions) in available_extension_versions { 13 | let default_version = available_extensions.get(&extension_name); 14 | 15 | println!("{extension_name}"); 16 | println!(" available versions:"); 17 | for version in &versions { 18 | print!(" {version}"); 19 | if Some(version) == default_version { 20 | print!(" (default)"); 21 | } 22 | println!(); 23 | } 24 | 25 | println!(" available update paths:"); 26 | let update_paths = update_paths(conn, &extension_name).await?; 27 | if update_paths.is_empty() { 28 | println!(" None"); 29 | } else { 30 | for update_path in update_paths { 31 | println!(" from {} to {}", update_path.source, update_path.target); 32 | } 33 | } 34 | println!(); 35 | } 36 | 37 | Ok(()) 38 | } 39 | 40 | #[derive(sqlx::FromRow)] 41 | struct AvailableExtensionVersion { 42 | name: String, 43 | version: String, 44 | } 45 | 46 | async fn available_extensions_versions( 47 | conn: &mut PgConnection, 48 | ) -> anyhow::Result>> { 49 | let mut rows = sqlx::query_as::<_, AvailableExtensionVersion>( 50 | "select name, version from pgtle.available_extension_versions()", 51 | ) 52 | .fetch(conn); 53 | 54 | let mut available_extensions = HashMap::new(); 55 | while let Some(row) = rows.try_next().await? { 56 | let versions: &mut Vec = available_extensions.entry(row.name).or_default(); 57 | versions.push(row.version); 58 | } 59 | 60 | Ok(available_extensions) 61 | } 62 | 63 | #[derive(sqlx::FromRow)] 64 | struct AvailableExtension { 65 | name: String, 66 | default_version: String, 67 | } 68 | 69 | async fn available_extensions(conn: &mut PgConnection) -> anyhow::Result> { 70 | let mut rows = sqlx::query_as::<_, AvailableExtension>( 71 | "select name, default_version from pgtle.available_extensions()", 72 | ) 73 | .fetch(conn); 74 | 75 | let mut extensions = HashMap::new(); 76 | while let Some(row) = rows.try_next().await? { 77 | extensions.insert(row.name, row.default_version); 78 | } 79 | 80 | Ok(extensions) 81 | } 82 | -------------------------------------------------------------------------------- /supabase/migrations/20230405163940_download_metrics.sql: -------------------------------------------------------------------------------- 1 | -- Return API request headers 2 | create or replace function app.api_request_headers() 3 | returns json 4 | language sql 5 | stable 6 | as 7 | $$ 8 | select coalesce(current_setting('request.headers', true)::json, '{}'::json); 9 | $$; 10 | 11 | 12 | -- Return specific API request header 13 | create or replace function app.api_request_header(text) 14 | returns text 15 | language sql 16 | stable 17 | as 18 | $$ 19 | select app.api_request_headers() ->> $1 20 | $$; 21 | 22 | 23 | -- IP address of current API request 24 | create or replace function app.api_request_ip() 25 | returns inet 26 | language sql 27 | stable 28 | as 29 | $$ 30 | select split_part(app.api_request_header('x-forwarded-for') || ',', ',', 1)::inet 31 | $$; 32 | 33 | -- IP address of current API request 34 | create or replace function app.api_request_client_info() 35 | returns text 36 | language sql 37 | stable 38 | as 39 | $$ 40 | select app.api_request_header('x-client-info') 41 | $$; 42 | 43 | 44 | create table app.downloads( 45 | id uuid primary key default gen_random_uuid(), 46 | package_id uuid not null references app.packages(id), 47 | ip inet not null default app.api_request_ip()::inet, 48 | client_info text default app.api_request_client_info(), 49 | created_at timestamptz not null default now() 50 | ); 51 | 52 | -- Speed up metrics query 53 | create index downloads_package_id_ip 54 | on app.downloads (package_id); 55 | 56 | create index downloads_created_at 57 | on app.downloads 58 | using brin(created_at); 59 | 60 | create or replace function public.register_download(package_name text) 61 | returns void 62 | language sql 63 | security definer 64 | as 65 | $$ 66 | insert into app.downloads(package_id) 67 | select id 68 | from app.packages ap 69 | where ap.package_name = $1 70 | $$; 71 | 72 | 73 | -- Public facing download metrics view. For website only. Not a stable part of dbdev API 74 | create materialized view public.download_metrics 75 | as 76 | select 77 | dl.package_id, 78 | count(dl.id) downloads_all_time, 79 | count(dl.id) filter (where dl.created_at > now() - '180 days'::interval) downloads_180_days, 80 | count(dl.id) filter (where dl.created_at > now() - '90 days'::interval) downloads_90_days, 81 | count(dl.id) filter (where dl.created_at > now() - '30 days'::interval) downloads_30_day 82 | from 83 | app.downloads dl 84 | group by 85 | dl.package_id; 86 | 87 | 88 | -- High frequency refresh for debugging 89 | select cron.schedule( 90 | 'refresh download metrics', 91 | '*/30 * * * *', 92 | 'refresh materialized view public.download_metrics;' 93 | ); 94 | -------------------------------------------------------------------------------- /website/pages/forgot-password.tsx: -------------------------------------------------------------------------------- 1 | import { isAuthApiError } from '@supabase/supabase-js' 2 | import Head from 'next/head' 3 | import { useRouter } from 'next/router' 4 | import toast from 'react-hot-toast' 5 | import Form, { FORM_ERROR } from '~/components/forms/Form' 6 | import FormButton from '~/components/forms/FormButton' 7 | import FormInput from '~/components/forms/FormInput' 8 | import Layout from '~/components/layouts/Layout' 9 | import H1 from '~/components/ui/typography/h1' 10 | import { useForgotPasswordMutation } from '~/data/auth/forgot-password-mutation' 11 | import { NextPageWithLayout } from '~/lib/types' 12 | import { ForgotPasswordSchema } from '~/lib/validations' 13 | 14 | const ForgotPasswordPage: NextPageWithLayout = () => { 15 | const router = useRouter() 16 | const { mutateAsync: forgotPassword } = useForgotPasswordMutation({ 17 | onSuccess() { 18 | toast.success( 19 | 'Password reset email sent successfully! Please check your inbox.' 20 | ) 21 | router.push('/sign-in') 22 | }, 23 | }) 24 | 25 | return ( 26 |
    27 | 28 | Forgot Password | The Database Package Manager 29 | 30 | 31 |
    32 |

    Forgot Password

    33 | 34 |
    { 39 | try { 40 | await forgotPassword({ 41 | email, 42 | redirectTo: `${location.origin}/reset-password`, 43 | }) 44 | } catch (error: any) { 45 | if (isAuthApiError(error)) { 46 | return { 47 | [FORM_ERROR]: error.message, 48 | } 49 | } 50 | 51 | return { 52 | [FORM_ERROR]: 53 | 'Sorry, we had an unexpected error. Please try again. - ' + 54 | error.toString(), 55 | } 56 | } 57 | }} 58 | schema={ForgotPasswordSchema} 59 | > 60 |
    61 | 67 |
    68 | 69 | Send Reset Email 70 |
    71 |
    72 |
    73 | ) 74 | } 75 | 76 | ForgotPasswordPage.getLayout = (page) => {page} 77 | 78 | export default ForgotPasswordPage 79 | -------------------------------------------------------------------------------- /supabase/migrations/20220117142141_security_utilities.sql: -------------------------------------------------------------------------------- 1 | create function app.is_organization_maintainer(account_id uuid, organization_id uuid) 2 | returns boolean 3 | language sql 4 | stable 5 | as $$ 6 | -- Does the currently authenticated user have permission to admin orgs and org members? 7 | select 8 | exists( 9 | select 10 | 1 11 | from 12 | app.members m 13 | where 14 | m.account_id = $1 15 | and m.organization_id = $2 16 | and m.role = 'maintainer' 17 | ) 18 | $$; 19 | 20 | 21 | create function app.is_handle_maintainer(account_id uuid, handle app.valid_name) 22 | returns boolean 23 | language sql 24 | stable 25 | as $$ 26 | select 27 | exists( 28 | select 29 | 1 30 | from 31 | app.accounts acc 32 | where 33 | acc.id = $1 34 | and acc.handle = $2 35 | ) 36 | or exists( 37 | select 38 | 1 39 | from 40 | app.organizations o 41 | join app.members m 42 | on o.id = m.organization_id 43 | where 44 | m.role = 'maintainer' 45 | and m.account_id = $1 46 | and o.handle = $2 47 | ) 48 | $$; 49 | 50 | 51 | 52 | 53 | create function app.is_package_maintainer(account_id uuid, package_id uuid) 54 | returns boolean 55 | language sql 56 | stable 57 | as $$ 58 | select 59 | exists( 60 | select 61 | 1 62 | from 63 | app.accounts acc 64 | join app.packages p 65 | on acc.handle = p.handle 66 | where 67 | acc.id = $1 68 | and p.id = $2 69 | ) 70 | or exists( 71 | -- current user is maintainer of org that owns the package 72 | select 73 | 1 74 | from 75 | app.packages p 76 | join app.organizations o 77 | on p.handle = o.handle 78 | join app.members m 79 | on o.id = m.organization_id 80 | where 81 | m.role = 'maintainer' 82 | and m.account_id = $1 83 | and p.id = $2 84 | ) 85 | $$; 86 | 87 | 88 | create function app.is_package_version_maintainer(account_id uuid, package_version_id uuid) 89 | returns boolean 90 | language sql 91 | stable 92 | as $$ 93 | select 94 | app.is_package_maintainer($1, pv.package_id) 95 | from 96 | app.package_versions pv 97 | where 98 | pv.id = $2 99 | $$; 100 | -------------------------------------------------------------------------------- /website/pages/reset-password.tsx: -------------------------------------------------------------------------------- 1 | import { isAuthApiError } from '@supabase/supabase-js' 2 | import Head from 'next/head' 3 | import { useRouter } from 'next/router' 4 | import toast from 'react-hot-toast' 5 | import Form, { FORM_ERROR } from '~/components/forms/Form' 6 | import FormButton from '~/components/forms/FormButton' 7 | import FormInput from '~/components/forms/FormInput' 8 | import Layout from '~/components/layouts/Layout' 9 | import H1 from '~/components/ui/typography/h1' 10 | import { useUpdatePasswordMutation } from '~/data/auth/password-update-mutation' 11 | import { NextPageWithLayout } from '~/lib/types' 12 | import { ResetPasswordSchema } from '~/lib/validations' 13 | 14 | const ResetPasswordPage: NextPageWithLayout = () => { 15 | const router = useRouter() 16 | const { mutateAsync: updatePassword } = useUpdatePasswordMutation({ 17 | onSuccess() { 18 | toast.success('Password updated successfully!') 19 | router.push('/') 20 | }, 21 | }) 22 | 23 | return ( 24 |
    25 | 26 | Reset Password | The Database Package Manager 27 | 28 | 29 |
    30 |

    Reset Password

    31 | 32 |
    { 38 | try { 39 | await updatePassword({ 40 | newPassword: password, 41 | }) 42 | } catch (error: any) { 43 | if (isAuthApiError(error)) { 44 | return { 45 | [FORM_ERROR]: error.message, 46 | } 47 | } 48 | 49 | return { 50 | [FORM_ERROR]: 51 | 'Sorry, we had an unexpected error. Please try again. - ' + 52 | error.toString(), 53 | } 54 | } 55 | }} 56 | schema={ResetPasswordSchema} 57 | > 58 |
    59 | 65 | 66 | 72 |
    73 | 74 | Save Password 75 |
    76 |
    77 |
    78 | ) 79 | } 80 | 81 | ResetPasswordPage.getLayout = (page) => {page} 82 | 83 | export default ResetPasswordPage 84 | -------------------------------------------------------------------------------- /cli/src/util.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use futures::TryStreamExt; 3 | use regex::Regex; 4 | use sqlx::postgres::PgConnection; 5 | use sqlx::Connection; 6 | use std::collections::HashSet; 7 | use std::fs::{File, OpenOptions}; 8 | use std::path::Path; 9 | 10 | use crate::models::{ExtensionVersion, UpdatePath}; 11 | 12 | pub async fn get_connection(connection_str: &str) -> anyhow::Result { 13 | PgConnection::connect(connection_str) 14 | .await 15 | .context("Failed to establish PostgreSQL connection") 16 | } 17 | 18 | pub fn is_valid_extension_name(name: &str) -> bool { 19 | let name_regex = Regex::new(r"^[A-z][A-z0-9\_]{2,32}$").expect("regex is valid"); 20 | name_regex.is_match(name) 21 | } 22 | 23 | pub fn is_valid_version(version: &str) -> bool { 24 | let nums: Vec<&str> = version.split('.').collect(); 25 | if nums.len() != 3 { 26 | println!("1"); 27 | return false; 28 | } 29 | 30 | for num_str in nums { 31 | let num: i16 = match num_str.parse() { 32 | Ok(n) => n, 33 | _ => return false, 34 | }; 35 | if num < 0 { 36 | return false; 37 | } 38 | } 39 | 40 | true 41 | } 42 | 43 | pub async fn extension_versions( 44 | conn: &mut PgConnection, 45 | extension_name: &str, 46 | ) -> anyhow::Result> { 47 | let mut rows = sqlx::query_as::<_, ExtensionVersion>( 48 | "select version from pgtle.available_extension_versions() where name = $1", 49 | ) 50 | .bind(extension_name) 51 | .fetch(conn); 52 | 53 | let mut versions = HashSet::new(); 54 | while let Some(installed_version) = rows.try_next().await? { 55 | versions.insert(installed_version.version); 56 | } 57 | 58 | Ok(versions) 59 | } 60 | 61 | pub(crate) async fn update_paths( 62 | conn: &mut PgConnection, 63 | extension_name: &str, 64 | ) -> anyhow::Result> { 65 | let mut rows = sqlx::query_as::<_, UpdatePath>( 66 | "select source, target from pgtle.extension_update_paths($1) where path is not null;", 67 | ) 68 | .bind(extension_name) 69 | .fetch(conn); 70 | 71 | let mut paths = HashSet::new(); 72 | while let Some(update_path) = rows.try_next().await? { 73 | paths.insert(update_path); 74 | } 75 | 76 | Ok(paths) 77 | } 78 | 79 | #[cfg(target_family = "unix")] 80 | pub(crate) fn create_file(path: &Path) -> Result { 81 | use std::os::unix::fs::OpenOptionsExt; 82 | 83 | let mut options = OpenOptions::new(); 84 | // read/write permissions for owner, none for other 85 | options.create(true).write(true).mode(0o600); 86 | options.open(path) 87 | } 88 | 89 | #[cfg(not(target_family = "unix"))] 90 | pub(crate) fn create_file(path: &Path) -> Result { 91 | let mut options = OpenOptions::new(); 92 | options.create(true).write(true); 93 | options.open(path) 94 | } 95 | -------------------------------------------------------------------------------- /website/data/profiles/profile-query.ts: -------------------------------------------------------------------------------- 1 | import { PostgrestError } from '@supabase/supabase-js' 2 | import { 3 | QueryClient, 4 | useQuery, 5 | useQueryClient, 6 | UseQueryOptions, 7 | } from '@tanstack/react-query' 8 | import { useCallback } from 'react' 9 | import { getAvatarUrl } from '~/lib/avatars' 10 | import supabase from '~/lib/supabase' 11 | import { NotFoundError } from '../utils' 12 | 13 | export type ProfileVariables = { 14 | handle?: string 15 | } 16 | 17 | export async function getProfile( 18 | { handle }: ProfileVariables, 19 | signal?: AbortSignal 20 | ) { 21 | if (!handle) { 22 | throw new Error('handle is required') 23 | } 24 | 25 | let accountQuery = supabase.rpc('get_account', { handle }) 26 | let organizationQuery = supabase.rpc('get_organization', { handle }) 27 | 28 | if (signal) { 29 | accountQuery = accountQuery.abortSignal(signal) 30 | organizationQuery = organizationQuery.abortSignal(signal) 31 | } 32 | 33 | const [ 34 | { data: account, error: accountError }, 35 | { data: organization, error: organizationError }, 36 | ] = await Promise.all([ 37 | accountQuery.maybeSingle(), 38 | organizationQuery.maybeSingle(), 39 | ]) 40 | 41 | if (accountError) { 42 | throw accountError 43 | } 44 | 45 | if (organizationError) { 46 | throw organizationError 47 | } 48 | 49 | if (organization) { 50 | const avatar_url = getAvatarUrl(organization.avatar_path) 51 | 52 | return { ...organization, type: 'organization' as const, avatar_url } 53 | } 54 | 55 | if (account) { 56 | const avatar_url = getAvatarUrl(account.avatar_path) 57 | 58 | return { ...account, type: 'account' as const, avatar_url } 59 | } 60 | 61 | throw new NotFoundError('Account or organization not found') 62 | } 63 | 64 | export type ProfileData = Awaited> 65 | export type ProfileError = PostgrestError | NotFoundError 66 | 67 | export const useProfileQuery = ( 68 | { handle }: ProfileVariables, 69 | { 70 | enabled = true, 71 | ...options 72 | }: UseQueryOptions = {} 73 | ) => 74 | useQuery( 75 | ['profile', handle], 76 | ({ signal }) => getProfile({ handle }, signal), 77 | { 78 | enabled: enabled && typeof handle !== 'undefined', 79 | ...options, 80 | } 81 | ) 82 | 83 | export const prefetchProfile = ( 84 | client: QueryClient, 85 | { handle }: ProfileVariables 86 | ) => { 87 | return client.prefetchQuery(['profile', handle], ({ signal }) => 88 | getProfile({ handle }, signal) 89 | ) 90 | } 91 | 92 | export const useProfilePrefetch = () => { 93 | const client = useQueryClient() 94 | 95 | return useCallback( 96 | ({ handle }: ProfileVariables) => { 97 | if (handle) { 98 | return prefetchProfile(client, { handle }) 99 | } 100 | 101 | return Promise.resolve() 102 | }, 103 | [client] 104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /supabase/config.toml: -------------------------------------------------------------------------------- 1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working 2 | # directory name when running `supabase init`. 3 | project_id = "dbdev" 4 | 5 | [api] 6 | # Port to use for the API URL. 7 | port = 54321 8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 9 | # endpoints. public and storage are always included. 10 | schemas = ["public", "storage", "graphql_public"] 11 | # Extra schemas to add to the search_path of every request. public is always included. 12 | extra_search_path = ["public", "extensions"] 13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 14 | # for accidental or malicious requests. 15 | max_rows = 1000 16 | 17 | [db] 18 | # Port to use for the local database URL. 19 | port = 54322 20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 21 | # server_version;` on the remote database to check. 22 | major_version = 15 23 | 24 | [studio] 25 | # Port to use for Supabase Studio. 26 | port = 54323 27 | 28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 29 | # are monitored, and you can view the emails that would have been sent from the web interface. 30 | [inbucket] 31 | # Port to use for the email testing server web interface. 32 | port = 54324 33 | smtp_port = 54325 34 | pop3_port = 54326 35 | 36 | [storage] 37 | # The maximum file size allowed (e.g. "5MB", "500KB"). 38 | file_size_limit = "50MiB" 39 | 40 | [auth] 41 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 42 | # in emails. 43 | site_url = "http://localhost:3000" 44 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 45 | additional_redirect_urls = ["https://localhost:3000"] 46 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one 47 | # week). 48 | jwt_expiry = 3600 49 | # Allow/disallow new user signups to your project. 50 | enable_signup = true 51 | 52 | [auth.email] 53 | # Allow/disallow new user signups via email to your project. 54 | enable_signup = true 55 | # If enabled, a user will be required to confirm any email change on both the old, and new email 56 | # addresses. If disabled, only the new email is required to confirm. 57 | double_confirm_changes = true 58 | # If enabled, users need to confirm their email address before signing in. 59 | enable_confirmations = false 60 | 61 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 62 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`, 63 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`. 64 | [auth.external.apple] 65 | enabled = false 66 | client_id = "" 67 | secret = "" 68 | # Overrides the default auth redirectUrl. 69 | redirect_uri = "" 70 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 71 | # or any other third-party OIDC providers. 72 | url = "" 73 | -------------------------------------------------------------------------------- /website/data/packages/package-query.ts: -------------------------------------------------------------------------------- 1 | import { PostgrestError } from '@supabase/supabase-js' 2 | import { 3 | QueryClient, 4 | useQuery, 5 | useQueryClient, 6 | UseQueryOptions, 7 | } from '@tanstack/react-query' 8 | import { useCallback } from 'react' 9 | import supabase from '~/lib/supabase' 10 | import { NonNullableObject } from '~/lib/types' 11 | import { Database } from '../database.types' 12 | import { NotFoundError } from '../utils' 13 | 14 | export type PackageVariables = { 15 | handle?: string 16 | partialName?: string 17 | } 18 | 19 | export type Package = NonNullableObject< 20 | Database['public']['Views']['packages']['Row'] 21 | > 22 | 23 | export type PackageResponse = Package & { 24 | download_metrics: { 25 | package_id: string 26 | downloads_30_day: number 27 | downloads_90_days: number 28 | downloads_180_days: number 29 | downloads_all_time: number 30 | } | null 31 | } 32 | 33 | export async function getPackage( 34 | { handle, partialName }: PackageVariables, 35 | signal?: AbortSignal 36 | ) { 37 | if (!handle) { 38 | throw new Error('handle is required') 39 | } 40 | if (!partialName) { 41 | throw new Error('partialName is required') 42 | } 43 | 44 | let query = supabase 45 | .from('packages') 46 | .select('*,download_metrics(*)') 47 | .eq('handle', handle) 48 | .eq('partial_name', partialName) 49 | 50 | if (signal) { 51 | query = query.abortSignal(signal) 52 | } 53 | 54 | const { data, error } = await query.maybeSingle() 55 | 56 | if (error) { 57 | throw error 58 | } 59 | 60 | if (!data) { 61 | throw new NotFoundError('Package not found') 62 | } 63 | 64 | return data 65 | } 66 | 67 | export type PackageData = Awaited> 68 | export type PackageError = PostgrestError | NotFoundError 69 | 70 | export const usePackageQuery = ( 71 | { handle, partialName }: PackageVariables, 72 | { 73 | enabled = true, 74 | ...options 75 | }: UseQueryOptions = {} 76 | ) => 77 | useQuery( 78 | ['package', handle, partialName], 79 | ({ signal }) => getPackage({ handle, partialName }, signal), 80 | { 81 | enabled: 82 | enabled && 83 | typeof handle !== 'undefined' && 84 | typeof partialName !== 'undefined', 85 | ...options, 86 | } 87 | ) 88 | 89 | export const prefetchPackage = ( 90 | client: QueryClient, 91 | { handle, partialName }: PackageVariables 92 | ) => { 93 | return client.prefetchQuery(['package', handle, partialName], ({ signal }) => 94 | getPackage({ handle, partialName }, signal) 95 | ) 96 | } 97 | 98 | export const usePackagePrefetch = ({ 99 | handle, 100 | partialName, 101 | }: PackageVariables) => { 102 | const client = useQueryClient() 103 | 104 | return useCallback(() => { 105 | if (handle && partialName) { 106 | prefetchPackage(client, { handle, partialName }) 107 | } 108 | }, [client, handle, partialName]) 109 | } 110 | -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | The `dbdev` CLI can be used for: 2 | 3 | - Installing TLEs from [database.dev](https://database.dev/) or your local machine. 4 | - Updating TLEs from [database.dev](https://database.dev/) or your local machine. 5 | - Generating migrations from packages available on [database.dev](https://database.dev/). 6 | - Publishing TLEs to [database.dev](https://database.dev/). 7 | 8 | ## Installation 9 | 10 | Installation is available through a native package, binary download or building from source. 11 | 12 | ### Native Package 13 | 14 | === "macOS" 15 | 16 | Install the CLI with [Homebrew](https://brew.sh/): 17 | ``` 18 | brew install supabase/tap/dbdev 19 | ``` 20 | 21 | === "Linux" 22 | 23 | Install the CLI with [Homebrew](https://brew.sh/): 24 | ``` 25 | brew install supabase/tap/dbdev 26 | ``` 27 | 28 | #### Linux packages 29 | 30 | Debian Linux packages are provided in [Releases](https://github.com/supabase/dbdev/releases). 31 | To install, download the `.deb` file and run the following: 32 | 33 | ``` 34 | sudo dpkg -i <...>.deb 35 | ``` 36 | 37 | === "Windows" 38 | 39 | Install the CLI with [Scoop](https://scoop.sh/). 40 | ``` 41 | scoop bucket add supabase https://github.com/supabase/scoop-bucket.git 42 | scoop install dbdev 43 | ``` 44 | 45 | ## Upgrading 46 | 47 | Use `dbdev --version` to check if you are on the latest version of the CLI. 48 | 49 | ### Native Package 50 | 51 | === "macOS" 52 | 53 | Upgrade the CLI with [Homebrew](https://brew.sh/): 54 | ``` 55 | brew upgrade dbdev 56 | ``` 57 | 58 | === "Linux" 59 | 60 | Install the CLI with [Homebrew](https://brew.sh/): 61 | ``` 62 | brew upgrade dbdev 63 | ``` 64 | 65 | #### Linux packages 66 | 67 | Debian Linux packages are provided in [Releases](https://github.com/supabase/dbdev/releases). 68 | To upgrade, download the `.deb` file and run the following: 69 | 70 | ``` 71 | sudo dpkg -i <...>.deb 72 | ``` 73 | 74 | === "Windows" 75 | 76 | Update the CLI with [Scoop](https://scoop.sh/). 77 | ``` 78 | scoop update dbdev 79 | ``` 80 | 81 | ### Binary Download 82 | 83 | Binaries for dbdev CLI are available for Linux, Windows and macOS platforms. Visit the [dbdev releases page](https://github.com/supabase/dbdev/releases) to download a binary for your OS. The downloaded binary should be placed in a folder which is in your [PATH](). 84 | 85 | ### Build From Source 86 | 87 | Alternatively, you can build the binary from source. You will need to have [Rust installed](https://www.rust-lang.org/tools/install) on your system. To build from source: 88 | 89 | 1. Clone the repo: `git clone https://github.com/supabase/dbdev.git`. 90 | 2. Change directory to `dbdev`: `cd dbdev`. 91 | 3. Build: `cargo install --release`. 92 | 4. Copy the `dbdev` binary in `target/release` to a folder in your PATH. 93 | 94 | If you have [cargo-install](https://doc.rust-lang.org/cargo/commands/cargo-install.html), you can perform all the above steps with a single command: `cargo install --git https://github.com/supabase/dbdev.git dbdev`. 95 | 96 | Now you're ready to [publish your first package](publish-extension.md). 97 | -------------------------------------------------------------------------------- /website/data/package-versions/package-versions-query.ts: -------------------------------------------------------------------------------- 1 | import { PostgrestError } from '@supabase/supabase-js' 2 | import { 3 | QueryClient, 4 | useQuery, 5 | useQueryClient, 6 | UseQueryOptions, 7 | } from '@tanstack/react-query' 8 | import { useCallback } from 'react' 9 | import supabase from '~/lib/supabase' 10 | import { NonNullableObject } from '~/lib/types' 11 | import { Database } from '../database.types' 12 | 13 | export type PackageVersion = NonNullableObject< 14 | Database['public']['Views']['package_versions']['Row'] 15 | > 16 | 17 | export type PackageVersionsVariables = { 18 | handle?: string 19 | partialName?: string 20 | } 21 | 22 | const SELECTED_COLUMNS = ['id', 'created_at', 'version'] as const 23 | 24 | export type PackageVersionsResponse = Pick< 25 | PackageVersion, 26 | (typeof SELECTED_COLUMNS)[number] 27 | >[] 28 | 29 | export async function getPackageVersions( 30 | { handle, partialName }: PackageVersionsVariables, 31 | signal?: AbortSignal 32 | ) { 33 | if (!handle) { 34 | throw new Error('handle is required') 35 | } 36 | if (!partialName) { 37 | throw new Error('partialName is required') 38 | } 39 | 40 | let query = supabase 41 | .from('package_versions') 42 | .select(SELECTED_COLUMNS.join(',')) 43 | .or( 44 | `package_name.eq.${handle}@${partialName},package_alias.eq.${handle}@${partialName}` 45 | ) 46 | .order('created_at', { ascending: false }) 47 | 48 | if (signal) { 49 | query = query.abortSignal(signal) 50 | } 51 | 52 | const { data, error } = await query.returns() 53 | 54 | if (error) { 55 | throw error 56 | } 57 | 58 | return data ?? [] 59 | } 60 | 61 | export type PackageVersionsData = Awaited> 62 | export type PackageVersionsError = PostgrestError 63 | 64 | export const usePackageVersionsQuery = ( 65 | { handle, partialName }: PackageVersionsVariables, 66 | { 67 | enabled = true, 68 | ...options 69 | }: UseQueryOptions = {} 70 | ) => 71 | useQuery( 72 | ['package-versions', handle, partialName], 73 | ({ signal }) => getPackageVersions({ handle, partialName }, signal), 74 | { 75 | enabled: 76 | enabled && 77 | typeof handle !== 'undefined' && 78 | typeof partialName !== 'undefined', 79 | ...options, 80 | } 81 | ) 82 | 83 | export const prefetchPackageVersions = ( 84 | client: QueryClient, 85 | { handle, partialName }: PackageVersionsVariables 86 | ) => { 87 | return client.prefetchQuery( 88 | ['package-versions', handle, partialName], 89 | ({ signal }) => getPackageVersions({ handle, partialName }, signal) 90 | ) 91 | } 92 | 93 | export const usePackageVersionsPrefetch = ({ 94 | handle, 95 | partialName, 96 | }: PackageVersionsVariables) => { 97 | const client = useQueryClient() 98 | 99 | return useCallback(() => { 100 | if (handle && partialName) { 101 | prefetchPackageVersions(client, { handle, partialName }) 102 | } 103 | }, [client, handle, partialName]) 104 | } 105 | -------------------------------------------------------------------------------- /website/data/packages/packages-search-query.ts: -------------------------------------------------------------------------------- 1 | import { PostgrestError } from '@supabase/supabase-js' 2 | import { 3 | QueryClient, 4 | useQuery, 5 | useQueryClient, 6 | UseQueryOptions, 7 | } from '@tanstack/react-query' 8 | import { useCallback } from 'react' 9 | import supabase from '~/lib/supabase' 10 | import { NonNullableObject } from '~/lib/types' 11 | import { Database } from '../database.types' 12 | 13 | export type PackagesSearchVariables = { 14 | query?: string 15 | } 16 | 17 | export type PackagesSearchResponse = NonNullableObject< 18 | Database['public']['Functions']['search_packages']['Returns'] 19 | > 20 | 21 | export async function searchPackages( 22 | { 23 | handle, 24 | partialName, 25 | }: { 26 | handle?: string 27 | partialName?: string 28 | }, 29 | signal?: AbortSignal 30 | ) { 31 | let query = supabase.rpc('search_packages', { 32 | handle, 33 | partial_name: partialName, 34 | }) 35 | 36 | if (signal) { 37 | query = query.abortSignal(signal) 38 | } 39 | 40 | const { data, error } = await query.returns() 41 | 42 | if (error) { 43 | throw error 44 | } 45 | 46 | return data ?? [] 47 | } 48 | 49 | export type PackagesSearchData = Awaited> 50 | export type PackagesSearchError = PostgrestError 51 | 52 | export const usePackagesSearchQuery = ( 53 | { query }: PackagesSearchVariables, 54 | { 55 | enabled, 56 | ...options 57 | }: UseQueryOptions = {} 58 | ) => { 59 | const { handle, partialName } = parseSearchQuery(query) 60 | 61 | return useQuery( 62 | ['packages', { type: 'search', handle, partialName }], 63 | ({ signal }) => searchPackages({ handle, partialName }, signal), 64 | { 65 | enabled: 66 | enabled && 67 | (typeof handle !== 'undefined' || typeof partialName !== 'undefined'), 68 | ...options, 69 | } 70 | ) 71 | } 72 | 73 | export const prefetchPackagesSearch = ( 74 | client: QueryClient, 75 | { query }: PackagesSearchVariables 76 | ) => { 77 | const { handle, partialName } = parseSearchQuery(query) 78 | 79 | return client.prefetchQuery( 80 | ['packages', { type: 'search', handle, partialName }], 81 | ({ signal }) => searchPackages({ handle, partialName }, signal) 82 | ) 83 | } 84 | 85 | export const usePackagesSearchPrefetch = ({ 86 | query, 87 | }: PackagesSearchVariables) => { 88 | const client = useQueryClient() 89 | 90 | return useCallback(() => { 91 | prefetchPackagesSearch(client, { query }) 92 | }, [client, query]) 93 | } 94 | 95 | export function parseSearchQuery(query?: string) { 96 | let [handle, partialName] = 97 | query?.trim().split('/') ?? ([] as (string | undefined)[]) 98 | 99 | // If the user only entered a partial name, then we'll search for that 100 | if (handle && !handle.startsWith('@') && !partialName) { 101 | partialName = handle 102 | handle = undefined 103 | } 104 | 105 | // Remove optional leading @ from handle 106 | handle = handle?.replace(/^@/, '') 107 | 108 | return { 109 | handle: handle?.trim() ?? undefined, 110 | partialName: partialName?.trim() ?? undefined, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /.github/workflows/release-scoop-bucket.yaml: -------------------------------------------------------------------------------- 1 | name: Release Scoop Bucket 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | tag: 7 | required: true 8 | type: string 9 | secrets: 10 | scoop_bucket_rw: 11 | required: true 12 | 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | release: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v6 21 | with: 22 | repository: supabase/scoop-bucket 23 | ref: "main" 24 | token: ${{ secrets.scoop_bucket_rw }} 25 | fetch-depth: 0 26 | 27 | - name: Compute tag and version 28 | id: vars 29 | run: | 30 | tag="${{ inputs.tag }}" 31 | echo "tag=${tag}" >> "$GITHUB_OUTPUT" 32 | # strip the leading v (if present) 33 | echo "version=${tag#v}" >> "$GITHUB_OUTPUT" 34 | 35 | - name: Download Windows AMD64 package 36 | uses: robinraju/release-downloader@v1.12 37 | with: 38 | repository: "supabase/dbdev" 39 | tag: ${{ inputs.tag }} 40 | fileName: "dbdev-${{ inputs.tag }}-windows-amd64.zip" 41 | 42 | - name: Generate Manifest File 43 | run: | 44 | windows_amd64_hash=`shasum -a 256 dbdev-${{ inputs.tag }}-windows-amd64.zip | cut -d" " -f1` 45 | 46 | version="${{ steps.vars.outputs.version }}" 47 | 48 | # update dbdev.json file 49 | echo '{' > dbdev.json 50 | echo " \"version\": \"${version}\"," >> dbdev.json 51 | echo ' "architecture": {' >> dbdev.json 52 | echo ' "64bit": {' >> dbdev.json 53 | echo " \"url\": \"https://github.com/supabase/dbdev/releases/download/v${version}/dbdev-v${version}-windows-amd64.zip\"," >> dbdev.json 54 | echo ' "bin": [' >> dbdev.json 55 | echo ' "dbdev.exe"' >> dbdev.json 56 | echo ' ],' >> dbdev.json 57 | echo " \"hash\": \"${windows_amd64_hash}\"" >> dbdev.json 58 | echo ' }' >> dbdev.json 59 | echo ' },' >> dbdev.json 60 | echo ' "homepage": "https://database.dev",' >> dbdev.json 61 | echo ' "license": "Apache",' >> dbdev.json 62 | echo ' "description": "CLI to help publish extensions to database.dev"' >> dbdev.json 63 | echo '}' >> dbdev.json 64 | echo '' 65 | 66 | # prepare a PR body file with some context 67 | echo "This PR updates the Scoop manifest for dbdev to version v${version}." > PR_BODY.md 68 | echo >> PR_BODY.md 69 | echo "It was auto-generated by the dbdev release workflow." >> PR_BODY.md 70 | 71 | - name: Create Pull Request 72 | uses: peter-evans/create-pull-request@v7 73 | with: 74 | token: ${{ secrets.scoop_bucket_rw }} 75 | commit-message: "Release dbdev version v${{ steps.vars.outputs.version }}" 76 | title: "Release dbdev version v${{ steps.vars.outputs.version }}" 77 | body-path: PR_BODY.md 78 | add-paths: | 79 | dbdev.json 80 | branch: "dbdev/update/v${{ steps.vars.outputs.version }}" 81 | base: "main" 82 | committer: "dbdev release-scoop-bucket.yaml workflow " 83 | author: "dbdev release-scoop-bucket.yaml workflow " 84 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # dbdev cli 2 | 3 | CLI tooling for creating, publishing, and installing [TLE](https://github.com/aws/pg_tle) packages. 4 | 5 | Note that this tooling is primarily intended for package authors. There is a WIP TLE package that will be pre-installed in the database (or installed via this CLI) that will be the main way end-users fetch packages into their database. For example: 6 | 7 | Once the `dbdev` TLE is installed 8 | 9 | ```sql 10 | select dbdev.install('math', '0.0.1'); 11 | ``` 12 | 13 | Where the `dbdev` CLI functions as a backup solution for installing packages when requirements for the `dbdev` TLE are not met (no [pgsql-http](https://github.com/pramsey/pgsql-http)) 14 | 15 | ``` 16 | dbdev install --connection 'postgresql://...' --package 'math' --version '0.0.1' 17 | ``` 18 | 19 | ## Objective Statements 20 | 21 | As a package author, I want to: 22 | 23 | - install extensions from a local directory into a database 24 | - sign up for an account with a package index 25 | - publish extensions to a package index 26 | 27 | As an end user, I want to: 28 | 29 | - install the TLE that enables remotely installing packages from the dbdev package index 30 | - install extensions from a package index into a database (a backup solution the dbdev TLE is not available in the database) 31 | - uninstall extensions from a database 32 | 33 | ## Interface 34 | 35 | ```sh 36 | Usage: dbdev [OPTIONS] 37 | 38 | Commands: 39 | install Install a package to a database 40 | uninstall Uninstall a package from a database 41 | help Print this message or the help of the given subcommand(s) 42 | 43 | 44 | signup Create a user account 45 | publish Upload a package to the package index 46 | 47 | Options: 48 | -d, --debug Turn debugging information on 49 | -h, --help Print help 50 | -V, --version Print version 51 | ``` 52 | 53 | ### install 54 | 55 | ``` 56 | Install a package to a database 57 | 58 | Usage: dbdev install [OPTIONS] --connection 59 | 60 | Options: 61 | -c, --connection PostgreSQL connection string 62 | -p, --package Package name on package index 63 | --path From local directory 64 | -h, --help Print help 65 | ``` 66 | 67 | ### uninstall 68 | 69 | ``` 70 | Uninstall a package from a database 71 | 72 | Usage: dbdev uninstall --connection --package 73 | 74 | Options: 75 | -c, --connection PostgreSQL connection string 76 | -p, --package Package name on dbdev package index 77 | -h, --help Print help 78 | ``` 79 | 80 | ### signup (NOT IMPLEMENTED) 81 | 82 | ``` 83 | Create a user account 84 | 85 | Usage: dbdev signup 86 | 87 | Arguments: 88 | PostgreSQL connection string 89 | 90 | Options: 91 | -h, --help Print help 92 | ``` 93 | 94 | The user is additionally prompted for an email address and password during signup. Packages can not be uploaded to an account until the email address is confirmed. 95 | 96 | ### publish (NOT IMPLEMENTED) 97 | 98 | ``` 99 | Upload a package to the package index 100 | 101 | Usage: dbdev publish 102 | 103 | Options: 104 | -h, --help Print help 105 | ``` 106 | 107 | Publishes the current directory's TLE package to the package index. 108 | -------------------------------------------------------------------------------- /supabase/migrations/20230906111353_publish_package.sql: -------------------------------------------------------------------------------- 1 | grant insert (partial_name, handle, control_description) 2 | on app.packages 3 | to authenticated; 4 | 5 | grant update (control_description) 6 | on app.packages 7 | to authenticated; 8 | 9 | create policy packages_update_policy 10 | on app.packages 11 | as permissive 12 | for update 13 | to authenticated 14 | using ( app.is_package_maintainer(auth.uid(), id) ); 15 | 16 | create or replace function public.publish_package( 17 | package_name app.valid_name, 18 | package_description varchar(1000) 19 | ) 20 | returns void 21 | language plpgsql 22 | as $$ 23 | declare 24 | account app.accounts = account from app.accounts account where id = auth.uid(); 25 | begin 26 | if account.handle is null then 27 | raise exception 'user not logged in'; 28 | end if; 29 | 30 | insert into app.packages(handle, partial_name, control_description) 31 | values (account.handle, package_name, package_description) 32 | on conflict on constraint packages_handle_partial_name_key 33 | do update 34 | set control_description = excluded.control_description; 35 | end; 36 | $$; 37 | 38 | create or replace function public.publish_package_version( 39 | package_name app.valid_name, 40 | version_source text, 41 | version_description text, 42 | version text 43 | ) 44 | returns uuid 45 | language plpgsql 46 | as $$ 47 | declare 48 | account app.accounts = account from app.accounts account where id = auth.uid(); 49 | package_id uuid; 50 | version_id uuid; 51 | begin 52 | if account.handle is null then 53 | raise exception 'user not logged in'; 54 | end if; 55 | 56 | select ap.id 57 | from app.packages ap 58 | where ap.handle = account.handle and ap.partial_name = publish_package_version.package_name 59 | into package_id; 60 | 61 | begin 62 | insert into app.package_versions(package_id, version_struct, sql, description_md) 63 | values (package_id, app.text_to_semver(version), version_source, version_description) 64 | returning id into version_id; 65 | 66 | return version_id; 67 | exception when unique_violation then 68 | return null; 69 | end; 70 | end; 71 | $$; 72 | 73 | create or replace function public.publish_package_upgrade( 74 | package_name app.valid_name, 75 | upgrade_source text, 76 | from_version text, 77 | to_version text 78 | ) 79 | returns uuid 80 | language plpgsql 81 | as $$ 82 | declare 83 | account app.accounts = account from app.accounts account where id = auth.uid(); 84 | package_id uuid; 85 | upgrade_id uuid; 86 | begin 87 | if account.handle is null then 88 | raise exception 'user not logged in'; 89 | end if; 90 | 91 | select ap.id 92 | from app.packages ap 93 | where ap.handle = account.handle and ap.partial_name = publish_package_upgrade.package_name 94 | into package_id; 95 | 96 | begin 97 | insert into app.package_upgrades(package_id, from_version_struct, to_version_struct, sql) 98 | values (package_id, app.text_to_semver(from_version), app.text_to_semver(to_version), upgrade_source) 99 | returning id into upgrade_id; 100 | 101 | return upgrade_id; 102 | exception when unique_violation then 103 | return null; 104 | end; 105 | end; 106 | $$; 107 | -------------------------------------------------------------------------------- /docs/publish-extension.md: -------------------------------------------------------------------------------- 1 | 2 | Let's create your first Trusted Language Extension for [database.dev](https://database.dev). 3 | 4 | ## Create your package 5 | 6 | Create a folder which will contain the extension: 7 | 8 | ``` 9 | mkdir my_first_tle 10 | cd my_first_tle 11 | ``` 12 | 13 | Next create a `hello_world--0.0.1.sql` file, which will contain your extension's SQL objects. Add the following function definition to this file: 14 | 15 | ```sql 16 | create function greet(name text default 'world') 17 | returns text 18 | language sql 19 | as $$ 20 | select 'hello, ' || name; 21 | $$; 22 | ``` 23 | 24 | Let's also add some docs about this extension. Create a `README.md` file and add the following content to it: 25 | 26 | ```markdown 27 | The `hello_world` extension provides a `greet` function, which returns a greeting. 28 | ``` 29 | 30 | Lastly, add a `hello_world.control` file with the following key-value pairs: 31 | 32 | ``` 33 | default_version = 0.0.1 34 | comment = 'An extension to generate greetings' 35 | relocatable = true 36 | ``` 37 | 38 | Your extension is ready to publish. Its name is `hello_world` and version is `0.0.1`. For details about what constitutes a valid extension, read about the [Structure of an Extension](extension_structure.md). 39 | 40 | 41 | ## Login to database.dev 42 | 43 | Before you can publish your extension, you need to authenticate with [database.dev](https://database.dev/). If you don't have an account, [sign-up for one](https://database.dev/sign-up) on the website. Then follow the steps below: 44 | 45 | 1. Make sure you are logged into the `database.dev` website. 46 | 2. Navigate to the **Access Tokens** page from the account drop-down at top right. 47 | 3. Click **New Token**. 48 | 4. Enter a token name and click **Create Token**. 49 | 5. Copy the generated token. Note that this is the only time the token will be shown. 50 | 6. On the terminal, run the `dbdev login` command. 51 | 7. Paste the token you copied. 52 | 53 | ## Publish your extension 54 | 55 | Now run the `dbdev publish` command to publish it. 56 | 57 | ``` 58 | dbdev publish 59 | ``` 60 | 61 | Your extension is now published to `database.dev` and visible under your account profile. You can visit your account profile from the account drop-down at the top right. Users can now [install your extension](install-a-package.md) by using the `dbdev add` command. 62 | 63 | 64 | ## Tips 65 | 66 | Here are a few useful tips for creating extensions. 67 | 68 | 69 | ### Don't hardcode schemas 70 | 71 | It's common to hardcode `public` into your SQL files, but this isn't a great practice. Remove all hard-coded schema references so that the user can choose to install the extension in other schemas. 72 | 73 | ### Relocatable schemas 74 | 75 | You can define a schema to be `relocatable` in the `.control` file: 76 | 77 | ``` 78 | default_version = 0.0.1 79 | comment = 'An basic extension.' 80 | relocatable = true 81 | ``` 82 | 83 | If this is `true`, users can choose where to install their schema: 84 | 85 | ```sql 86 | create extension 87 | schema 88 | version ; 89 | ``` 90 | 91 | If you need to reference the schema in your SQL files, you can use the `@extschema@` parameter: 92 | 93 | ```sql 94 | create function @extschema@.my_function(args) 95 | returns return_type 96 | language plpgsql 97 | as $$ 98 | begin 99 | -- function body 100 | end; 101 | $$; 102 | ``` 103 | -------------------------------------------------------------------------------- /supabase/migrations/20230323180034_reserved_user_accts.sql: -------------------------------------------------------------------------------- 1 | insert into auth.users(instance_id, id, aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data, is_sso_user) 2 | values 3 | ( 4 | '00000000-0000-0000-0000-000000000000', gen_random_uuid(), 'authenticated', 'authenticated', 'oliver@oliverrice.com', 'TBD', now(), 5 | '{"provider": "email", "providers": ["email"]}', '{"handle": "olirice", "display_name": "Oli", "bio": "Supabase Staff"}', false 6 | ), 7 | ( 8 | '00000000-0000-0000-0000-000000000000', gen_random_uuid(), 'authenticated', 'authenticated', 'alaister@supabase.io', 'TBD', now(), 9 | '{"provider": "email", "providers": ["email"]}', '{"handle": "alaister", "display_name": "Alaister", "bio": "Supabase Staff"}', false 10 | ), 11 | ( 12 | '00000000-0000-0000-0000-000000000000', gen_random_uuid(), 'authenticated', 'authenticated', 'copple@supabase.io', 'TBD', now(), 13 | '{"provider": "email", "providers": ["email"]}', '{"handle": "kiwicopple", "display_name": "Copple", "bio": "Supabase Staff"}', false 14 | ), 15 | ( 16 | '00000000-0000-0000-0000-000000000000', gen_random_uuid(), 'authenticated', 'authenticated', 'michel@supabase.io', 'TBD', now(), 17 | '{"provider": "email", "providers": ["email"]}', '{"handle": "michelp", "display_name": "Michele", "bio": "Supabase Staff"}', false 18 | ), 19 | ( 20 | '00000000-0000-0000-0000-000000000000', gen_random_uuid(), 'authenticated', 'authenticated', 'mark@supabase.io', 'TBD', now(), 21 | '{"provider": "email", "providers": ["email"]}', '{"handle": "burggraf", "display_name": "Mark", "bio": "Supabase Staff"}', false 22 | ); 23 | 24 | insert into app.handle_registry(handle, is_organization) 25 | values 26 | ('supabase', true), 27 | ('langchain', true), 28 | -- Reserve common impersonation handles 29 | ('admin', false), 30 | ('administrator', false), 31 | ('superuser', false), 32 | ('superadmin', false), 33 | ('root', false), 34 | ('user', false), 35 | ('guest', false), 36 | ('anon', false), 37 | ('authenticated', false), 38 | ('sysadmin', false), 39 | ('support', false), 40 | ('manager', false), 41 | ('default', false), 42 | ('staff', false), 43 | ('help', false), 44 | ('helpdesk', false), 45 | ('test', false), 46 | ('password', false), 47 | ('demo', false), 48 | ('service', false), 49 | ('info', false), 50 | ('webmaster', false), 51 | ('security', false), 52 | ('installer', false); 53 | 54 | begin; 55 | -- Required for trigger on handle registry 56 | select app.simulate_login('oliver@oliverrice.com'); 57 | 58 | insert into app.organizations(handle, display_name, bio) 59 | values 60 | ('supabase', 'Supabase', 'Build in a weekend, scale to millions'); 61 | end; 62 | 63 | insert into app.members(organization_id, account_id, role) 64 | select 65 | o.id, 66 | acc.id, 67 | 'maintainer' 68 | from 69 | app.organizations o, 70 | app.accounts acc 71 | where 72 | -- olirice is already a member because that account created it 73 | acc.handle <> 'olirice'; 74 | 75 | begin; 76 | -- Required for trigger on handle registry 77 | select app.simulate_login('oliver@oliverrice.com'); 78 | 79 | insert into app.organizations(handle, display_name, bio) 80 | values 81 | ('langchain', 'LangChain', 'LangChain is a framework for developing applications powered by language models'); 82 | end; 83 | -------------------------------------------------------------------------------- /website/pages/sign-in.tsx: -------------------------------------------------------------------------------- 1 | import { isAuthApiError } from '@supabase/supabase-js' 2 | import Head from 'next/head' 3 | import Link from 'next/link' 4 | import { useRouter } from 'next/router' 5 | import toast from 'react-hot-toast' 6 | import Form, { FORM_ERROR } from '~/components/forms/Form' 7 | import FormButton from '~/components/forms/FormButton' 8 | import FormInput from '~/components/forms/FormInput' 9 | import Layout from '~/components/layouts/Layout' 10 | import H1 from '~/components/ui/typography/h1' 11 | import { useSignInMutation } from '~/data/auth/sign-in-mutation' 12 | import { NextPageWithLayout } from '~/lib/types' 13 | import { SignInSchema } from '~/lib/validations' 14 | 15 | const SignInPage: NextPageWithLayout = () => { 16 | const router = useRouter() 17 | const { mutateAsync: signIn } = useSignInMutation({ 18 | onSuccess() { 19 | toast.success('You have signed in successfully!') 20 | router.replace('/') 21 | }, 22 | }) 23 | 24 | return ( 25 |
    26 | 27 | Sign In | The Database Package Manager 28 | 29 | 30 |
    31 |

    Sign In

    32 | 33 |
    { 39 | try { 40 | await signIn({ email, password }) 41 | } catch (error: any) { 42 | if (isAuthApiError(error)) { 43 | return { 44 | [FORM_ERROR]: error.message, 45 | } 46 | } 47 | 48 | return { 49 | [FORM_ERROR]: 50 | 'Sorry, we had an unexpected error. Please try again. - ' + 51 | error.toString(), 52 | } 53 | } 54 | }} 55 | schema={SignInSchema} 56 | > 57 |
    58 | 64 | 65 |
    66 | 72 |
    73 | 77 | Forgot your password? 78 | 79 |
    80 |
    81 |
    82 | 83 | Sign In 84 |

    85 | Need to create an account?{' '} 86 | 90 | Sign Up 91 | 92 |

    93 |
    94 |
    95 |
    96 | ) 97 | } 98 | 99 | SignInPage.getLayout = (page) => {page} 100 | 101 | export default SignInPage 102 | -------------------------------------------------------------------------------- /website/lib/auth.tsx: -------------------------------------------------------------------------------- 1 | import { Session } from '@supabase/supabase-js' 2 | import { useRouter } from 'next/router' 3 | import { 4 | ComponentType, 5 | createContext, 6 | PropsWithChildren, 7 | useCallback, 8 | useContext, 9 | useEffect, 10 | useMemo, 11 | useState, 12 | } from 'react' 13 | import supabase from './supabase' 14 | import { isNextPageWithLayout, NextPageWithLayout } from './types' 15 | 16 | /* Auth Context */ 17 | 18 | export type AuthContext = { refreshSession: () => Promise } & ( 19 | | { 20 | session: Session 21 | isLoading: false 22 | } 23 | | { 24 | session: null 25 | isLoading: true 26 | } 27 | | { 28 | session: null 29 | isLoading: false 30 | } 31 | ) 32 | 33 | export const AuthContext = createContext({ 34 | session: null, 35 | isLoading: true, 36 | refreshSession: () => Promise.resolve(null), 37 | }) 38 | 39 | export type AuthProviderProps = {} 40 | 41 | export const AuthProvider = ({ 42 | children, 43 | }: PropsWithChildren) => { 44 | const [session, setSession] = useState(null) 45 | const [isLoading, setIsLoading] = useState(true) 46 | 47 | useEffect(() => { 48 | let mounted = true 49 | 50 | supabase.auth 51 | .getSession() 52 | .then(({ data: { session } }) => { 53 | if (session && mounted) { 54 | setSession(session) 55 | } 56 | 57 | setIsLoading(false) 58 | }) 59 | .catch(() => { 60 | setIsLoading(false) 61 | }) 62 | 63 | return () => { 64 | mounted = false 65 | } 66 | }, []) 67 | 68 | useEffect(() => { 69 | const { 70 | data: { subscription }, 71 | } = supabase.auth.onAuthStateChange((_event, session) => { 72 | setSession(session) 73 | setIsLoading(false) 74 | }) 75 | 76 | return subscription.unsubscribe 77 | }, []) 78 | 79 | const refreshSession = useCallback(async () => { 80 | const { 81 | data: { session }, 82 | } = await supabase.auth.refreshSession() 83 | 84 | return session 85 | }, []) 86 | 87 | const value = useMemo(() => { 88 | if (session) { 89 | return { session, isLoading: false, refreshSession } as const 90 | } 91 | 92 | return { session: null, isLoading: isLoading, refreshSession } as const 93 | }, [session, isLoading, refreshSession]) 94 | 95 | return {children} 96 | } 97 | 98 | /* Auth Utils */ 99 | 100 | export const useAuth = () => useContext(AuthContext) 101 | 102 | export const useSession = () => useAuth().session 103 | 104 | export const useUser = () => useSession()?.user ?? null 105 | 106 | export const useIsLoggedIn = () => { 107 | const user = useUser() 108 | 109 | return user !== null 110 | } 111 | 112 | /* With Auth HOC */ 113 | 114 | export function withAuth( 115 | Component: ComponentType | NextPageWithLayout 116 | ) { 117 | const WithAuth: ComponentType = (props: any) => { 118 | const { push } = useRouter() 119 | const { session, isLoading } = useAuth() 120 | 121 | useEffect(() => { 122 | if (!isLoading && !session) { 123 | push('/sign-in') 124 | } 125 | }, [session, isLoading, push]) 126 | 127 | return 128 | } 129 | 130 | WithAuth.displayName = `withAuth(${Component.displayName})` 131 | 132 | if (isNextPageWithLayout(Component)) { 133 | ;(WithAuth as NextPageWithLayout).getLayout = Component.getLayout 134 | } 135 | 136 | return WithAuth 137 | } 138 | -------------------------------------------------------------------------------- /website/components/search/Search.tsx: -------------------------------------------------------------------------------- 1 | import * as Dialog from '@radix-ui/react-dialog' 2 | import { useRouter } from 'next/router' 3 | import { useEffect, useRef, useState } from 'react' 4 | import { usePackagesSearchQuery } from '~/data/packages/packages-search-query' 5 | import { useDebounce } from '~/lib/utils' 6 | import Spinner from '../ui/spinner' 7 | import SearchInput from './SearchInput' 8 | import SearchPackageRow from './SearchPackageRow' 9 | 10 | const Search = () => { 11 | const [searchValue, setSearchValue] = useState('') 12 | const [isOpen, setIsOpen] = useState(false) 13 | 14 | const containerRef = useRef(null) 15 | 16 | const onSearchChange = (value: string) => { 17 | setSearchValue(value) 18 | 19 | setIsOpen(value.trim().length > 0) 20 | } 21 | 22 | const query = useDebounce(searchValue, 300) 23 | 24 | const { data, isSuccess, isLoading, isError } = usePackagesSearchQuery( 25 | { 26 | query, 27 | }, 28 | { 29 | enabled: Boolean(query), 30 | keepPreviousData: true, 31 | } 32 | ) 33 | 34 | const router = useRouter() 35 | useEffect(() => { 36 | const handler = () => { 37 | setIsOpen(false) 38 | } 39 | 40 | router.events.on('routeChangeStart', handler) 41 | 42 | return () => { 43 | router.events.off('routeChangeStart', handler) 44 | } 45 | }, [router.events]) 46 | 47 | return ( 48 |
    49 | 50 | 51 |
    52 | 53 | 54 | 55 | { 58 | e.preventDefault() 59 | }} 60 | > 61 | {isLoading && ( 62 |
    63 | 64 |
    65 | )} 66 | 67 | {isError && ( 68 |
    69 |

    Something went wrong

    70 |
    71 | )} 72 | 73 | {isSuccess && 74 | (data.length > 0 ? ( 75 |
    76 | {data.map((pkg) => ( 77 | 83 | ))} 84 |
    85 | ) : ( 86 |
    87 |

    No results found

    88 |

    89 | To search packages in an organization, try prefixing your 90 | query with an @ symbol. 91 |
    92 | For example @supabase to see packages from Supabase. 93 |

    94 |
    95 | ))} 96 |
    97 |
    98 |
    99 |
    100 | ) 101 | } 102 | 103 | export default Search 104 | -------------------------------------------------------------------------------- /website/pages/sign-up.tsx: -------------------------------------------------------------------------------- 1 | import { isAuthApiError } from '@supabase/supabase-js' 2 | import Head from 'next/head' 3 | import Link from 'next/link' 4 | import { useRouter } from 'next/router' 5 | import toast from 'react-hot-toast' 6 | import Form, { FORM_ERROR } from '~/components/forms/Form' 7 | import FormButton from '~/components/forms/FormButton' 8 | import FormInput from '~/components/forms/FormInput' 9 | import Layout from '~/components/layouts/Layout' 10 | import H1 from '~/components/ui/typography/h1' 11 | import { useSignUpMutation } from '~/data/auth/sign-up-mutation' 12 | import { NextPageWithLayout } from '~/lib/types' 13 | import { SignUpSchema } from '~/lib/validations' 14 | 15 | const SignUpPage: NextPageWithLayout = () => { 16 | const router = useRouter() 17 | const { mutateAsync: signUp } = useSignUpMutation({ 18 | onSuccess() { 19 | toast.success( 20 | 'You have signed up successfully! Please check your email to confirm your account.' 21 | ) 22 | router.push('/sign-in') 23 | }, 24 | }) 25 | 26 | return ( 27 |
    28 | 29 | Sign Up | The Database Package Manager 30 | 31 | 32 |
    33 |

    Sign Up

    34 | 35 |
    { 43 | try { 44 | await signUp({ email, password, handle, displayName }) 45 | } catch (error: any) { 46 | if (isAuthApiError(error)) { 47 | return { 48 | [FORM_ERROR]: error.message, 49 | } 50 | } 51 | 52 | return { 53 | [FORM_ERROR]: 54 | 'Sorry, we had an unexpected error. Please try again. - ' + 55 | error.toString(), 56 | } 57 | } 58 | }} 59 | schema={SignUpSchema} 60 | > 61 |
    62 | 68 | 69 | 75 | 76 | 82 | 83 | 89 |
    90 | 91 | Sign Up 92 | 93 |

    94 | Already have an account?{' '} 95 | 99 | Sign In 100 | 101 |

    102 |
    103 |
    104 |
    105 | ) 106 | } 107 | 108 | SignUpPage.getLayout = (page) => {page} 109 | 110 | export default SignUpPage 111 | -------------------------------------------------------------------------------- /supabase/migrations/20231207113857_olirice@read_once-0.3.2.sql: -------------------------------------------------------------------------------- 1 | insert into app.package_versions(package_id, version_struct, sql, description_md) 2 | values ( 3 | (select id from app.packages where package_alias = 'olirice@read_once'), 4 | (0,3,2), 5 | $pkg$ 6 | 7 | -- Enforce requirements 8 | -- Workaround to https://github.com/aws/pg_tle/issues/183 9 | do $$ 10 | declare 11 | pg_cron_exists boolean = exists( 12 | select 1 13 | from pg_available_extensions 14 | where 15 | name = 'pg_cron' 16 | and installed_version is not null 17 | ); 18 | begin 19 | 20 | if not pg_cron_exists then 21 | raise 22 | exception '"olirice@read_once" requires "pg_cron"' 23 | using hint = 'Run "create extension pg_cron" and try again'; 24 | end if; 25 | end 26 | $$; 27 | 28 | 29 | create schema read_once; 30 | 31 | create unlogged table read_once.messages( 32 | id uuid primary key default gen_random_uuid(), 33 | contents text not null default '', 34 | created_at timestamp default now() 35 | ); 36 | 37 | revoke all on read_once.messages from public; 38 | revoke usage on schema read_once from public; 39 | 40 | create or replace function send_message( 41 | contents text 42 | ) 43 | returns uuid 44 | security definer 45 | volatile 46 | strict 47 | language sql 48 | as 49 | $$ 50 | insert into read_once.messages(contents) 51 | values ($1) 52 | returning id; 53 | $$; 54 | 55 | create or replace function read_message(id uuid) 56 | returns text 57 | security definer 58 | volatile 59 | strict 60 | language sql 61 | as 62 | $$ 63 | delete from read_once.messages 64 | where read_once.messages.id = $1 65 | returning contents; 66 | $$ 67 | $pkg$, 68 | 69 | $description_md$ 70 | 71 | # read_once 72 | 73 | A Supabase application for sending messages that can only be read once 74 | 75 | Features: 76 | - messages can only be read one time 77 | - messages are not logged in PostgreSQL write-ahead-log (WAL) 78 | 79 | ## Installation 80 | 81 | `pg_cron` is a dependency of `read_once`. 82 | Dependency resolution is currently under development. 83 | In the near future it will not be necessary to manually create dependencies. 84 | 85 | To expose the `send_message` and `read_message` functions over HTTP, install the extension in a schema that is on the search_path. 86 | 87 | 88 | For example: 89 | ```sql 90 | select dbdev.install('olirice@read_once'); 91 | create extension if not exists pg_cron; 92 | create extension "olirice@read_once" 93 | schema public 94 | version '0.3.1'; 95 | ``` 96 | 97 | 98 | ## HTTP API 99 | 100 | ### Create a Message 101 | 102 | ```sh 103 | curl -X POST https://.supabase.co/rest/v1/rpc/send_message \ 104 | -H 'apiKey: ' \ 105 | -H 'Content-Type: application/json' 106 | --data-raw '{"contents": "hello, dbdev!"} 107 | 108 | # Returns: "2989156b-2356-4543-9d1b-19dfb8ec3268" 109 | ``` 110 | 111 | ### Read a Message 112 | 113 | ```sh 114 | curl -X https://.supabase.co/rest/v1/rpc/read_message 115 | -H 'apiKey: ' \ 116 | -H 'Content-Type: application/json' \ 117 | --data-raw '{"id": "2989156b-2356-4543-9d1b-19dfb8ec3268"} 118 | 119 | # Returns: "hello, dbdev!" 120 | ``` 121 | 122 | 123 | ## SQL API 124 | 125 | ### Create a Message 126 | 127 | ```sql 128 | -- Creates a new messages and returns its unique id 129 | create or replace function send_message( 130 | contents text 131 | ) 132 | returns uuid 133 | ``` 134 | 135 | ### Read a Message 136 | 137 | ```sql 138 | -- Read a message by its id 139 | create or replace function read_message( 140 | id uuid 141 | ) 142 | returns text 143 | ``` 144 | $description_md$ 145 | ); 146 | -------------------------------------------------------------------------------- /supabase/migrations/20231205051816_add_default_version.sql: -------------------------------------------------------------------------------- 1 | -- default_version column has a default value '0.0.0' only temporarily because the column is not null. 2 | -- It will be removed below. 3 | alter table app.packages 4 | add column default_version_struct app.semver not null default app.text_to_semver('0.0.0'), 5 | add column default_version text generated always as (app.semver_to_text(default_version_struct)) stored; 6 | 7 | -- for now we set the default version to current latest version 8 | -- new client will allow users to set a specific default version in the control file 9 | update app.packages 10 | set default_version_struct = app.text_to_semver(pp.latest_version) 11 | from public.packages pp 12 | where packages.id = pp.id; 13 | 14 | -- now that every row has a valid default_version, remove the default value of '0.0.0' 15 | alter table app.packages 16 | alter column default_version_struct drop default; 17 | 18 | -- add new default_version column to the view 19 | create or replace view public.packages as 20 | select 21 | pa.id, 22 | pa.package_name, 23 | pa.handle, 24 | pa.partial_name, 25 | newest_ver.version as latest_version, 26 | newest_ver.description_md, 27 | pa.control_description, 28 | pa.control_requires, 29 | pa.created_at, 30 | pa.default_version 31 | from 32 | app.packages pa, 33 | lateral ( 34 | select * 35 | from app.package_versions pv 36 | where pv.package_id = pa.id 37 | order by pv.version_struct desc 38 | limit 1 39 | ) newest_ver; 40 | 41 | -- grant insert and update permissions to authenticated users on the new default_version_struct column 42 | grant insert (partial_name, handle, control_description, control_relocatable, control_requires, default_version_struct) 43 | on app.packages 44 | to authenticated; 45 | 46 | grant update (control_description, control_relocatable, control_requires, default_version_struct) 47 | on app.packages 48 | to authenticated; 49 | 50 | -- publish_package accepts an additional `default_version` argument 51 | drop function public.publish_package(app.valid_name, varchar, bool, text[]); 52 | create or replace function public.publish_package( 53 | package_name app.valid_name, 54 | package_description varchar(1000), 55 | relocatable bool default false, 56 | requires text[] default '{}', 57 | default_version text default null 58 | ) 59 | returns void 60 | language plpgsql 61 | as $$ 62 | declare 63 | account app.accounts = account from app.accounts account where id = auth.uid(); 64 | require text; 65 | begin 66 | if account.handle is null then 67 | raise exception 'user not logged in'; 68 | end if; 69 | 70 | if default_version is null then 71 | raise exception 'default_version is required. If you are on `dbdev` CLI version 0.1.5 or older upgrade to the latest version.'; 72 | end if; 73 | 74 | foreach require in array requires 75 | loop 76 | if not exists ( 77 | select true 78 | from app.allowed_extensions 79 | where 80 | name = require 81 | ) then 82 | raise exception '`requires` in the control file can''t have `%` in it', require; 83 | end if; 84 | end loop; 85 | 86 | insert into app.packages(handle, partial_name, control_description, control_relocatable, control_requires, default_version_struct) 87 | values (account.handle, package_name, package_description, relocatable, requires, app.text_to_semver(default_version)) 88 | on conflict on constraint packages_handle_partial_name_key 89 | do update 90 | set control_description = excluded.control_description, 91 | control_relocatable = excluded.control_relocatable, 92 | control_requires = excluded.control_requires, 93 | default_version_struct = excluded.default_version_struct; 94 | end; 95 | $$; 96 | -------------------------------------------------------------------------------- /cli/src/credential_store.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fs::{create_dir, read_to_string}, 4 | io::Write, 5 | }; 6 | 7 | use anyhow::anyhow; 8 | use dirs::home_dir; 9 | use serde::{Deserialize, Serialize}; 10 | use thiserror::Error; 11 | 12 | use crate::{secret::Secret, util::create_file}; 13 | 14 | #[derive(Serialize, Deserialize)] 15 | pub(crate) struct Credentials { 16 | #[serde(rename = "registries")] 17 | pub(crate) tokens: HashMap, 18 | } 19 | 20 | #[derive(Serialize, Deserialize)] 21 | pub(crate) struct Token { 22 | #[serde(rename = "token")] 23 | pub(crate) value: String, 24 | } 25 | 26 | #[derive(Error, Debug)] 27 | pub(crate) enum FindTokenError { 28 | #[error("token not found: {0}")] 29 | NotFound(String), 30 | } 31 | 32 | #[derive(Error, Debug)] 33 | pub(crate) enum CredentialsReadError { 34 | #[error("credentials file missing")] 35 | CredentialsFileMissing, 36 | 37 | #[error("error reading credentials file")] 38 | Io(#[from] std::io::Error), 39 | 40 | #[error("error parsing toml from credentials file")] 41 | Toml(#[from] toml::de::Error), 42 | } 43 | 44 | pub(crate) fn get_secret_from_stdin() -> anyhow::Result> { 45 | let secret = rpassword::prompt_password("Please paste the token found on database.dev: ")?; 46 | Ok(Secret::from(secret)) 47 | } 48 | 49 | impl Credentials { 50 | pub(crate) fn get_token(&self, registry_name: &str) -> Result<&Token, FindTokenError> { 51 | match self.tokens.get(registry_name) { 52 | Some(token) => Ok(token), 53 | None => Err(FindTokenError::NotFound(format!( 54 | "token for registry `{registry_name}` not found" 55 | ))), 56 | } 57 | } 58 | 59 | pub(crate) fn write(registry_name: &str, access_token: &Secret) -> anyhow::Result<()> { 60 | if let Some(home_dir) = home_dir() { 61 | let dbdev_dir = home_dir.join(".dbdev"); 62 | if !dbdev_dir.exists() { 63 | create_dir(&dbdev_dir)?; 64 | } 65 | 66 | let mut credentials = Self::read_or_create_credentials()?; 67 | let token = Token { 68 | value: access_token.expose().clone(), 69 | }; 70 | credentials.tokens.insert(registry_name.to_string(), token); 71 | let credentials_str = toml::to_string(&credentials)?; 72 | 73 | let credentials_file_path = dbdev_dir.join("credentials.toml"); 74 | let mut credentials_file = create_file(&credentials_file_path)?; 75 | credentials_file.write_all(credentials_str.as_bytes())?; 76 | } else { 77 | return Err(anyhow!("Failed to find home directory")); 78 | } 79 | 80 | Ok(()) 81 | } 82 | 83 | pub(crate) fn read() -> Result { 84 | if let Some(home_dir) = home_dir() { 85 | let dbdev_dir = home_dir.join(".dbdev"); 86 | if !dbdev_dir.exists() { 87 | return Err(CredentialsReadError::CredentialsFileMissing); 88 | } 89 | let credentials_file_path = dbdev_dir.join("credentials.toml"); 90 | if !credentials_file_path.exists() { 91 | return Err(CredentialsReadError::CredentialsFileMissing); 92 | } 93 | let credentials_str = read_to_string(&credentials_file_path)?; 94 | let credentials = toml::from_str(&credentials_str)?; 95 | Ok(credentials) 96 | } else { 97 | panic!("Failed to find home directory"); 98 | } 99 | } 100 | 101 | fn read_or_create_credentials() -> Result { 102 | match Self::read() { 103 | Ok(credentials) => Ok(credentials), 104 | Err(CredentialsReadError::CredentialsFileMissing) => Ok(Credentials { 105 | tokens: HashMap::new(), 106 | }), 107 | e => e, 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /website/components/ui/markdown.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentPropsWithoutRef } from 'react' 2 | import ReactMarkdown from 'react-markdown' 3 | import rehypeHighlight from 'rehype-highlight' 4 | import remarkGfm from 'remark-gfm' 5 | import { cn } from '~/lib/utils' 6 | import CopyButton from './copy-button' 7 | import A from './typography/a' 8 | import H1 from './typography/h1' 9 | import H2 from './typography/h2' 10 | import H3 from './typography/h3' 11 | import Li from './typography/li' 12 | import P from './typography/p' 13 | import Strong from './typography/strong' 14 | 15 | type MarkdownProps = ComponentPropsWithoutRef & { 16 | copyableCode?: boolean 17 | } 18 | 19 | function childrenToText(children: any): string { 20 | if (typeof children === 'string') { 21 | return children 22 | } 23 | 24 | if (Array.isArray(children)) { 25 | return children.map(childrenToText).join('') 26 | } 27 | 28 | if (children.props && children.props.children) { 29 | return childrenToText(children.props.children) 30 | } 31 | 32 | return '' 33 | } 34 | 35 | const DEFAULT_COMPONENTS: MarkdownProps['components'] = { 36 | pre({ node, children, className, ...props }) { 37 | return ( 38 |
     39 |         {children}
     40 |       
    41 | ) 42 | }, 43 | code({ node, className, children, ...props }) { 44 | const isInline = !className?.includes('language-') 45 | return ( 46 | 47 | {children} 48 | 49 | ) 50 | }, 51 | a({ node, children, ...props }) { 52 | return {children} 53 | }, 54 | p({ node, children, ...props }) { 55 | return

    {children}

    56 | }, 57 | li({ node, children, ...props }) { 58 | return
  • {children}
  • 59 | }, 60 | strong({ node, children, ...props }) { 61 | return {children} 62 | }, 63 | h1({ node, children, ...props }) { 64 | return

    {children}

    65 | }, 66 | h2({ node, children, ...props }) { 67 | return

    {children}

    68 | }, 69 | h3({ node, children, ...props }) { 70 | return

    {children}

    71 | }, 72 | th({ node, className, children, ...props }) { 73 | return ( 74 | 75 | {children} 76 | 77 | ) 78 | }, 79 | td({ node, className, children, ...props }) { 80 | return ( 81 | 82 | {children} 83 | 84 | ) 85 | }, 86 | } 87 | 88 | const COPYABLE_CODE_COMPONENTS: MarkdownProps['components'] = { 89 | code({ node, className, children, ...props }) { 90 | const isInline = !className?.includes('language-') 91 | return ( 92 | 93 | {!isInline && ( 94 | childrenToText(children)} 96 | className="absolute top-2 right-2" 97 | variant="light" 98 | /> 99 | )} 100 | 101 | {children} 102 | 103 | ) 104 | }, 105 | } 106 | 107 | const Markdown = ({ 108 | className, 109 | remarkPlugins = [], 110 | rehypePlugins = [], 111 | components, 112 | copyableCode = true, 113 | children, 114 | ...props 115 | }: MarkdownProps) => ( 116 | 127 | {children} 128 | 129 | ) 130 | 131 | export default Markdown 132 | -------------------------------------------------------------------------------- /supabase/migrations/20230331163909_olirice-read_once.sql: -------------------------------------------------------------------------------- 1 | insert into app.packages( 2 | handle, 3 | partial_name, 4 | control_description, 5 | control_relocatable, 6 | control_requires 7 | ) 8 | values ( 9 | 'olirice', 10 | 'read_once', 11 | 'Send messages that can only be read once', 12 | false, 13 | '{pg_cron}' 14 | ); 15 | 16 | 17 | insert into app.package_versions(package_id, version_struct, sql, description_md) 18 | values ( 19 | (select id from app.packages where package_name = 'olirice-read_once'), 20 | (0,3,1), 21 | $pkg$ 22 | 23 | -- Enforce requirements 24 | -- Workaround to https://github.com/aws/pg_tle/issues/183 25 | do $$ 26 | declare 27 | pg_cron_exists boolean = exists( 28 | select 1 29 | from pg_available_extensions 30 | where 31 | name = 'pg_cron' 32 | and installed_version is not null 33 | ); 34 | begin 35 | 36 | if not pg_cron_exists then 37 | raise 38 | exception '"olirice-read_once" requires "pg_cron"' 39 | using hint = 'Run "create extension pg_cron" and try again'; 40 | end if; 41 | end 42 | $$; 43 | 44 | 45 | create schema read_once; 46 | 47 | create unlogged table read_once.messages( 48 | id uuid primary key default gen_random_uuid(), 49 | contents text not null default '', 50 | created_at timestamp default now() 51 | ); 52 | 53 | revoke all on read_once.messages from public; 54 | revoke usage on schema read_once from public; 55 | 56 | create or replace function send_message( 57 | contents text 58 | ) 59 | returns uuid 60 | security definer 61 | volatile 62 | strict 63 | language sql 64 | as 65 | $$ 66 | insert into read_once.messages(contents) 67 | values ($1) 68 | returning id; 69 | $$; 70 | 71 | create or replace function read_message(id uuid) 72 | returns text 73 | security definer 74 | volatile 75 | strict 76 | language sql 77 | as 78 | $$ 79 | delete from read_once.messages 80 | where read_once.messages.id = $1 81 | returning contents; 82 | $$ 83 | $pkg$, 84 | 85 | $description_md$ 86 | 87 | # read_once 88 | 89 | A Supabase application for sending messages that can only be read once 90 | 91 | Features: 92 | - messages can only be read one time 93 | - messages are not logged in PostgreSQL write-ahead-log (WAL) 94 | 95 | ## Installation 96 | 97 | `pg_cron` is a dependency of `read_once`. 98 | Dependency resolution is currently under development. 99 | In the near future it will not be necessary to manually create dependencies. 100 | 101 | To expose the `send_message` and `read_message` functions over HTTP, install the extension in a schema that is on the search_path. 102 | 103 | 104 | For example: 105 | ```sql 106 | select dbdev.install('olirice-read_once'); 107 | create extension if not exists pg_cron; 108 | create extension "olirice-read_once" 109 | schema public 110 | version '0.3.1'; 111 | ``` 112 | 113 | 114 | ## HTTP API 115 | 116 | ### Create a Message 117 | 118 | ```sh 119 | curl -X POST https://.supabase.co/rest/v1/rpc/send_message \ 120 | -H 'apiKey: ' \ 121 | -H 'Content-Type: application/json' 122 | --data-raw '{"contents": "hello, dbdev!"} 123 | 124 | # Returns: "2989156b-2356-4543-9d1b-19dfb8ec3268" 125 | ``` 126 | 127 | ### Read a Message 128 | 129 | ```sh 130 | curl -X https://.supabase.co/rest/v1/rpc/read_message 131 | -H 'apiKey: ' \ 132 | -H 'Content-Type: application/json' \ 133 | --data-raw '{"id": "2989156b-2356-4543-9d1b-19dfb8ec3268"} 134 | 135 | # Returns: "hello, dbdev!" 136 | ``` 137 | 138 | 139 | ## SQL API 140 | 141 | ### Create a Message 142 | 143 | ```sql 144 | -- Creates a new messages and returns its unique id 145 | create or replace function send_message( 146 | contents text 147 | ) 148 | returns uuid 149 | ``` 150 | 151 | ### Read a Message 152 | 153 | ```sql 154 | -- Read a message by its id 155 | create or replace function read_message( 156 | id uuid 157 | ) 158 | returns text 159 | ``` 160 | $description_md$ 161 | ); 162 | --------------------------------------------------------------------------------