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 |
22 | {children}
23 |
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 |
31 | Go back home
32 |
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 |
deleteAccessToken({ tokenId })}
38 | disabled={isDeletingAccessToken}
39 | >
40 | Revoke
41 |
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 |
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 | {
51 | copyToClipboardWithMeta(getValue())
52 | setHasCopied(true)
53 | }}
54 | {...props}
55 | >
56 | Copy
57 | {hasCopied ? (
58 |
59 | ) : (
60 |
61 | )}
62 |
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 |
42 |
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 |
5 |
6 |
7 |
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 |
45 | {label}
46 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------